Wireless・のおと

サイレックスの無線LAN 開発者が語る、無線技術についてや製品開発の秘話、技術者向け情報、新しく興味深い話題、サイレックスが提供するサービスや現状などの話題などを配信していきます。

前の記事:「JavaScript のはなし」へ

HTTPクッキーのはなし

2016年5月27日 10:00
YS
今回の話題は「クッキー」です。HTTP のこぼれ話というか、番外編みたいなものです。まぁ、ミルクティーでも淹れて気軽に読んでください。

HTTP Cookie とは
既に解説したように、HTTP はもともと「指定されたサーバから指定されたファイルを取ってくるだけ」のプロトコルとして開発されました。しかし HTTP/1.0 で POST 機能が追加され HTML FORM を用いた CGI ページが増えてくると、ユーザー毎に異なるデフォルト値を FORM に設定したいという要求も出てきました。例えば「掲示板」CGI ならば、一度発言した後はユーザ名・e-mail・WEB サイトなどの項目がデフォルトで再表示される、といったように。
クッキーはそういったデータをクライアント(ブラウザ)側で保持するための仕組みです。例によって例のごとく Netscape Communications 社がブラウザ Navigator に実装したものが原型で、1997 年には RFC2109 として正式に仕様公開されました(※註)。

※註:2000 年には更新版の RFC2965 が公開されていますが、もはや著者欄に Netscape の名前は無く Bell lab と Epinions.com になっています。RFC2965 では Cookie2: という新フォーマットが定義されましたが、これは結局使われることなくスルーされ、RFC6265(2011) では公式に「なかったこと」になりました。


HTTP Cookie の実装と動作

クッキーの動作を説明するために、まず簡単な CGI FORM を仮定しましょう。「name」「password」「comment」の3フィールドを持つ掲示版のフォームで、cookie-test.cgi というスクリプトへの POST で動作するという前提です。HTML では次のようなものになります。

<html><body>
<br>
<form method=POST action="cookie-test.cgi">
Name:<input type="text" name="username"><br>
Password:<input type="password" name="password"><br>
<textarea name="comment" cols=80 rows=10></textarea><br>
<input type="submit">
</form>
</body></html>

この HTML に対応する CGI が URL http://www.arutokoro.jp で動作していると仮定して、ブラウザで http://www.arutokoro.jp/cookie-test.cgi を開くと、HTTP サーバ www.localhost.com に「GET /cookie-test.cgi」のリクエストが送信されます。そしてサーバ上で CGI スクリプトが実行され、既存発言の一覧と FORM を含んだ HTML 文書が作成され HTTP レスポンスとして返されます。この時点ではまだクッキーは登場せず、通常の HTTP トランザクションと違いありません。

(ブラウザからの送信)
GET /cookie-test.cgi HTTP/1.1

(サーバからの返答)
HTTP/1.0 200 OK
Content-Type: text/html
Content-Length:...

<html><body>
<br>
<form method=POST action="cookie-test.cgi">
:
:

さて、掲示板に発言してみましょう。発言名=Momotaro、パスワード=Kibi Dango、発言内容="Does anybody interested Oni conquest?" を入力して Submit を押すと、ブラウザからサーバに対し、フォーム内容が Content として付加された HTTP Post request が送信されます。この時点でもまだクッキーは登場してきません。

(ブラウザからの送信)
POST /cookie-test.cgi HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Content-Length:85

username=Momotaro&password=Kibi+Dango&comment=Does+anybody+interested+Oni+conquest%3f


クッキーが働くのは、この POST を受信した CGI が返答を返すところです。CGI は「ブラウザに覚えておいてもらいたい情報」をまとめて文字列にし、それを Set-Cookie: ヘッダとして HTTP レスポンス部分に付加して返送します。

(サーバからの返答)
HTTP/1.0 200 OK
Set-Cookie: cookie-test="username:Momotaro,password:Kibi Dango"
Content-Type: text/html
Content-Length:...

