Webを支える技術を読んでる まとめ5

この記事について

技術研修で読み進めているWebを支える技術について、各章のまとめを順次上げていく。

HTTPヘッダとは

おさらい

Webを支える技術を研修で読んでる まとめ2 - pokotyamu的書きなぐり日記 メッセージヘッダーの部分が今回説明するHTTPヘッダである。

概要

ヘッダには、メッセージボディに対する付加情報(メタデータ)を入れる。 例えば、メディアタイプや言語タグなどである。 また、リソースへのアクセス権を設定する認証や、クライアントとサーバの通信回数と量を減らすキャッシュなどのHTTPの機能はヘッダで実現する。

つまりこのような機能は第7~8章で述べたHTTPメソッドステータスコードと、本章のHTTPヘッダの組み合わせによって表現される。

本章では、 HTTP1.1で主に使用される頻度の高いものだけに絞って説明する。

歴史的背景

  • HTTP0.9の頃には、HTTPヘッダは存在しなかった
  • HTTPの仕様がバージョンアップするに連れ、本文のメタデータを表現するために電子メールのメッセージ仕様を使う方式が採用される
    • そのため、現在のHTTPヘッダは電子メールのヘッダと同じものもある
    • メールのメッセージ仕様は番兵構造である。^1
  • メールは一方通行のやり取りだったが、HTTPは双方向にやり取りをするため拡張する必要がある

日時

DateやExpiresで表現する。 メッセージ作成の時間や、PUTやDELETEの時間指定などに使用される。 
以下は主要な日時を使用するヘッダである。

利用するメッセージ ヘッダ 意味
リクエストとレスポンス Date メッセージを作成した日時
リクエスト If-Modified-Since 条件付きGETでリソースの更新日時の指定
リクエスト If-Unmodified-Since 条件付きPUTやDELETEでリソースの更新日時の指定
レスポンス Expries レスポンスをキャッシュ出来る期限
レスポンス Last-Modified リソースを最後に更新した日時
レスポンス Retry-After 再度リクエストを送信出来るようになる日時の目安
Date: Tue, 06 Jun 2010 03:34:00 GMT

HTTPメソッドで使用する日時は、必ず GMT(グリニッジ標準時)で返さなければならない。

MIMEメディアタイプ

MIME(Multipurpose Internet Mail Extensions)の略。 名前の通り、電子メールのヘッダから採用されたものである。 今後はメディアタイプで示す。

Cotent-Type

メディアタイプの指定を行う。

Content-Type: text/plain

このメッセージボディの中身がどのような形式のものなのかを指定する。

メディアタイプには、以下の表の9つのタイプが存在している。

タイプ 意味
text 人の読めるデータ text/plain
image 画像データ image/jpeg
audio 音声データ audio/mpeg
video 動画データ video/mp4
application その他のデータ aplication/pdf
multipart 複数データからなる複合データ multipart/related
message 電子メール message/rfc822
model 複数次元で構成するモデルデータ model/vrml
example 例示用データ example/foo-bar

text/planeの/の左側を「タイプ」、右側を「サブタイプ」と呼ぶ。 タイプは勝手に増やすことは出来ないが、サブタイプは比較的自由に作ることが出来る。 サブタイプ一覧については数が膨大なので、IANAのページを参照する。 IANAは、「Internet Assigned Numbers Authority」の略。 ドメイン名、IPアドレスプロトコル番号など、インターネット資源のグローバルな管理を行っている南カリフォルニア大学情報科学研究所のプロジェクトチーム。サブタイプの管理もここが行っている。

charset (日本語メッセージの注意)

文字エンコーディングを指定する。

Content-Type: application/xml; charset=utf-8

charsetは省略可能だが、タイプがtextの場合は注意が必要である。 HTTPでのtextタイプのデフォルト文字エンコーディングは、ISO 8859-1と定義されている。 もし、日本語文字列が入っているメッセージに対して、ISO 8859-1と解釈しようとするため、文字化けを引き起こす可能性がある。 さらに、メッセージボディ内で、文字エンコーディングを指定している場合でも、Conttent-Typeヘッダのcharsetが宣言されているならば、こちらが優先される。

