続・HTTPキャッシュを使いこなして、Webアプリを快適に(1)

2023年05月25日 木曜日


【この記事を書いた人】
ヒラマツ

セキュリティ本部 セキュリティ情報統括室に所属 システム開発者。2000年問題で「2038年問題は定年で対応しなくていい!」とフラグを...。

「続・HTTPキャッシュを使いこなして、Webアプリを快適に(1)」のイメージ

HTTPキャッシュを使いこなして、Webアプリを快適に」の続編です。
まだ読んでない方は、そちらを先に読んでください。

cats_dogs開発者のヒラマツです。

なんとか、HTTPキャッシュ制御の基礎が説明できたので、やっと、肝心のCache-Control ヘッダーの話が書けます。
ちなみに、cats_dogsも、このCache-Control ヘッダーで高速化できるように作っています。
cats_dogs利用者も、この文章を読んで、ぜひ、cats_dogsを高速化してください。

前編同様に、絵を多めに、出来るだけかみ砕いて書いてます。
その結果、やはり同じぐらいの長さになってますが、頑張って読んでください。

Cache-Controlヘッダー

以下はCache-Controlヘッダーにno-cacheを指定した例ですが、no-cacheの部分に指定する値をディレクィブと言います。

Cache-Control: no-cache

ディレクティブは、,(カンマ)で区切って、複数指定が可能です。
例えば、max-age=3600must-revalidateの2つのディレクティブを指定するときは、以下のように書きます。(ディレクティブの個々の意味は、後ほど説明するので、まだ解らなくて大丈夫です。)

Cache-Control: max-age=3600, must-revalidate

ただし、複数指定する場合は、矛盾しないように指定する必要があります。(矛盾する組み合わせの動作は未定義なので)

そして、互換性のため、ブラウザやプロキシが未対応のディレクティブは、無視する決まりがあります。この動作のおかげで、古いブラウザは新しいディレクティブを無視できるので、ブラウザがおかしくなることは防げます。
RFCやMDNにも、この説明の例として、互換性のため、類似効果のディレクティブを並記する例が書かれていたりします。

ですが、この方法で、古いシステムとの互換性を考え出すとどんどん複雑になります。
現実的に考えて、セキュリティアップデートもされない過去のブラウザを救う意味は、あまりありません。

と言うわけで、この説明では、過去の互換性のための表記や実装状況が怪しい新しすぎる表記などの、実用性が低い部分は積極的に省略します。
正しい定義や全てのディレクティブを知りたい方は、MDNのCache-Controlの説明や、RFC 9111を参照してください。
この説明では、定義より、現実や実用性重視でCache-Controlヘッダーを説明して行きます。 ご了承ください。

2種類のCache-Control

ブラウザの要求時と応答時の2つで用途で、Cache-Controlヘッダーは異なる意味に扱われます。

文字列としては同じCache-Controlヘッダーですが、実質、別物なので注意が必要です。

要求時のCache-Control

ブラウザ側が要求時のCache-Controlヘッダーを使うと、Webサーバ側へキャッシュの扱いについて要求できます。

例えば、スーパーリロード(super reload)と呼ばれる、最新データの強制取得の動作などで使われます。(多くのブラウザで、Shiftキー+リロード操作に機能が割り当てられてます。)

要求時のCache-Controlヘッダーの主要なディレクティブは以下の通りです。

ディレクティブ名 概略
no-cache 途中経路の古いキャッシュを無視して、最新のデータを要求する指定です。
例: Cache-Control: no-cache
max-age 利用できるキャッシュの新しさを制限して、新しいデータを要求する指定です。指定する時間の単位は秒です。
例: Cache-Control: max-age=3600 (3600秒を指定する例)

要求時のCache-Controlヘッダーは、それほど難しくないですね。

応答時のCache-Control

Webサーバ側から応答時のCache-Controlヘッダーを送信すると、ブラウザ側へキャッシュの扱いについて要求できます。

この応答時のCache-Controlヘッダーは、キャッシュ制御をするための主な手段でもあります。

応答時のCache-Controlヘッダーのディレクティブは、要求時のCache-Controlヘッダーと違って、けっこう結構複雑です。数も多く、その上、ディレクティブの意味が状況によって変わります。
複雑すぎて、ディレクティブのリストを眺めるだけでは、理解できる気がしません。

そこで、「304 Not Modified 」に対応しているブラウザについて、キャッシュの再利用に関わる動作だけに注目して、ブラウザやプロキシの挙動を整理してみました。 以下の図が整理した結果をフロートチャートにしたものです。(分かりやすさ重視で、関係ない部分は、曖昧にして誤魔化してます。)