<html><body>
<br>
2016/3/24 12:05 Momotaro: Does anybody interested Oni conquest?<br>
<br>
<form method=POST action="cookie-test.cgi">
Name:<input type="text" name="username"><br>
:
:

ブラウザは Set-Cookie: ヘッダを検出すると、name=value ペアの羅列としてそのページに関連づけて記憶します。ここで重要なポイントは、ブラウザ側はクッキーを「なんだかわからない文字列」として保存するだけで、その中身については原則として理解しないしする必要もない(※註)というところです。

※註:クッキーの後に ';' で続けて連ねるクッキー属性(cookie-av)についてはフォーマットと機能が定義されていますが、ここにも仕様の混乱があります。例えばクッキー有効期限の属性は RFC2109 において「Max-Age=秒数」形式が定められているものの、世に出回っている多くの CGI では Netscape 独自時代の形式「Expires=GMT日時」を使っています。しかし Expires は RFC2109/2965 には正式には記述されていません(「互換性に対する考慮」としてわずかに言及されているだけ)。この辺りの矛盾も RFC6265 で是正されました。

ここでブラウザを閉じ、もう一度 CGI ページを開き直すとしましょう。ブラウザは自分の持つデータベースから「以前に www.arutokoro.jp を訪れたときクッキーを渡された」ことを覚えていて、その値を引っ張り出して Cookie: ヘッダとしてリクエストに付加します。この時もブラウザはクッキーを「なんだかわからない文字列」として付加するだけで、その中身については関与しません。

(ブラウザからの送信)
GET /cookie-test.cgi HTTP/1.1
Cookie: cookie-test="username:Momotaro,password:Kibi Dango"

クッキー機能が意味を持つためには、ブラウザから送られた Cookie: ヘッダを CGI 側で解釈し処理する必要があります。CGI では通常、環境変数 HTTP_COOKIE を用いて Cookie: ヘッダの値を参照することができます。CGI スクリプトをクッキーに対応する場合、リクエスト処理部分で HTTP_COOKIE を name=value のペア(RFC2109 の仕様上 ';' が区切り文字)に分解し、特定の name (ここでは「cookie-test」)に対応する value を引き出して、その値を更に分解します(ここでは ',' が区切り文字になっているが、これは実装固有)。

username:Momotaro
password:Kibi Dango

※註:この例ではクッキー文字列を「素のまま」扱っていますが、これだと " や ; のような文字や多バイト長文字の扱いに問題が出るので、何らかのエンコード処理(urlencode や BASE64)を施すのが「お作法」です。

これを更に ':' を区切り文字として(これも実装固有) name:value のペアに再分解し、その結果を必要に応じて FORM のデフォルト値に設定し HTML としてブラウザに返します。ここでは username と password がクッキーの対象ですから、CGI を Perl で書くなら

(前略)
:
:
@pairs = split(/\;/, $ENV{'HTTP_COOKIE'});
foreach $pair (@pairs) {
  local($name, $value) = split(/\=/, $pair);
  $cookies{$name} = $value;
}
@pairs = split(/\,/, $cookies{'cookie-test'});
foreach $pair (@pairs) {
  local($name, $value) = split(/\:/, $pair);
  $value =~ s/\"//g;
  $local_cookies{$name} = $value;
}
$username = $local_cookies{'username'};
$password = $local_cookies{'password'};

if ($ENV{'REQUEST_METHOD'} eq "POST") {
  print $set_cookie_header;
}

print << "EOH";

<html><body>
@bbs_logs
<br>
<form method=POST action="cookie-test.cgi">
Name:<input type="text" name="username" value="$username"><br>
Password:<input type="password" name="password" value="$password"><br>
<textarea name="comment" cols=80 rows=10></textarea><br>
<input type="submit">
</form>
</body></html>
EOH