文字化けを起こさないように気をつけるためには、必ずcharsetパラメータを付ける事が望ましい。

言語タグ

charsetパラメータは文字エンコーディング方式を指定するが、リソース表現の自然言語を指定する場合は、 Content-Languageを使用する。

Content-Language: ja-JP

言語タグの「-」の左側には、ISO 639が指定する言語コードが入る。 日本語ならば「jp」英語ならば「en」など
右側にはISO 3166が定義する地域コードが入る。 日本ならば「JP」アメリカならば「US」など

言語コードと地域コードの組み合わせにより、1つの国に複数言語ある場合や多数の国で使われる言語にも対応することができる。

コンテントネゴシエーション

メディアタイプや文字エンコーディングや言語タグはサーバが一方的に決定するだけでなく、クライアントと交渉して決めることも出来る。

クライアントが処理出来るメディアタイプを伝える時は、Accept クライアントが処理できる文字エンコーディングを伝える時は、Accept-Charset クライアントが処理できる言語を伝える時は、Accept-Language

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Charset: Shift_JIS,utf-8;q=0.7,*;q=0.7
Accept-Language: ja,en-us;q=0.7,en=0.3

q=のパラメータは優先度が指定でき、1に近いほど優先される。 省略されている場合は1が入る。

Content-Length

Content-Lengthヘッダを利用して、メッセージボディのサイズを10進数のバイトで示すことが出来る。 静的ファイルなど、あらかじめサイズが分かっているリソースを転送する場合などは、これを利用する。
 しかし、動的な画像ファイルなど、すぐにサイズが確定しない場合、Transfer-Encodingヘッダを利用して、ボディを複数の塊(チャンク)にして、転送することが出来る。 これにより、最終的なサイズが分からないデータに対しても、ボディを少しずつ転送することが出来る。

次にチャンク転送の例を示す。

POST /test HTTP/1.1
Host: example.jp
Transfer-Encoding: chunked
Content-Type: text/plain; charset=utf-8

10
The brown fox ju

10
mps quickly over

e
 the lazy dog.
 
0
<空行>

各チャンクの先頭にはチャンクサイズが16進数で入る。 上記の例において、16バイトで表現する16進数の「10」と14バイトを表現する「e」が登場する。 最後には必ず長さ0の空行を付ける。

なお、HTTP1.1の仕様では、全てのHTTP1.1実装は、チャンクエンコーディングを受信しなければならない。

認証

概要

現在主流のHTTP認証方式には、HTTP1.1が規定しているBasic認証とDigest認証がある。 またWeb APIでWSSEというHTTP認証の拡張仕様を利用する場合もある。

今回は、主流の2つを簡潔に説明する。

そもそも認証について、あるリソースに制御がかかってる場合、401 UnauthorizedとWWW-Authenticateヘッダを利用して、クライアントにリソースへのアクセスに必要な認証情報を通知出来る。

DELETE /test HTTP/1.1
Host: example.jp
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Basic realm="Example.jp"

これは、Basic認証も、Digest認証もどちらも使用する。

Basic認証

最も簡単かつ基本的な方式。 ユーザ名とパスワードを使って認証を行う。

ブラウザから送信される認証情報は、「ユーザ名:パスワード」という形式で「:」区切りで並べ、これをBASE64エンコーディングした文字列をHTTPヘッダのAuthorizationフィールドに記載する。

古くから使用される方式のため、ほとんどのWebサーバやWebクライアントが対応しているが、認証情報が暗号化やハッシュ化されないため、通信途上で第三者に覗き見られる危険性がある。

そもそも、

$ echo Z3Vlc3Q6VGFrZTU= | base64 -D
guest:Take5

このようにbase64をデコードすることによって、簡単に元の文字列を判定することが出来る。

よって、Basic認証を使う時は、それが許される程度のセキュリティ強度でいいのか?SSL(Secure Socket Layer)やTLS(Transport Layer Security)を使ってHTTPS(HTTP over Secure Socket Layer)通信し、通信路上で暗号化するか?を検討する必要がある。

Digest認証

