#navi_header|PHP| 特にIE系でよく見られる「ページの期限切れ」画面。これを発生させないためにはどうすればよいのか、現在は少しGoogleで検索するだけで実に様々な対策方法が蓄積されている。だが、そもそも「ページの期限切れ」とはいったい何を示しているのか?いったいこの画面はユーザーに何を訴えているのだろうか? 今回はPHP言語に限定して、この現象を可能な範囲その原因を追及し、抜本的対策と巷間にあふれる対策方法の是非を検討する。最終的に必要となった知識はHTTPのRFC2616のキャッシュ機能およびPHPのext/session/session.cのソースコードとなった。 #outline|| * 「ページの有効期限切れ」が発生するスクリプト・発生しないスクリプト まず現象を再現するところから始める。「ページの有効期限切れ」画面が表示されるのはIE系なので、以下に示す各スクリプトもIEで表示させることを前提とする。Firefoxの場合の動作は後述する。 ** 発生方法 + 「発生するスクリプト」を設置し、IE系のブラウザでアクセスする。 + Submitボタンをクリックし、フォームを送信する。(テキストボックスは単に見栄えや何かの問題で付けただけで、特に使用することはないので適当な値を入力しても、空のまま送信しても問題ない) + 3~4回Submitしたのち、ブラウザバックする。(「戻る」ボタン or BackSpace) + 「ページの有効期限切れ」画面が表示される。 ** 発生するスクリプト - session01.php
- session02.php
** 発生しないスクリプト - session03.php
- (番外編:普通のHTMLでも発生しない。)form01.html
** ブラウザで異なる挙動(IE/FireFox) *** 発生するスクリプトの場合(Firefox) 「ページの有効期限切れ」画面は主にIE系で取り上げられる現象である。一方のNetscape系(Geckoエンジン系)のブラウザではあまりそう言った話題はあがらない。では、実際どうなるのか?Firefox1.04で前掲のsession01/02.phpにアクセスし、IEの時と同様の手順を踏んでみる。 結論として、「再度POSTして良いですか?」という下図に示すような画面が表示され、OKをクリックすると再度サーバーにリクエストが送られている。(この段階ではあくまでも送られている「らしい」までしか目視確認できないが、後述のFirefoxのLiveHTTPHeadersの解析により実際にリクエストが送られていることを確認できた。) &image(30) *** 発生しないスクリプトの場合(IE/Firefox) IE/Firefoxとも、発生しないスクリプトの場合特にサーバーにアクセスも発生せず、表示の早さとフォームに入力された値を覚えてくれている辺り(実際、各Submit毎に入力した値を完全に覚えていた)も同様である。どうやらブラウザのキャッシュにアクセスしているらしい。 ** 推測:session_cache_limiter()が原因か? 発生するスクリプト・しないスクリプトの違いはsession_cache_limiter()の違いである。発生するスクリプトはsession_cache_limiter()を呼んでいないか、'nocache'を渡している。実際にPHPのマニュアルを参照してみる。 http://jp.php.net/manual/ja/function.session-cache-limiter.php これによると、この関数はHTTPヘッダーを操作し、クライアントに対してキャッシュ制御を行う関数らしい。引数はマニュアルを読む限り次の4つ。 : nocache : クライアント(および途中に介在するプロクシ)にキャッシュさせない。 : public : クライアント(および途中に介在するプロクシ)にキャッシュを許す。 : private : クライアントにのみキャッシュを許す。途中に介在するプロクシにはキャッシュを許さない。 : private_no_expire : 機能はprivateと同じだが、Mozilla系を混乱させないためExpiresヘッダを送信しない。 当関数を呼ばない場合、自動的にsession.cache_limiterに指定された値が適用されるらしい。そのデフォルト値は'nocache'であるらしい。従って、前掲のsession01.phpは当関数を呼んでいないため'nocache'が仮定され、結果として動作はsession02.phpと同等のものになっていたことが推測される。 * HTTPヘッダーの比較 session_cache_limiter()はHTTPヘッダーを操作する。従って、前掲のスクリプトの動作を解析するにはそのHTTPヘッダーを観察する必要がある。今回はFirefoxの拡張(extension)の一つであるLiveHTTPHeadersを用いて、前掲の三つのスクリプトにアクセスした場合のHTTPヘッダーを観察してみることにした。 session01 - 03.phpにアクセスする。いずれもアクセスの前にCookieとブラウザキャッシュを全クリアする。操作としては以下の手順で統一した。 + アドレス欄に直接URLを入力し、GETでアクセスする。(GET一回目) + フォームに適当な値を入力し、Submitする。(POST一回目) + フォームに前回とは異なる値を入力し、再度Submitする。(POST二回目) + ブラウザバックする。(IEなら「ページの有効期限切れ」画面が表示されるが、Firefoxの場合は再度POSTされる。) このときのLiveHTTPHeadersのヘッダーを以下のファイルに保存した。 - [[添付ファイル/PHP/session01.log]] - session01.phpのログ - [[添付ファイル/PHP/session02.log]] - session02.phpのログ - [[添付ファイル/PHP/session03.log]] - session03.phpのログ 以下にログファイルを解析した結果を、要点を絞ってまとめる。 - session01.phpのサーバーからの応答ヘッダー サーバーからの応答の内、Expires, Cache-Control, Pragma 辺りが怪しい。 HTTP/1.x 200 OK Date: Sat, 09 Jul 2005 15:03:38 GMT Server: Apache/2.0.50 (Win32) PHP/4.3.8 X-Powered-By: PHP/4.3.8 Set-Cookie: CacheExpireExperiment01=225b4c965b29028b78bec94bfebce4cb; path=/ Expires: Thu, 19 Nov 1981 08:52:00 GMT Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0 Pragma: no-cache Keep-Alive: timeout=15, max=100 Connection: Keep-Alive Transfer-Encoding: chunked Content-Type: text/html; charset=EUC-JP - session02.phpのサーバーからの応答ヘッダー HTTP/1.x 200 OK Date: Sat, 09 Jul 2005 15:04:21 GMT Server: Apache/2.0.50 (Win32) PHP/4.3.8 X-Powered-By: PHP/4.3.8 Set-Cookie: CacheExpireExperiment02=56e20d4184fbe8690b9e1beb00d314e1; path=/ Expires: Thu, 19 Nov 1981 08:52:00 GMT Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0 Pragma: no-cache Keep-Alive: timeout=15, max=99 Connection: Keep-Alive Transfer-Encoding: chunked Content-Type: text/html; charset=EUC-JP - session03.phpのサーバーからの応答ヘッダー HTTP/1.x 200 OK Date: Sat, 09 Jul 2005 15:04:51 GMT Server: Apache/2.0.50 (Win32) PHP/4.3.8 X-Powered-By: PHP/4.3.8 Set-Cookie: CacheExpireExperiment03=ef639f33f6259fea6437c5aa3255a429; path=/ Expires: Thu, 19 Nov 1981 08:52:00 GMT Cache-Control: private, max-age=10800, pre-check=10800 Last-Modified: Sat, 09 Jul 2005 15:00:52 GMT Keep-Alive: timeout=15, max=100 Connection: Keep-Alive Transfer-Encoding: chunked Content-Type: text/html; charset=EUC-JP ** 比較・解析・推測 まず、session01と02において、Expires/Cache-Control/Pragmaヘッダーフィールドが完全に一致している。これは前述のsession_cache_limiter()とsession.cache_limiterのデフォルト設定との関連より推測した、session01と02の動作が同じであることを証明している。 続いてsession03.phpとの比較だが、session03.phpで大きく異なるのが次の部分である。 Expires: Thu, 19 Nov 1981 08:52:00 GMT Cache-Control: private, max-age=10800, pre-check=10800 Last-Modified: Sat, 09 Jul 2005 15:00:52 GMT Cache-Controlが no-cache ではなくなり、privateになっている。また幾つかのパラメータが追加されている。さらにLast-Modifiedフィールドが追加されている。ちなみに、session_cache_limiter()にprivate_no_expireを指定するとExpiresフィールドが無くなる。この点についてはPHPのマニュアルと一致している。 以上より、session_cache_limiter()に渡す値は Cache-Control, Pragma のヘッダーフィールドに影響を与える事が確認できた。ではこの二つ(およびExpires, Last-Modified)ヘッダーフィールドが、ブラウザにどういった影響を与えるのかをHTTPのRFC2616を元に調べてみる。 * HTTPのキャッシュ制御について HTTPについての文献は次のURLに日本語で非常に詳しくまとめられている。以下、このHPを元に今回の調査対象となるヘッダーフィールドおよびブラウザのキャッシュ制御について著者の視点からまとめてみる。 - http://www.studyinghttp.net/ -- http://www.studyinghttp.net/intro : HTTPの概要と歴史 -- http://www.studyinghttp.net/header : HTTP/1.1ヘッダーフィールドの解説 -- http://www.studyinghttp.net/caching : HTTP/1.1, 1.0のキャッシュ機能詳細(詳しすぎて理解不能...) -- http://www.studyinghttp.net/cgi-bin/rfc.cgi?2616 : RFC2616(HTTP/1.1)の日本語訳 '' 注意 '' :日本語訳の使用上の注意点については必ず http://www.studyinghttp.net/translations#Notice を参照して下さい。あくまでも英語文書が正式版であり、日本語訳した場合の誤訳や誤解については無保証だそうです。 ** キャッシュされる場所とキャッシュの許可の考え方 Webページがキャッシュされる場所はクライアントという視点では二カ所ある。 - クライアントPCのWebブラウザ(つまりクライアントPC) - プロクシサーバー プロクシサーバーは一般に共用サーバーである。この事により、「個人のプライバシーに関わるページのキャッシュの是非」を操作できる必要性が生じる。これにより、キャッシュには主に以下の3段階の"レベル"が導入されるべきである。 + クライアントPCおよびプロクシサーバーにキャッシュされても大丈夫な"公開(public)"レベル + クライアントPCにのみキャッシュされ、プロクシサーバーにはキャッシュされるべきではない、"個人(private)"レベル + クライアントPC/プロクシサーバーの両方にキャッシュを許さない、"キャッシュ不可(no-cache)"レベル さらに重要な点として、「キャッシュが無効化されるのをいつ、どのようにしてブラウザは認識するのか」という問題も出てくる。すなわち、 + キャッシュレベル + キャッシュ無効化検出 の二点を制御できて初めてキャッシュを安全に確実に利用できるようになる。 ** HTTP/1.0におけるキャッシュ制御ヘッダーフィールド(Expires, Pragma) 現在主なサーバー・ブラウザ(クライアントプログラム)が対応しているHTTPのバージョンは 1.1/1.0 の二種類。このうち、HTTP/1.0時代のキャッシュ制御ヘッダーフィールドについてまとめる。 : Expires : そのWebページ(リソース)がいつ無効になるのかを示す。形式は RFC 1123( http://www.studyinghttp.net/header#HTTP-Date 参照) にて定義((PHPのsession.cのソース内では gmdate("D, d M Y H:i:s") として生成されている。))されている。 : Pragma : 本来は汎用に使えるOptionalなヘッダーフィールドを目的としていたが、実際にはキャッシュを許可しない((正確には、たとえクライアントプログラムがキャッシュを保持していたとしても、それを無視して再度リクエストを送出することをRequestする。))"no-cache"のみが定義されている。 Expires, Pragma ともにHTTP/1.0時代のフィールドだが、下位互換性のため使用している。HTTP/1.1に対応したプログラムの場合はCache-Controlヘッダーフィールドがこれらより優先して使用される。 ** HTTP/1.1におけるキャッシュ制御ヘッダーフィールド(Cache-Control, Last-Modified) 近年の主要なサーバー・ブラウザが対応しているHTTPバージョン1.1では、Cache-Controlがキャッシュ制御用のヘッダーフィールドとして利用されている。 : Cache-Control : 次のURLを参照のこと。 http://www.studyinghttp.net/cgi-bin/rfc.cgi?2616#Sec14.9 : Last-Modified : 一般的にメッセージの最終更新日を表す。ファイルアクセスであればそのサーバーのファイルシステムにおける最終更新日を返すべきであるし、CGIなどによるなにがしかのメッセージであれば、そのメッセージが生成された(すなわちそのCGIが起動・生成した)日付を返すべきである。 Last-Modifiedヘッダーは、Cache-Controlでno-cache以外が指定されてキャッシュが利用されるシーンにおいて、有効期限の判別材料として使われるらしい。。RFCではキャッシュの有効性チェック関連で大まかな規定はされているようだが、細かい部分は個々のプログラムに依ってしまうようだ。( http://www.studyinghttp.net/caching ) ** ヘッダーフィールドと「ページの有効期限切れ」画面の関連についての第一のまとめ 以上の調査より、以下の結論を導き出せる。 「ページの有効期限切れ」画面が・・・ + 表示される場合とは、すなわちHTTPヘッダーのキャッシュ制御フィールドにおいてキャッシュ不可を指示された場合である。 + 表示されない場合とは、すなわちHTTPヘッダーのキャッシュ制御フィールドにおいてキャッシュ可能である事を指示された場合である。 つまるところ件の画面はIEのバグでも何でもなく、RFCに従った正常な動作であることが導き出せた。Firefoxの場合も、キャッシュがないため再度リクエストを送る動作なのでRFCから逸脱していない。ではいったいなぜ、標準に従っている筈のこの動作について多くのWebプログラマーが頭を悩ませるのか?また、巷間にあふれる処方箋はどこまでが正しいのか? 以下では、この二つの疑問について「そもそも論」と「PHPのソースコード」の二つの側面から追求し、もっとも効果的かつ抜本的な対策案「チケットの導入」を考える。 * そもそもなぜ「ページの有効期限切れ」対策に頭を悩ませるのか? まずこの点について考えなければならない。なぜなら"「ページの有効期限切れ」画面を表示させたくない"という要求自体が無くなれば、そもそもこの対策に頭を悩ませる必要はなくなる。 - なぜ件の画面が表示されると都合が悪いのか? -- 格好と見栄えが悪い。 -- 意味不明の画面。何か異状があったのかと戸惑う。 - では、なぜ件の画面が表示されるのか? -- ユーザーがブラウザバックを実行する。 -- なぜブラウザバックを実行するのか? --- (確認画面などで確認した結果)フォームに入力した値が間違っていたので、''前の画面に戻って''修正したい。 --- フォームに入力したが、続く手続きをキャンセルしたい。 - ではユーザーはブラウザバックをしたとき何が表示されていることを望むのか? -- 直前の入力画面 -- 当然、自分が入力した値が表示されているべき。 ユーザーの立場に立てばいずれも至極まっとうな要求である。しかしWebプログラマはこれらの要求を聞くと眉をひそめる。なぜか? - ブラウザバックされた後に再度POSTされるとまずい。 - なぜまずいのか? -- 登録した後にブラウザバックされてPOSTされると、同じ情報がPOSTされてエラーとなる。→''プログラマーとして気持ち悪い。'' つまり以下の二点が頭を悩ませる最大の要因である。 - 本来は再度POSTされると面倒なので、キャッシュを不可にしてブラウザバックを無効化したい。 - ユーザーとしては直感的に「データの修正」のためにブラウザバックを実行してしまい、またブラウザバックができて当然だと考えている。 これが為に本来であれば意味的に至極まっとうな、IEの「ページの有効期限切れ」画面やFirefoxの「再POSTの確認」ダイアログを、ユーザーを戸惑わせたくないが為にどうにかして無効化する必要が出てくるのであろう。元々入力値や個人情報の安全性を補助するためのキャッシュ無効化機能が、そのクライアントプログラム側のインターフェイス故に、結果としてユーザーを戸惑わせる機能として否定されようとしているのだ。Webプログラマは、結果としてキャッシュ機能を有効化することにより元の入力フォームに"戻らせ"、代わりに再POST防止のため頭をひねることとなる。 * PHPのソースコードの探索 ではいったい、キャッシュ機能を正しく有効にするにはどうすればよいのか?PHPに限定して、その処方箋をざっとGoogleで検索してみた結果を以下に列挙する。 - http://www.workspot.jp/tech/php_tips.html - http://www.stackasterisk.jp/tech/php/searchAction.do?cid=15#58 - http://ns1.php.gr.jp/pipermail/php-users/2001-October/003062.html - http://ns1.php.gr.jp/pipermail/php-users/2004-October/024018.html - http://ns1.php.gr.jp/pipermail/php-users/2004-October/024022.html - http://ns1.php.gr.jp/pipermail/php-users/2004-October/024024.html - http://ns1.php.gr.jp/pipermail/php-users/2005-January/024581.html どうやらsession_cache_limiter()に結局は落ち着くようである。関連するPHPマニュアルを以下に列挙する。 - http://jp.php.net/manual/ja/ref.session.php : php.ini設定関連 - http://jp.php.net/manual/ja/function.session-cache-expire.php : session_cache_expire() - http://jp.php.net/manual/ja/function.session-cache-limiter.php : session_cache_limiter() PHPソースコードの探索に入る。ソースコードのバージョンは4.3.11, ターゲットは ext/session/session.c のみである。 ** PHP_FUNCTION(session_cache_limiter), PHP_FUNCTION(session_cache_expire) まずPHP関数を定義している"PHP_FUNCTION"マクロを使用した関数の中からsession_cache_limiter, session_cache_expireを探る。1331行目以降に出てくる。以下、適当に省略して要点のみを抜き出す。 // 現在のcache_limiterを返す。引数が渡されれば、引数でcache_limiterを更新する。 PHP_FUNCTION(session_cache_limiter) { zval **p_cache_limiter; char *old; zend_get_parameters_ex(ac, &p_cache_limiter); old = estrdup(PS(cache_limiter)); convert_to_string_ex(p_cache_limiter); zend_alter_ini_entry("session.cache_limiter", sizeof("session.cache_limiter"), Z_STRVAL_PP(p_cache_limiter), ...); RETVAL_STRING(old, 0); } // 現在のcache_expireを返す。引数が渡されれば、引数でcache_expireを更新する。 PHP_FUNCTION(session_cache_expire) { zval **p_cache_expire; long old; old = PS(cache_expire); zend_get_parameters_ex(ac, &p_cache_expire); convert_to_long_ex(p_cache_expire); PS(cache_expire) = Z_LVAL_PP(p_cache_expire); RETVAL_LONG(old); } どうやらPS(cache_limiter), PS(cache_expire), session.cache_limiter辺りが怪しいようです。ただ、なぜcache_limiterではiniエントリを更新していてexpireの方ではそれをしないかは不明です。 ** PS(cache_limiter), PS(cache_expire) 147行目でこの二つがiniファイルより初期化されています。 STD_PHP_INI_ENTRY("session.cache_limiter", "nocache", PHP_INI_ALL, OnUpdateString, cache_limiter, ...) STD_PHP_INI_ENTRY("session.cache_expire", "180", PHP_INI_ALL, OnUpdateInt, cache_expire, ...) このマクロの詳細は不明ですが、デフォルト値とおぼしき値はマニュアルと一致しています。 PS(cache_limiter)は前述のPHP_FUNCTION(session_cache_limiter)および822行から始まる static int php_session_cache_limiter(TSRMLS_D) 内でしか使われていません。 PS(cache_expire)は770行目 CACHE_LIMITER_FUNC(public) および789行目 CACHE_LIMITER_FUNC(private_no_expire) の二カ所で使われています。 ざっとみてだいぶ本命に近づいてきたようです。cache_expireは秒数として使われているようです。cache_limiterを探ると本命に近づけそうです。 ** php_session_cache_limiter() PS(cache_limiter)が使われているphp_session_cache_limiter()について探索してみます。 // 238行目:session_cache_limiter()の各引数と対応する関数のマッピング用構造体の定義 typedef struct { char *name; void (*func)(TSRMLS_D); } php_session_cache_limiter_t; // 814行目:実際の構造体の実体 static php_session_cache_limiter_t php_session_cache_limiters[] = { CACHE_LIMITER_ENTRY(public) CACHE_LIMITER_ENTRY(private) CACHE_LIMITER_ENTRY(private_no_expire) CACHE_LIMITER_ENTRY(nocache) {0} }; // 822行目:php_session_cache_limiter()の要点のみ抜粋 static int php_session_cache_limiter(TSRMLS_D) { php_session_cache_limiter_t *lim; // 構造体の中から、現在のcache_limiterに一致するのを取り出し、対応付けされている関数を実行する。 for (lim = php_session_cache_limiters; lim->name; lim++) { if (!strcasecmp(lim->name, PS(cache_limiter))) { lim->func(TSRMLS_C); return 0; } } return -1; } php_session_cache_limiter()自体はsession_start()内で php_session_reset_id(TSRMLS_C) の後に呼ばれています(1097行目)。 確信まであと少しです。構造体に使われているマクロで注目してみると、構造体の実体のすぐ上にCACHE_LIMITER_FUNCと言う一連の関数が定義されていました。ヘッダーフィールドらしき文字列も見えます。 ** CACHE_LIMITER_FUNC(public, private_no_expire, private, nocache) この一連の関数こそが、Expires/Pragma/Cache-Control/Last-Modifierをセッション使用時に制御している中枢です。実体は751行目のlast_modifiedから始まり813行目まで続きますが、要はどの関数がどのヘッダーフィールドを送信しているのか分かればよいので、そこだけまとめました。 - CACHE_LIMITER_FUNC(public) -- Expires: (PS(cache_expire)などを吟味して送出) -- Cache-Control: public, max-age=PS(cache_expire)*60 -- Last-Modified: (last_modified関数) - CACHE_LIMITER_FUNC(private_no_expire) -- Cache-Control: private, max-age=PS(cache_expire)*60, pre-check=PS(cache_expire)*60 -- Last-Modified: (last_modified関数) - CACHE_LIMITER_FUNC(private) -- Expires: Thu, 19 Nov 1981 08:52:00 GMT (固定) -- Cache-Control: (private_no_cache呼び出し) -- Last-Modified: (private_no_cache呼び出し) - CACHE_LIMITER_FUNC(nocache) (全て固定) -- Expires: Thu, 19 Nov 1981 08:52:00 GMT -- Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0 -- Pragma: no-cache '' ようやく追いつめました。 '' これらが、session_cache_limiter()により呼び出されるキャッシュ制御ヘッダーフィールドを送出している部分です。これらはphp_session_cache_limiter()内で、現在のcache_limiterから呼び出されます。 ** 幾つかの間違いとそれでも動作する理由 Webを調べていると、session_cache_limiter()について次の例がそれぞれ別の場所で見つかった。 session_cache_limiter('none'); session_cache_limiter('no-cache'); session_cache_limiter('private, must-revalidate'); これまでの調査によると、これらの引数は明らかに無効である。しかし掲載場所ではいずれも「これで思った通りに件の画面が表示されなくなった。」と報告されていた。 この理由は簡単で、php_session_cache_limiter()では引数(つまり現在のcache_limiter)にヒットしなかった場合はそのままスルーしている。スルーするとどうなるか。単にキャッシュ制御ヘッダーフィールドが送出されなくなるだけである。そうなると、只のHTMLと同様に普通にブラウザにキャッシュされ、結果、「ページの有効期限切れ」画面はブラウザバックでは表示されなくなる。 これが今まで明らかにされていなかった理由としては、session_cache_limiter()に指定した値が有効かどうか判別するためのインターフェイスが無いことが考えられる。それに加え、関数名からCache-Controlヘッダーに指定するものを渡せばよいと誤解を招いた可能性もある。前掲の2・3番目の間違いがそれにあたると思われる。 * まとめ - チケットの導入へ 以上より、今回の件に関する動作はほぼ押さえられた。結論としては「ページの有効期限切れ」画面を表示させない対策としてはsession_cache_limiter('private_no_expire')をコールすることが最大公約数として導き出せる。(('public'だとプロクシに残るCache-Controlになるので個人情報保護の観点からN.G.となる。'private'ではMozilla系が混乱するという情報があり、残った'private_no_expire'が公約数として使用できる。無効な文字列を指定してそもそもヘッダーを送出させないのは、全てを分かった上での反則技としてなら有効。)) しかし、再POSTの危険性に対してはどうすればよいのか?多くが試行錯誤中ではあり、たとえばDBにレコードを挿入・更新する場面ではUNIQUE違反を検出する場合がある。しかし、この場合UNIQUEキーが無く単純にシーケンスと関連づけされたPrimary Keyだけが有効となる場面ではUNIQUEを検出できない。 一つの手法としては、都度ユニークなキー値を生成し、hiddenとしてフォームに埋め込んでおく「チケット」の導入が考えられる。アプリケーションは裏側でチケットを管理し、アクションが発生すればチケットに「使用済み」マークを付ける。もしも使用済みマークのついたチケットがPOSTされてきたら、エラーで弾く。Javaでの開発では導入される場合があるようだが、これが比較的安全であろう。 あるいは、そもそも個人情報をキャッシュさせないためのキャッシュ制御であるのだから、ユーザーに「そういうもの」として受け止めて頂くよう説得し、「ページの有効期限切れ」画面が表示されることに対して理解を求める、という手もある。 #navi_footer|PHP|