のようにしておけば、Cookie: で渡された username と password をフォームのデフォルト値として含めた HTML 文書を作って返すことができます。
...え、まどろっこしい?ごもっとも。Cookie がまどろっこしい(わかりにくい)のは「ブラウザに覚えるデータをわざわざサーバ経由で処理する」というところと、「name=value のペアが二重構造になる」ところでしょう。


HTTP Cookie の「まどろっこしさ」
Cookie は要するに「ブラウザで覚えるべき値」をサーバからの Set-Cookie: ヘッダとして「教えてもらい」、それを再生するときはブラウザで覚えていた「以前に入力した値」を Cookie: ヘッダとしてサーバに渡し、サーバ側で HTML に混ぜ込んで返してもらうという仕組みです。行きも帰りも2度手間ですね。
やっぱりみんなそう思ったのか、最近はクッキーに頼らずフォームデータをローカルにキャッシュできるブラウザが多くなっています。この場合、記憶すべきフォームやデータの選択はブラウザで勝手に行い、次回ロード時に以前の値を嵌めこむのもブラウザで勝手に行います(※註)。「フォームデータの記憶再生」という機能に限っていえば、クッキーはブラウザのフォームキャッシュ機能によって存在意義を失い盲腸化しつつあると言っていいでしょう。

※註:フォームデータの記憶がキャッシュによるものかクッキーによるものかは、「ソースコードを表示」してみればわかります。キャッシュならばソースの HTML に値は入っていませんが、クッキーならば <input type="text" name="username" value="Momotaro"> のようなかたちで入っているはずです

クッキーのフォーマットが二重構造になっているのは技術的必然性からではなく、歴史的な理由です。RFC2109 ではクッキーの動作例として複数の name=value ペアが Cookie: ヘッダに乗る様子が示されており、少なくとも昔はクッキー名を FORM 名に直接紐付けようとしていた形跡が伺えます。しかし複数の name=value を Set-Cookie: に載せる文法について、RFC2109 では「Set-Cookie:, followed by a comma-separated list of one or more cookies」と文中に記されていただけで実例が示されず、しかもコンマの存在も ABNF 文法には示されなかったので混乱がありました。この中途半端な既述は何故か RFC2965 の Set-Cookie2 にも丸写しで受け継がれてしまい、14 年後の RFC6265 でようやく「"Set-Cookie" followed by a ":" and a cookie」という既述に改められ、「Set-Cookie: で渡せるクッキー名は1個だけ」と明示されました。

クッキー名の混乱については仕様上の曖昧さだけではなく、実装上の問題もありました。古い Netscape ブラウザの実装ではクッキー名は「ページ(URL)ごと」ではなく「ドメイン(サーバ)ごと」のデータベースとして管理されていたので、同じサーバ上で動く複数の CGI が同じクッキー名を使うと衝突して上書きされてしまいました。対症療法として CGI 固有のクッキー情報は「CGI の識別名」をクッキー名とし(上記の例では "cookie-test")、その CGI に含まれるフォーム情報は1本の文字列として value に収める実装がデファクトスタンダードとして普及し、それが「CGI のお作法」として定着したようです。RFC2109 で定義された Set-Cookie 属性の "Path" はクッキー名をページごとに管理すべく追加されたもののようですが、クッキー対応の CGI スクリプトには起源の古いものが多く、必ずしも Path を付加するとは限りません。