Digest認証は、Basic認証よりもセキュアな認証方式である。
あるメッセージに対して、ハッシュ関数を適用した結果のハッシュ値を使って認証を行う。 ハッシュ関数とは、データからそのデータを代表する数値を求める関数のことで、ハッシュ関数を使って得られた値のことをハッシュ値と呼ぶ。

Basic認証では、

DELETE /test HTTP/1.1
Host: example.jp
Authorizaition: Basic dXNlcjpwYXNzd29yZA==

このようなリクエストで送信していたが、Digest認証では、

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Digest realm="Example.jp", nonce="1ac421d9e0a4k7q982z966p903372922", qop="auth", opaque="92eb5ffee6ae2fec3ad71c777531578f"

このようにより複雑なレスポンスが返ってくる。

このWWW-Authenticateの値のことをチャレンジと言う。 クライアントはまず認証なしで、リクエストを送信した上で、チャレンジを使って次回のリクエストを組み直す。

nonceはnumber used onceの略で、リクエストごとに変化する文字列である。 nonceの値は、サーバの実装に依存しているが、基本的には、タイムスタンプやサーバだけが知り得るパスワードから生成する。

qopはquality of protectionの略で、authauth-initを指定して使用する。 qopの値はクライアントが送信するダイジェストの作成方法に影響を与える。 authの場合、メソッドURIからダイジェストを作成する。 auth-initの場合、メソッドURIに加えてメッセージボディーも利用する。 これにより、POSTやPUTでボディを送信するときは、auth-initを使うとメッセージ全体が改ざんされないことを保証することが出来る。

opaqueは、クライアントには不透明(推測できない)文字列で、同じURI空間へのリクエストえは共通してクライアントからサーバに送る。

これらを以下のような流れで実行する。

  1. ユーザ名、realm、パスワードを「:」で連結し、MDS(Message Digest Algorithm)ハッシュ値を求める
  2. メソッドURIのパスを「:」で連結し、MDSハッシュ値を求める
  3. 1の値、サーバから得たnonce、クライアントがnonceを送った回数(cnonce)、qopの値2の値を「:」で連結し、MDSハッシュ値を求める

サーバ上にパスワードのハッシュ値を保管し、そのハッシュ値を元に認証を行うため、パスワードそのものをサーバに預けなくて良くなる点から、セキュリティリスクを下げることが出来る。

Basic認証の場合は、同じURI空間のリソースであれば、クライアントは一度認証してしまえば、2回目以降は自動的にユーザ名とパスワードを送れた。しかし、Digest認証では、サーバからのnonceがなければクライアントが側でダイジェスト計算されないため、一度401 Unauthorizedレスポンスを返さなければならない。

また、パスワードを暗号化するだけなので、メッセージを暗号化したい場合は、Basic認証同様HTTPSを利用する方法が考えられる。

さらに、ApacheなどのWebサーバでは、Digest認証がオプション扱いでサポートしておらず、自前で実装しなければならないといった状況が考えられる。

キャシュ

概要

キャッシュとは、サーバから取得したリソースをローカルストレージ(HDDなど)に蓄積し、再利用する手法のことである。 ローカルストレージにキャッシュしたデータそのものをキャッシュと呼ぶも事もある。 クライアントが蓄積したキャッシュは、キャッシュが有効な間、再度そのリソースにアクセスしようとした時に、再利用する。

キャッシュ用ヘッダ

クライアントが、サーバから取得したリソースがキャッシュ可能かどうか? そのリソースのキャッシュに関するメタデータが入るヘッダについて説明する。

Pragma

Pragmaヘッダに指定できる値は、公式にはno-cacheのみ。 この値はリソースをキャッシュしてはならないことを示す。 クライアントは、次回リソースにアクセスする時、必ず再度サーバにアクセスしなければならない。

Expries

Exouresヘッダはキャッシュの有効期限を示す。 指定できる値には、日時をGMTのみである。 リソースを変更することがない場合、永久にキャッシュ可能であることを示したくないが、そのような場合でも、Expriesには、最長で、約1年後の値を入れることが推奨されている。

Cache-Control

HTTP1.1から導入されたヘッダ。 PragmaもExpiresもどちらも、Cache-Controlで代用出来る。