このフロートチャートで、キャッシュが再利用される時に使われる流れを、青い矢印にしてあります。この青い矢印の流れをよく見ると、A、B、Cのマークをつけた「判断」で、キャッシュの再利用が決まっていることがわかります。
なお、これら3つの判断は通信前および通信失敗時の処理ですから、サーバからは情報を得られないので、キャッシュ内に残っている前回のCache-Controlヘッダーなどの古い情報を元に判断しています。

これらの3つの判断の概要を、説明したのが以下の表です。

判断のマーク 判断タイミング 概要
A判断(検証は必要か?) サーバへの接続前 キャッシュ更新の検証を行なうかどうかの判断をします。
B判断(キャッシュは新鮮か?) サーバへの接続前 キャッシュ期限(max-ages-maxageで指定)より、キャッシュが新しいかどうかの判断をします。
C判断(接続失敗時、キャッシュを使用するか?) サーバへの接続失敗後 通信エラーが発生したとき、キャッシュのデータを使って動作を続けるかの判断をします。

後は、各判断の処理内容がわかれば、理解できそうです。
と言うことで、これらの判断ごとに、各種のディレクティブがどのように働くかを説明して行きます。

A判断「 検証は必要か?」

A判断「 検証は必要か?」は、ブラウザがキャッシュ期間を無視して、キャッシュの検証を行なうか決める判断です。

キャッシュの検証が実施され、成功すれば、適切にキャッシュが更新されて、最新のデータが表示されます。 最新データが表示されるので、キャッシュを使っていても、表示上は「キャッシュしてない」状況と同じになります。
(応答時のCache-Controlヘッダーにno-cacheが設定されても、現実には、ブラウザはキャッシュするしかありません。そこで、RFC的には、この検証実施が強制された状態をno-cacheということにして妥協したようです。これが混乱の元凶です。)

一般的なまともな利用に限定すれば、A判断に、実質関係があるディレクティブは、no-cacheno-storeprivateです。(一般的ではない状況については、後ほど説明します。)

一般的な利用方法に限定すると、以下のとおりです。

条件 対象 検証の有無 補足
no-cache有り ブラウザおよびプロキシ 検証する
no-store有り ブラウザおよびプロキシ 検証する C判断にも影響を与える
private有り プロキシ 検証する ブラウザはprivateを無視する
その他(デフォルト) ブラウザおよびプロキシ 検証しない 上記の3条件にマッチしないとき

それ以外の、一般的ではない動きについて説明します。 以下の条件を全てを満たすと、A判断でprivateディレクティブ相当の動きをします。

  1. プロキシである。
  2. HTTPSを使ってない(暗号化せずにプロキシを使っている)。
  3. Authorizationヘッダーがある。
  4. publics-maxagemust-revalidateディレクティブが使われていない。

暗号化せずに認証情報を処理している状態ですので、意図しない限り、そうそう無い用途かと思います。
もし、この条件で使うときは、あまり安全ではないので注意してください。

B判断「キャッシュは新鮮か?」

B判断「キャッシュは新鮮か?」の判断は、 Webページのキャッシュがキャッシュ期間を超えて、古くなってないかをサーバへの接続前に判断する処理です。 検証なしでキャッシュを活用するか判断する処理でもあります。
A判断が、常に検証が行われる条件(no-cacheもしくはno-store)の時は、B判断に関するディレクティブの指定は無視されるので、省略できます。

この判断に影響を与えるディレクティブは、max-ages-maxageで、どちらもキャッシュ期間を設定する為のディレクティブです。

ディレクティブの条件と、適用するシステムの組み合わせは、以下のようになります。

条件 キャッシュ期間が適用されるシステム
max-ageのみ指定 ブラウザもプロキシもどちらも適応される。
s-maxageのみ指定 プロキシのみに適応される。
max-ages-maxageの両方を指定 ブラウザにはmax-agaの値が、プロキシにはs-maxageの値が適応される。
ブラウザとプロキシに異なるキャッシュ期間を設定したいときに使う。

補足すると、Expiresヘッダーでもmax-age相当の指定することが出来ます。ただ、max-ageおよびs-maxageを指定すると、Expiresヘッダーは無視されます。(Expiresヘッダーは、古いシステムとの互換性の為にあるので、使う意味はあまりないので説明は省きます。)

max-ageおよびs-maxageで、指定するキャッシュ期間は、ディレクティブ名に=と秒数の数字を繋げて記述します。
例えば、3600秒をする場合は、max-ageおよびs-maxageで以下のような内容になります。

  • Cache-Control: maxage=3600
  • Cache-Control: s-maxage=3600

C判断「 接続失敗時、キャッシュを使用するか?」