サードパーティ・クッキー
今日のブラウザの多くはクッキーの有効・無効を設定でき、更には「ファーストパーティ・クッキー」「サードパーティ・クッキー」の有効・無効を個別に設定できるものもあります。しかし「サードパーティ・クッキー」って何?クッキーに純正品と OEM でもあるの?と疑問に思うかもしれません。
まず「ファーストパーティ・クッキー」というのは、HTML 文書の URL に直接関連づけられれるクッキーです。これに対して「サードパーティ・クッキー」は、HTML 文書の中から img href= などのタグで参照される他サイトからの引用コンテンツに付随するクッキーです。
クッキーはクライアントからの投稿データに付随するため、HTTP POST 動作と組み合わせて使うものと思われがちですが、この連載を通して何度か書いてきたように、HTTP の動作において POST と GET に大きな違いはありません。リクエストが POST であろうが GET であろうがレスポンスの形式は同じですから、Set-Cookie: ヘッダは GET レスポンスに付加しても構いません。ブラウザは POST だろうが GET だろうがレスポンスにクッキーが付いてくれば(設定で無効化されていないかぎり)記憶し、次回同じ URL へのアクセスには記憶していたクッキーを Cookie: ヘッダに付加します。
クッキーはもともとユーザーの投稿データを再生するための仕組みでしたが、サードパーティ・クッキーではサーバー側で生成した疑似乱数 ID をクッキーとして返すことでユーザーの追跡(トラッキング)を行います。クッキーは基本的に「サーバから教えて貰った情報をブラウザ側でキャッシュして再送信する」だけの仕組みですから、サードパーティ・クッキーを使っても原則としてログイン名やパスワードなどの個人情報を盗むことはできません。しかしサーバ側で発行した疑似乱数 ID をデータベースとして記憶すれば、どこの誰かはわからなくとも「以前にアクセスがあった人か、初めての人か」という区別はつきます。更にデータベースにいつ(タイムスタンプ)・どこから(IP アドレス)・どのようにして(Referer: ヘッダによる参照元追跡)リクエストが来たかという情報も追加すれば、それなりに有用なトラッキング情報を得ることができます。
広告バナーやカウンタのいわゆる「アクセス解析サービス」はこういう仕組みで動いているわけですが、しかし「監視されているようで気持ちが悪い」という人も増えてきたことから、ブラウザ側でサードパーティ・クッキー無効化を選択できるものが増えてきたという事情です。


Cookie と JavaScript
このようにまどろっこしく中途半端で、フォームデータの記憶再生機構としては盲腸化しつつあり、トラッキングに使われるためセキュリティ懸念の対象にされたりするクッキーですが、じゃあクッキーなんてもう要らない?というと実はそうでもありません。クッキーは JavaScript と組み合わせることで、サーバに頼らない状態の保存・再生に使えるからです。
HTML に埋め込まれた JavaScript からは document.cookie プロパティを用いて「そのページに関連づけられているクッキー情報」を参照でき、document.cookie に対して値を書き込むことでクッキー値を追加・更新することもできます。これはサーバを経由しないローカルな操作であり、もはや HTTP POST だとか Cookie: / Set-Cookie: ヘッダは関与しません。
JavaScript のクッキー API は必ずしも使いやすいものではなく、また「ブラウザのキャッシュに記憶するだけ」なので用途は限られます。同じユーザーアカウントでも別 PC からログインすればクッキーは参照できませんし、同じ PC 上でも異なるブラウザ間では参照できません。事故であれ意図的操作であれ、キャッシュを消去すればクッキーも一緒に消えてしまいます。ネットショッピング購入履歴のような情報ならばサーバ上に記録すべきで、まかり間違ってもクッキーなどに頼るべきではありません。しかし、「サーバ上でアカウント管理しなくてもデータや状態の保存再生に使える」仕組みとしては、知っていると便利(なことがあるかもしれない)という程度のものです。


まとめ
「HTTP クッキー」という名前は聞いたことはあっても、それが具体的に何をするものであり、どんな仕組みで動いていているのかは知らない人は多いのではないかと思います。CGI スクリプトを書いたことのある技術者でも、クッキー処理は「グーグルで検索して出てきたコードをコピペ」している場合が多いのではないでしょうか。実際 RFC6265 が世に出るまでは仕様と実態が乖離していたわけで、RFC だけを信じて Set-Cooike2: ヘッダを使ったり Max-Age 属性を与えてもまともに動かず、「何だかよくわからないけれど、昔から伝えられている CGI ソースに合わせてみたら動いた」みたいな妙な世界でした。まぁ例によって例のごとく「世間によくある話」ですね。

次の記事:「802.11adのはなし」へ

最新の記事

カテゴリ

バックナンバー