Pragma: no-cache
Cache-Control: no-cache

これら2つは同様の意味として使える。

また、有効期限の書き方として、相対時間(あと○○秒有効) といった設定が出来るようになった。

Cache-Control: max-age: 86400

この他にも、様々な細かい設定が行うことが出来る。 詳細については、RFC 2616を参照。

使い分け

以上の3種類の使い分けについては、次の3つの方針がある。

  • キャッシュさせない場合は、PragmaとCache-Controlのno-cacheを同時に指定する
  • キャッシュの有効期限が明確に決まっている場合は、Expiresを指定する
  • キャッシュの有効期限を相対的に指定したい場合は、Cache-Controlのmax-ageで相対時間を指定する

条件付きGET

クライアントがExpiresやCache-Controlヘッダを検証した結果、ローカルキャッシュをそのまま再利用出来ない時でも、条件付きGETを行えば再利用出来る可能性がある。 仕組みとしては、変更されているかを調べる項目をリクエストヘッダに送って、そのレスポンスからそのまま使えるかどうかを検証する。

If-Modified-Since

まず、リクエストの例は次のようになる。

GET /test HTTP1.1
Host: example.jp
If-Modified-Since: Thu, 11 May 2010 03:34:00 GMT

これはローカルキャッシュの更新日時が2010年5月11日3時34分ちょうどであることを示している。 これに対して、サーバはこれ以降変更がない場合、以下のレスポンスを返す。

HTTP/1.1 304 Not Modified
Content-Type: application/xhtml+xml; charset=utf-8
Last-Modified: Thu, 11 May 2010 03:34:00 GMT

304 Not Modifiedは条件付きGETへのレスポンスで、リソースが変更されていないことを示す。 これにより、レスポンスにボディを含まないため、ネットワーク帯域を節約出来るメリットがある。

If-None-Match

これも、リクエストの例をベースに説明する。

GET /test2 HTTP1.1
Host: example.jp
If-None-Match: ab3322028
HTTP/1.1 304 Not Modified
Content-Type: application/xhtml+xml; charset=utf-8
ETag: ab3322028

If-Modified-Sinceヘッダによく似ているが、一番の違いはETagというヘッダの値を更新の判定に使用している点である。 ETagはリソースが更新されるたびに新しい値に変わる文字列であり、特に文字列的な意味はない。 このETagはリソースに紐付けているため、たとえETagの文字列が同じでも違うリソース同士では意味が無い。

ETagの生成方法は静的ファイルと動的ページでは異なる。

  1. 静的ファイルの場合

    Apacheのデフォルトでは、静的ファイルのETagの値はinode番号、ファイルサイズ、更新日時から自動で計算してくれる。 ただし、inode番号は、同一内容のファイルでもファイルシステムが異なれば別の値となる。 例えば、サーバを分散されている時に注意すべきである。 そのため、ファイルサイズと更新日時のみでETagの値を計算させるようにする。

  2. 動的ページの場合 ApacheなどのWebサーバは動的に生成されるHTMLやフィードなどは自動的にETagを計算してくれない。 そのため、Webアプリケーション側でETagの値を計算する必要がある。 一般的には、リソースのメタデータ(更新日時、サイズなど)から作成する方法や、リソースの更新カウ

使い分け

それぞれを使い分けるための方針は次のようになる。

  • Last-Modifiedヘッダしか分からない場合はIf-Modified-Since
  • サーバがETagヘッダを出している場合にはIf-None-Match
  • 時計を持っていないサーバに対してはIf-None-Match
  • ミリ単位で変更される可能性のあるリソースにはIf-None-Match

持続的接続

HTTP1.1での新機能がこの持続的接続(Persistent Connection)

HTTP1.0までは、サーバがレスポンスを返すたびにTCPコネクションを切断していた。 これを解決するために、Keep-Aliveヘッダを利用して、まとめて接続し続ける手法が開発された。 これにより、クライアントはレスポンスを待たずに、同じサーバにリクエストを送信することが出来る。

HTTP1.1では、このKeep-Aliveが標準化されている。 コネクションを切断したい場合は、リクエストのConnectionヘッダにcloseという値を指定すれば良い。