1 <?xml version="1.0" encoding="UTF-8"?>
3 <!-- EN-Revision: 20827 -->
4 <sect1 id="zend.openid.consumer">
5 <title>Zend_OpenId_Consumer の基本</title>
7 <classname>Zend_OpenId_Consumer</classname> を使用して、
8 ウェブサイト上の OpenID 認証スキーマを実装します。
11 <sect2 id="zend.openid.consumer.authentication">
12 <title>OpenID Authentication</title>
27 OpenID の識別子を受け取り、それを OpenID プロバイダに渡す。
33 OpenID プロバイダからの応答を検証する。
39 実際のところ、OpenID 認証プロトコルはもう少し複雑な手順を踏んでいます。
40 しかしその大半は <classname>Zend_OpenId_Consumer</classname>
41 の中にカプセル化されており、開発者側が意識する必要はありません。
45 OpenID 認証手続きはエンドユーザ側から始まるもので、
46 まず認証情報を適切な形式で入力してそれを送信するところから始まります。
47 次の例は、OpenID 識別子を受け付けるシンプルなフォームを表示するものです。
48 このサンプルはログイン画面を表示するだけのものであることに注意しましょう。
51 <example id="zend.openid.consumer.example-1">
52 <title>シンプルな OpenID ログインフォーム</title>
53 <programlisting language="php"><![CDATA[
55 <form method="post" action="example-1_2.php"><fieldset>
56 <legend>OpenID ログイン</legend>
57 <input type="text" name="openid_identifier">
58 <input type="submit" name="openid_action" value="login">
59 </fieldset></form></body></html>
64 このフォームを送信すると、OpenID 識別子が継ぎの <acronym>PHP</acronym>
67 この <acronym>PHP</acronym> スクリプトで必要なのは、
68 <methodname>Zend_OpenId_Consumer::login()</methodname>
70 このメソッドの最初の引数は OpenID 識別子で、
71 2 番目の引数はスクリプトの <acronym>URL</acronym> となります。
72 ここで指定したスクリプトが認証の第三段階を処理します。
75 <example id="zend.openid.consumer.example-1_2">
76 <title>認証リクエストのハンドラ</title>
77 <programlisting language="php"><![CDATA[
78 $consumer = new Zend_OpenId_Consumer();
79 if (!$consumer->login($_POST['openid_identifier'], 'example-1_3.php')) {
80 die("OpenID でのログインに失敗しました。");
86 <methodname>Zend_OpenId_Consumer::login()</methodname>
87 は指定された識別子を調べ、成功した場合には識別プロバイダのアドレスと
90 サイトとプロバイダが同じ秘密情報を共有するようにします。
91 この情報を使用してそれ以降のメッセージの署名を行います。
92 それから、認証リクエストをプロバイダに渡します。
93 このリクエストは、エンドユーザ側のウェブブラウザから
94 OpenID サーバサイトにリダイレクトされることに注意しましょう。
95 ユーザは、その後も認証手続きを進めることができます。
99 OpenID サーバがユーザに通常たずねるのは、
100 パスワード (ユーザがまだログインしていない場合) や
101 ユーザがこのサイトを信頼しているかどうか、
102 そしてそのサイトからどんな情報を返すかといった内容です。
103 これらのやりとりは、OpenID 対応のサイトからは見えない状態になるので、
104 ユーザのパスワードやその他の情報はオープンにはなりません。
108 成功した場合は <methodname>Zend_OpenId_Consumer::login()</methodname>
109 は何も返さずに <acronym>HTTP</acronym> リダイレクトを行います。
110 エラーが発生した場合は <constant>FALSE</constant> を返します。
111 エラーが発生するのは、たとえば識別子が無効だったり
112 プロバイダが死んでいたり、通信障害が発生したりした場合などです。
116 認証の第三段階の処理は、ユーザのパスワードによる認証を終えた
117 OpenID プロバイダからの応答によって始まります。
118 この応答は、ウェブブラウザの <acronym>HTTP</acronym> リダイレクトによって間接的に渡されます。
119 サイト側では、この応答が正しいものであるかどうかだけを確認することになります。
122 <example id="zend.openid.consumer.example-1_3">
123 <title>認証の応答の検証</title>
124 <programlisting language="php"><![CDATA[
125 $consumer = new Zend_OpenId_Consumer();
126 if ($consumer->verify($_GET, $id)) {
127 echo "有効 " . htmlspecialchars($id);
129 echo "無効 " . htmlspecialchars($id);
135 この検証は <classname>Zend_OpenId_Consumer::verify</classname>
137 <acronym>HTTP</acronym> リクエストの引数の配列全体を受け取って、
138 そのレスポンスが適切な OpenID プロバイダによって署名されたものかどうかを調べます。
140 OpenID 識別子を 2 番目の (オプションの) 引数として渡します。
144 <sect2 id="zend.openid.consumer.combine">
145 <title>すべての処理をひとつのページにまとめる</title>
147 次の例は、これらの三段階をひとつにまとめたものです。
149 唯一の利点は、次の段階を処理するスクリプトの
150 <acronym>URL</acronym> を指定しなくてもよくなるということです。
151 デフォルトでは、すべての段階を同じ <acronym>URL</acronym> で処理します。
152 ただ、このスクリプトの内部にはディスパッチ用のコードが含まれており、
153 認証の各段階に応じて適切なコードに処理を振り分けるようになっています。
156 <example id="zend.openid.consumer.example-2">
157 <title>完全な OpenID ログインスクリプト</title>
158 <programlisting language="php"><![CDATA[
161 if (isset($_POST['openid_action']) &&
162 $_POST['openid_action'] == "login" &&
163 !empty($_POST['openid_identifier'])) {
165 $consumer = new Zend_OpenId_Consumer();
166 if (!$consumer->login($_POST['openid_identifier'])) {
167 $status = "OpenID でのログインに失敗しました。";
169 } else if (isset($_GET['openid_mode'])) {
170 if ($_GET['openid_mode'] == "id_res") {
171 $consumer = new Zend_OpenId_Consumer();
172 if ($consumer->verify($_GET, $id)) {
173 $status = "有効 " . htmlspecialchars($id);
175 $status = "無効 " . htmlspecialchars($id);
177 } else if ($_GET['openid_mode'] == "cancel") {
183 <?php echo "$status<br>" ?>
186 <legend>OpenID ログイン</legend>
187 <input type="text" name="openid_identifier" value=""/>
188 <input type="submit" name="openid_action" value="login"/>
197 キャンセルされた場合と認証の応答が間違っていた場合を区別しています。
199 識別プロバイダがその識別子について知らなかった場合や
201 あるいはユーザがそのサイトを信頼しない場合などです。
207 <sect2 id="zend.openid.consumer.realm">
208 <title>コンシューマレルム</title>
210 OpenID 対応のサイトがプロバイダへの認証リクエストを通過すると、
211 自分自身をレルム <acronym>URL</acronym> で識別するようになります。
212 この <acronym>URL</acronym> は、信頼済みサイトのルートとみなされます。
213 ユーザがその <acronym>URL</acronym> を信頼すると、
214 その配下の <acronym>URL</acronym> も同様に信頼することになります。
218 デフォルトでは、レルム <acronym>URL</acronym> は自動的にログインスクリプトがあるディレクトリの
219 <acronym>URL</acronym> に設定されます。大半の場合はこれで大丈夫ですが、
221 際と全体で共通のログインスクリプトを使用している場合や、
222 ひとつのドメインで複数のサーバを組み合わせて使用している場合などです。
227 <classname>Zend_OpenId_Consumer::login</classname> メソッドの 3 番目の引数として渡すことができます。
228 次の例は、すべての php.net サイトへの信頼済みアクセスを一度に確認するものです。
231 <example id="zend.openid.consumer.example-3_2">
232 <title>指定したレルムへの認証リクエスト</title>
233 <programlisting language="php"><![CDATA[
234 $consumer = new Zend_OpenId_Consumer();
235 if (!$consumer->login($_POST['openid_identifier'],
237 'http://*.php.net/')) {
238 die("OpenID でのログインに失敗しました。");
244 以下の例では、認証の第二段階のみを実装しています。
245 それ以外の段階については最初の例と同じです。
249 <sect2 id="zend.openid.consumer.check">
253 サーバにそのユーザがログインしているかどうかを
254 ユーザとのやりとりなしに知りたいこともあります。
255 そのような場合に最適なメソッドが <classname>Zend_OpenId_Consumer::check</classname>
256 です。このメソッドの引数は <classname>Zend_OpenId_Consumer::login</classname>
257 とまったく同じですが、ユーザ側には OpenID サーバのページを一切見せません。
258 したがって、ユーザ側から見れば処理は透過的に行われ、
259 まるで他のサイトに一切移動していないように見えるようになります。
260 そのユーザがすでにログインしており、かつそのサイトを信頼している場合に
261 第三段階の処理が成功し、それ以外の場合は失敗します。
264 <example id="zend.openid.consumer.example-4">
265 <title>対話形式でない即時確認</title>
266 <programlisting language="php"><![CDATA[
267 $consumer = new Zend_OpenId_Consumer();
268 if (!$consumer->check($_POST['openid_identifier'], 'example-4_3.php')) {
269 die("OpenID でのログインに失敗しました。");
275 以下の例では、認証の第二段階のみを実装しています。
276 それ以外の段階については最初の例と同じです。
280 <sect2 id="zend.openid.consumer.storage">
281 <title>Zend_OpenId_Consumer_Storage</title>
283 OpenID の認証手続きは三段階に分かれており、
284 それぞれで別々の <acronym>HTTP</acronym> リクエストを使用します。
285 それらのリクエスト間で情報を保存するため、
286 <classname>Zend_OpenId_Consumer</classname> では内部ストレージを使用します。
290 開発者は特にこのストレージを気にする必要はありません。
291 デフォルトで、<classname>Zend_OpenId_Consumer</classname>
292 は /tmp 配下のファイルベースのストレージを使用するからです。
293 これは <acronym>PHP</acronym> のセッションと同じ挙動です。
294 しかし、このストレージがあらゆる場合にうまく使えるというわけではありません。
295 たとえばその手の情報はデータベースに保存したいという人もいるでしょうし、
296 大規模なウェブファームで共通のストレージを使用したいこともあるでしょう。
297 幸いなことに、このデフォルトのストレージは簡単に変更できます。
298 そのために必要なのは、<classname>Zend_OpenId_Consumer_Storage</classname>
299 クラスを継承した独自のストレージクラスを実装して
300 それを <classname>Zend_OpenId_Consumer</classname>
301 のコンストラクタへの最初の引数として渡すことだけです。
305 次の例は、バックエンドとして <classname>Zend_Db</classname>
306 を使用するシンプルなストレージです。三種類の機能を持っています。
307 最初の機能は関連付けの情報、そして 2 番目が確認した内容のキャッシュ、
308 そして 3 番目が応答の一意性の確認です。このクラスは、
309 既存のデータベースや新しいデータベースで簡単に使用できるように実装されています。
310 必要に応じて、もしまだテーブルが存在しなければ自動的にテーブルを作成します。
313 <example id="zend.openid.consumer.example-5">
314 <title>データベースストレージ</title>
315 <programlisting language="php"><![CDATA[
316 class DbStorage extends Zend_OpenId_Consumer_Storage
319 private $_association_table;
320 private $_discovery_table;
321 private $_nonce_table;
323 // Zend_Db_Adapter オブジェクトと
325 public function __construct($db,
326 $association_table = "association",
327 $discovery_table = "discovery",
328 $nonce_table = "nonce")
331 $this->_association_table = $association_table;
332 $this->_discovery_table = $discovery_table;
333 $this->_nonce_table = $nonce_table;
334 $tables = $this->_db->listTables();
336 // アソシエーションテーブルが存在しない場合は作成します
337 if (!in_array($association_table, $tables)) {
338 $this->_db->getConnection()->exec(
339 "create table $association_table (" .
340 " url varchar(256) not null primary key," .
341 " handle varchar(256) not null," .
342 " macFunc char(16) not null," .
343 " secret varchar(256) not null," .
344 " expires timestamp" .
348 // ディスカバリーテーブルが存在しない場合は作成します
349 if (!in_array($discovery_table, $tables)) {
350 $this->_db->getConnection()->exec(
351 "create table $discovery_table (" .
352 " id varchar(256) not null primary key," .
353 " realId varchar(256) not null," .
354 " server varchar(256) not null," .
356 " expires timestamp" .
360 // ノンステーブルが存在しない場合は作成します
361 if (!in_array($nonce_table, $tables)) {
362 $this->_db->getConnection()->exec(
363 "create table $nonce_table (" .
364 " nonce varchar(256) not null primary key," .
365 " created timestamp default current_timestamp" .
370 public function addAssociation($url,
376 $table = $this->_association_table;
377 $secret = base64_encode($secret);
378 $this->_db->insert($table, array(
381 'macFunc' => $macFunc,
383 'expires' => $expires,
388 public function getAssociation($url,
394 $table = $this->_association_table;
396 $table, $this->_db->quoteInto('expires < ?', time())
398 $select = $this-_db->select()
399 ->from($table, array('handle', 'macFunc', 'secret', 'expires'))
400 ->where('url = ?', $url);
401 $res = $this->_db->fetchRow($select);
403 if (is_array($res)) {
404 $handle = $res['handle'];
405 $macFunc = $res['macFunc'];
406 $secret = base64_decode($res['secret']);
407 $expires = $res['expires'];
413 public function getAssociationByHandle($handle,
419 $table = $this->_association_table;
421 $table, $this->_db->quoteInto('expires < ', time())
423 $select = $this->_db->select()
424 ->from($table, array('url', 'macFunc', 'secret', 'expires')
425 ->where('handle = ?', $handle);
426 $res = $select->fetchRow($select);
428 if (is_array($res)) {
430 $macFunc = $res['macFunc'];
431 $secret = base64_decode($res['secret']);
432 $expires = $res['expires'];
438 public function delAssociation($url)
440 $table = $this->_association_table;
441 $this->_db->query("delete from $table where url = '$url'");
445 public function addDiscoveryInfo($id,
451 $table = $this->_discovery_table;
452 $this->_db->insert($table, array(
456 'version' => $version,
457 'expires' => $expires,
463 public function getDiscoveryInfo($id,
469 $table = $this->_discovery_table;
470 $this->_db->delete($table, $this->quoteInto('expires < ?', time()));
471 $select = $this->_db->select()
472 ->from($table, array('realId', 'server', 'version', 'expires'))
473 ->where('id = ?', $id);
474 $res = $this->_db->fetchRow($select);
476 if (is_array($res)) {
477 $realId = $res['realId'];
478 $server = $res['server'];
479 $version = $res['version'];
480 $expires = $res['expires'];
486 public function delDiscoveryInfo($id)
488 $table = $this->_discovery_table;
489 $this->_db->delete($table, $this->_db->quoteInto('id = ?', $id));
493 public function isUniqueNonce($nonce)
495 $table = $this->_nonce_table;
497 $ret = $this->_db->insert($table, array(
500 } catch (Zend_Db_Statement_Exception $e) {
506 public function purgeNonces($date=null)
511 $db = Zend_Db::factory('Pdo_Sqlite',
512 array('dbname'=>'/tmp/openid_consumer.db'));
513 $storage = new DbStorage($db);
514 $consumer = new Zend_OpenId_Consumer($storage);
519 このサンプルには OpenID の認証コードそのものは含まれません。
520 しかし、先ほどの例やこの後の例と同じロジックに基づいています。
524 <sect2 id="zend.openid.consumer.sreg">
525 <title>Simple Registration Extension</title>
527 認証に加えて、OpenID は軽量なプロファイル交換のためにも使用できます。
528 この機能は OpenID 認証の仕様ではカバーされておらず、
529 OpenID Simple Registration Extension プロトコルで対応しています。
531 OpenID 対応のサイトがエンドユーザに関する情報を
532 OpenID プロバイダから取得できるようになります。
533 取得できる情報には次のようなものがあります。
539 <emphasis>nickname</emphasis>
540 - ユーザがニックネームとして使用している UTF-8 文字列。
545 <emphasis>email</emphasis>
546 - エンドユーザのメールアドレス。RFC2822 のセクション 3.4.1
552 <emphasis>fullname</emphasis>
553 - エンドユーザのフルネームを表す UTF-8 文字列。
558 <emphasis>dob</emphasis>
559 - エンドユーザの誕生日を YYYY-MM-DD 形式で表したもの。
560 指定されている桁数より少ない場合は、ゼロ埋めされます。
562 エンドユーザがこの情報の公開を希望しない場合は、
563 その部分の値をゼロに設定する必要があります。
564 たとえば、1980 年生まれであることは公開するが
565 月や日は公開したくないというエンドユーザの場合、
566 返される値は "1980-00-00" となります。
571 <emphasis>gender</emphasis>
572 - エンドユーザの姓。"M" が男性で "F" が女性。
577 <emphasis>postcode</emphasis>
578 - エンドユーザの国の郵便システムに対応した UTF-8 文字列。
583 <emphasis>country</emphasis>
584 - エンドユーザの居住地 (国) を ISO3166 形式で表したもの。
589 <emphasis>language</emphasis>
590 - エンドユーザの使用言語を ISO639 形式で表したもの。
595 <emphasis>timezone</emphasis>
596 - TimeZone データベースの <acronym>ASCII</acronym> 文字列。
597 "Europe/Paris" あるいは "America/Los_Angeles" など。
604 これらのフィールドの任意の組み合わせについて問い合わせられます。
605 また、いくつかの情報についてのみ厳密に問い合わせを行い、
606 それ以外の情報については開示するかしないかをユーザに決めさせることもできます。
607 次の例は、<emphasis>nickname</emphasis> およびオプションで
608 <emphasis>email</emphasis> と <emphasis>fullname</emphasis>
609 を要求する <classname>Zend_OpenId_Extension_Sreg</classname>
613 <example id="zend.openid.consumer.example-6_2">
614 <title>Simple Registration Extension のリクエストの送信</title>
615 <programlisting language="php"><![CDATA[
616 $sreg = new Zend_OpenId_Extension_Sreg(array(
619 'fullname'=>false), null, 1.1);
620 $consumer = new Zend_OpenId_Consumer();
621 if (!$consumer->login($_POST['openid_identifier'],
625 die("OpenID でのログインに失敗しました。");
631 見てのとおり、<classname>Zend_OpenId_Extension_Sreg</classname>
632 のコンストラクタに渡すのは問い合わせたいフィールドの配列です。
633 この配列のインデックスはフィールド名、値はフラグとなります。
634 <constant>TRUE</constant> はそのフィールドが必須であること、そして
635 <constant>FALSE</constant> はそのフィールドがオプションであることを表します。
636 <classname>Zend_OpenId_Consumer::login</classname> の 4 番目の引数には、
637 extension あるいは extension のリストを指定できます。
641 認証の第三段階で、<classname>Zend_OpenId_Extension_Sreg</classname>
642 オブジェクトが <classname>Zend_OpenId_Consumer::verify</classname>
644 <classname>Zend_OpenId_Extension_Sreg::getProperties</classname>
648 <example id="zend.openid.consumer.example-6_3">
649 <title>Simple Registration Extension の応答内容の検証</title>
650 <programlisting language="php"><![CDATA[
651 $sreg = new Zend_OpenId_Extension_Sreg(array(
654 'fullname'=>false), null, 1.1);
655 $consumer = new Zend_OpenId_Consumer();
656 if ($consumer->verify($_GET, $id, $sreg)) {
657 echo "有効 " . htmlspecialchars($id) . "<br>\n";
658 $data = $sreg->getProperties();
659 if (isset($data['nickname'])) {
660 echo "nickname: " . htmlspecialchars($data['nickname']) . "<br>\n";
662 if (isset($data['email'])) {
663 echo "email: " . htmlspecialchars($data['email']) . "<br>\n";
665 if (isset($data['fullname'])) {
666 echo "fullname: " . htmlspecialchars($data['fullname']) . "<br>\n";
669 echo "無効 " . htmlspecialchars($id);
675 引数を渡さずに <classname>Zend_OpenId_Extension_Sreg</classname>
676 を作成した場合は、必要なデータが存在するかどうかを
677 ユーザ側のコードで調べなければなりません。
678 しかし、第二段階で必要となるフィールドと同じ内容のリストでオブジェクトを作成した場合は、
679 必要なデータの存在は自動的にチェックされます。
680 この場合、必須フィールドのいずれかが存在しなければ
681 <classname>Zend_OpenId_Consumer::verify</classname> は
682 <constant>FALSE</constant> を返します。
686 デフォルトでは <classname>Zend_OpenId_Extension_Sreg</classname> はバージョン
687 1.0 を使用します。バージョン 1.1 の仕様はまだ確定していないからです。
688 しかし、中にはバージョン 1.0 の機能では完全にはサポートしきれないライブラリもあります。
689 たとえば www.myopenid.com ではリクエストに SREG
690 名前空間が必須となりますが、これは 1.1 にしか存在しません。
691 このサーバを使用する場合は、<classname>Zend_OpenId_Extension_Sreg</classname>
692 のコンストラクタで明示的にバージョン 1.1 を指定する必要があります。
696 <classname>Zend_OpenId_Extension_Sreg</classname> のコンストラクタの 2 番目の引数は、
697 ポリシーの <acronym>URL</acronym> です。これは、識別プロバイダがエンドユーザに提供する必要があります。
701 <sect2 id="zend.openid.consumer.auth">
702 <title>Zend_Auth との統合</title>
704 Zend Framework には、ユーザ認証用のクラスが用意されています。
705 そう、<classname>Zend_Auth</classname> のことです。
706 このクラスを <classname>Zend_OpenId_Consumer</classname>
707 と組み合わせて使うこともできます。次の例は、
708 <code>OpenIdAdapter</code> が
709 <classname>Zend_Auth_Adapter_Interface</classname> の
710 <code>authenticate</code> メソッドを実装する方法を示すものです。
715 このアダプタと既存のアダプタの大きな違いは、
716 このアダプタが 2 回の <acronym>HTTP</acronym> リクエストで動作することと
717 OpenID 認証の第二段階、第三段階用に処理を振り分けるコードがあることです。
720 <example id="zend.openid.consumer.example-7">
721 <title>OpenID 用の Zend_Auth アダプタ</title>
722 <programlisting language="php"><![CDATA[
724 class OpenIdAdapter implements Zend_Auth_Adapter_Interface {
727 public function __construct($id = null) {
731 public function authenticate() {
734 $consumer = new Zend_OpenId_Consumer();
735 if (!$consumer->login($id)) {
740 $consumer = new Zend_OpenId_Consumer();
741 if ($consumer->verify($_GET, $id)) {
749 return new Zend_Auth_Result($ret, $id, array($msg));
754 $auth = Zend_Auth::getInstance();
755 if ((isset($_POST['openid_action']) &&
756 $_POST['openid_action'] == "login" &&
757 !empty($_POST['openid_identifier'])) ||
758 isset($_GET['openid_mode'])) {
759 $adapter = new OpenIdAdapter(@$_POST['openid_identifier']);
760 $result = $auth->authenticate($adapter);
761 if ($result->isValid()) {
762 Zend_OpenId::redirect(Zend_OpenId::selfURL());
764 $auth->clearIdentity();
765 foreach ($result->getMessages() as $message) {
766 $status .= "$message<br>\n";
769 } else if ($auth->hasIdentity()) {
770 if (isset($_POST['openid_action']) &&
771 $_POST['openid_action'] == "logout") {
772 $auth->clearIdentity();
774 $status = $auth->getIdentity() . " としてログインしました。<br>\n";
779 <?php echo htmlspecialchars($status);?>
780 <form method="post"><fieldset>
781 <legend>OpenID ログイン</legend>
782 <input type="text" name="openid_identifier" value="">
783 <input type="submit" name="openid_action" value="login">
784 <input type="submit" name="openid_action" value="logout">
785 </fieldset></form></body></html>
790 <classname>Zend_Auth</classname> と組み合わせた場合、
791 エンドユーザの識別子はセッションに保存されます。
792 これを取得するには <classname>Zend_Auth::hasIdentity</classname>
793 および <classname>Zend_Auth::getIdentity</classname>
798 <sect2 id="zend.openid.consumer.mvc">
799 <title>Zend_Controller との統合</title>
801 最後に、Model-View-Controller
802 アプリケーションへの組み込みについて簡単に説明しておきます。
803 Zend Framework のアプリケーションは
804 <classname>Zend_Controller</classname> クラスを使用して実装されており、
805 エンドユーザのウェブブラウザに返す <acronym>HTTP</acronym> レスポンスは
806 <classname>Zend_Controller_Response_Http</classname>
807 クラスのオブジェクトを使用して準備しています。
811 <classname>Zend_OpenId_Consumer</classname> には GUI 機能はありませんが、
812 <classname>Zend_OpenId_Consumer::login</classname> および
813 <classname>Zend_OpenId_Consumer::check</classname>
814 に成功した場合に <acronym>HTTP</acronym> リダイレクトを行います。
815 もしそれ以前に何らかの情報がウェブブラウザに送信されていると、
817 <acronym>MVC</acronym> コードで <acronym>HTTP</acronym> リダイレクトを正しく機能させるため、
818 <classname>Zend_OpenId_Consumer::login</classname> あるいは
819 <classname>Zend_OpenId_Consumer::check</classname> の最後の引数に
820 <classname>Zend_Controller_Response_Http</classname> を渡す必要があります。