C判断「接続失敗時、キャッシュを使用するか?」の判断に影響を与えるディレクティブは、no-storemust-revalidateproxy-revalidateです。

通信が失敗すると、サーバからは何も情報が得られないので、エラー表示するよりは古いコンテンツでも表示した方が良いという(ブラウザ側の一方的な)判断もできます。
そこで、通信が失敗した場合は、キャッシュ期間などの条件を無視して、(たとえ古くても)存在するキャッシュを強制的に再利用し、Webページの動作を続ける仕組みがあります。(例えば、オフライン動作するWebアプリなどでは有効な動作です。)

ネットワークなどの障害の影響を軽減する仕組みですが、この動作が問題になる場合があります。
ですので、C判断の動きで、この仕組みの有効無効を切り替えられるようになっています。

一見、都合がよさげな仕組みのどこが問題なのかというと、 例えば、認証失敗も通信失敗として扱われ、過去のキャッシュを表示してしまいます。 この動作は、認証が必要なデータを「認証無しで」表示してしまうので、内容次第では、情報漏えいになってしまいます。
このように、扱うデータ次第では問題になる動作なので、C判断で動作を止められるのです。
ですので、C判断の扱いは、扱うデータを考慮して慎重に決める必要があります。

関連するディレクティブが指定されると、対象のシステムでは、C判断でキャッシュを利用しない判断がされます。要は、対象のシステムでは、通信失敗時におけるキャッシュの再利用が禁止されます。
また、その動作の対象になるシステムはディレクティブによって変わります。
指定されたディレクティブと対象のシステムの関係を整理すると、以下のようになります。

ディレクティブ名 対象のシステム 補足
no-store ブラウザとプロキシの両方 no-cachemust-revalidateの並記と同等
must-revalidate ブラウザとプロキシの両方 通信が成功したときの動作に影響はない
proxy-revalidate プロキシのみを対象 通信が成功したときの動作に影響はない

例えば、通信コストを犠牲にしてでも、サーバに無断でキャッシュが利用されることを防ぎたい用途では、no-storeまたはmust-revalidateを指定します。
逆に、オフライン状態でも動作させたいWebアプリでは、no-storemust-revalidateを指定しない方が適切です。

外部ストレージへの保存について

応答時のCache-Controlヘッダーの値よる判断で、ブラウザがサーバの応答から作成したキャッシュのデータを外部ストレージ(ファイルシステム)に、保存するかどうかの判断についての話です。

A、B、Cの判断の説明では、分かりにくくなるだけなので、キャッシュの外部ストレージへの保存についての説明は、意図的に後回しにしていました。
ディレクティブの指定時に、キャッシュを外部ストレージへ保存するかどうかのについては、一応、RFC的には決まっています。 RFC的には、以下の条件時に、キャッシュを保存してはならないことになっています。(RFCでは「〜MUST NOT store〜」的な表現がつかわれています。)

  • no-storeが指定された時(ブラウザとプロキシの両方で)
  • privateが指定された時のプロキシ

RFC的には、これらのディレクティブが指定されたときの動作は、以下のようなイメージでしょうか。

でも、実際にHTTPを使うと、以下のイメージのように、データはバケツリレー的に複数のシステムを経由して、ブラウザや利用者へ届けられます。

バケツリレー的な処理なので、データが空ではリレーできません。ですので、データは各システムのメモリに保存されます。メモリに入らない大きなデータは外部ストレージに保存されます。
ということは、ディレクティブに関係なく、現実的には、キャッシュの保存を禁止できません。

もちろん、システムによっては、ディレクティブを変えれば、キャッシュの保存動作も変わる可能性はあります。
例えば、ブラウザによっては、no-storeno-cachemax-age=0の3つで、動作が違うかもしれません。
ただ、上記の通り定義が怪しいので、その動作はシステムのバージョンアップ等で変わる可能性が高く、長期的に安全に使うのは難しいのです。

というわけで、Cache-Controlヘッダーに何を設定しても、外部ストレージに保存される(ことがある)と思っておくのが現実的かと思います。(無責任な「MUST NOT store」に振り回されても、幸せにはなりそうにありませんから…)

つづく

Cache-Controlヘッダーの個々のディレクティブは、説明できました。
ただ、ディレクティブの組み合わせ方など、具体的な使い方をあまり説明していません。

と言うわけで、Cache-Controlヘッダーの具体的な使い方についての話が続きます。
次回「続・HTTPキャッシュを使いこなして、Webアプリを快適に(2)」もぜひ読んでください。

ヒラマツ

2023年05月25日 木曜日

セキュリティ本部 セキュリティ情報統括室に所属 システム開発者。2000年問題で「2038年問題は定年で対応しなくていい!」とフラグを...。

Related
関連記事