2010年11月21日日曜日

PHPで 「Webページの有効期限が切れてます」となる時の傾向と対策

PHPでフォーム等を作った場合、Webブラウザの戻るボタンやJavaScriptのhistory.back()で前のページに戻った時に「Webページの有効期限が切れてます」と表示されることがある。

上記はIEの場合で、ブラウザによって少し挙動が違う(下記)。
いずれもページを更新(リロード)するとサーバにPOSTが再送信され、ページが表示される。
  • IE
    • 上記(IE8の例)のような画面が表示される。
  • Firefox
    • 「このページを表示するにはフォームデータを再度送信する必要があります。フォームデータを再送信すると以前実行した検索、投稿や注文などの処理が繰り返されます。」という確認ダイアログが表示され、「再送信」ボタンと「キャンセル」ボタンが表示される。「再送信ボタン」をクリックするとページが表示される。
  • Chrome
    • 「フォーム再送信の確認
      このウェブページを正しく表示するには、先ほど入力したデータが必要です。データをもう一度送信することは可能ですが、このページで行った操作をすべて繰り返すことになります。データを再送信してこのページを表示する場合は [再読み込み] をクリックしてください。」というメッセージのみの画面が表示される。
  • Safari
    • このようなダイアログが表示される。(Windows版で確認。Mac版は分からない。)



犯人はだれだ?


原因は、PHPでSESSIONを使うと(デフォルト設定では)自動でキャッシュ制御用のHTTPヘッダーが送出され、それによりクライアント側のキャッシュが使用不可にされるため。(参考:floatingdays: PHPでブラウザキャッシュを有効にする

つまり、下記条件を全て満たした場合にこの現象が発生する。
  1. HTTP POSTで遷移してきた。
  2. SESSIONを使っている。(session_start()してるか、php.ini等でsession.auto_start=1に設定している。)
  3. 次のページに行ってから、ブラウザの履歴機能(JavaScriptのhistory.back()を含む)で戻ってきた。



じゃあどうすればいいの?

対策として有名なのは、session_start()する前に、session_cache_limiter('none')とすること。
session_cache_limiter('none');
session_start();
これにより、SESSIONを使っても余計なHTTPヘッダーが送出されなくなる。

または、php.ini等でsession.cache_limiterに"none"を設定しても同じことになる。(おそらく元ネタはこのあたりだろう → PHP: session_cache_limiter - Manual

実際にこれで問題は解決する。



異論反論オブジェクション! [shut very bad!]

しかし、「PHP/「ページの有効期限切れ」対策 - Glamenv-Septzen.net」によると、これはPHPが想定しているパラメータではなく、お行儀のよいやり方ではないらしい。

'none'というパラメータは正しいパラメータではなく、それゆえに何もHTTPヘッダーを送出しないという挙動になるらしい。
(ただし、もし「規定外のパラメータはスルーする」というのが意図した仕様だとしたら、'none'でも何でも「正しいパラメータ」だけど。)

上記の記事ではsession_cache_limiter('none')ではなくsession_cache_limiter('private_no_expire')を推奨している。
「ページの有効期限切れ」をsession_cache_limiter()で解決 - shinyanakaoのよすがブログ」でも同様にprivate_no_expireを推奨している。

しかし、実際にsession_cache_limiter('private_no_expire')を使うと、やはり余分なHTTPヘッダーが送出される。

private_no_expireの場合、(privateに比べて)Expiresが送出されなくなるが、「Cache-Control: max-age=(session.cache_expire ぶんだけ未来)」が送出されるため、やはりブラウザに影響が出てしまう。(参考:現在のキャッシュリミッタを取得または設定する - PHP 5.3 日本語マニュアル
Firefoxでは問題ないが、IEだとリロード時にもキャッシュを使ってしまい、サーバからのリロードができなくなるようだ。(参考:Webアプリケーション開発ラボ by NPO情報活用センター - PHP:キャッシュ問題について。PHP Tips|ワークスポット・ジェーピー
おそらくsession.cache_expireで設定されている期間はキャッシュが有効になるのだろう。(Expiresでそう指定しているのだから、IEは悪くない。)

なので、お行儀が悪くてもsession_cache_limiter('none')を使うしかないのでは?('none'じゃなくて'hoge'でも'hage'でも「正しい」パラメータ以外なら何でもいいけど。)



新たな選択肢

しかし、こういう手もあるよ。
session_start();
header('Expires:'); //下記「余談」の追記も参照
header('Cache-Control:');
header('Pragma:');

header()でセミコロンの右に何も書かないと、PHPは何も送出しないようだ。
これにより、session_start()のHTTPヘッダー送出を上書きし、結果的に何も送出しない。

session_cache_limiter('none')より冗長だが、明示的という意味ではいいかもしれないと思っている。
(キャッシュを有効にしたい場合にも応用が効く。)



余談

上記のようにブラウザキャッシュの無効化を無効化すると、当然ながらSESSIONの最新情報が反映されていないブラウザキャッシュをブラウザが表示してしまうので注意。

(2011/1/7 追記)
IEはキャッシュがあり、かつ、そのキャッシュがExpiresを何も指定されていないと、アドレスバーにURLを直接入力された場合やリダイレクトした場合などにサーバにアクセスせずにキャッシュの方を使ってしまう。(Firefoxはその場合もサーバにアクセスしてくれる。)
ブラウザの履歴機能を使うためにキャッシュはさせたいが、上記の場合にはサーバにアクセスさせたい場合は、Expiresで-1を指定すると良いようだ。
header('Expires: -1');
ログイン管理をする場合などはこれをやっておいた方が良さそう。
(追記終わり)

また、POSTのパラメータに「チケット」(=ワンタイムトークン)を入れることによる二重POST防止を推奨するのは正しい。というか二重POSTを確実に防ぎたいと思ったらこれしかない。


0 件のコメント:

コメントを投稿