ウェブサービスにログインすると、次のページに移動してもログイン状態が維持されています。しかし、少し立ち止まって考えると、不思議に思いませんか? 「ログイン」という操作は一回しかしていないのに、なぜその後もずっと「ログイン済み」として扱われるのでしょうか。
この記事では、サイトにログインしたあとにログイン状態がどのように維持されるのか、その仕組みをできるだけ丁寧に解説します。特にCookieを中心に、JWT(JSON Web Token)やlocalStorageといった関連技術についても触れながら、全体像を理解できるようにまとめています。
この記事で説明する設定について
Cookieの有効期限・HttpOnly・Secure・SameSiteといった設定は、すべてサービスを運営するウェブサーバー側が行うものです。ブラウザを使ってウェブサービスを閲覧している一般ユーザーが自分で設定・変更できるものではありません。この記事は「なぜそうなっているのか」を理解するための解説であり、読者に何か操作を求めるものではありません。
※この記事はAI(Gemini 3.8 Flash)に確認しながら執筆しました。技術的な内容の正確性については、個人の責任においてご参考いただくようお願いいたします。
- まず前提として:HTTPはリクエストのたびに「初対面」
- ログインの流れを確認する
- Cookieとは何か? どのドメインに送られるか?
- セッションCookieとセッション管理の仕組み
- 実際にCookieを確認してみる(Windows 11 / Chrome・Edge)
- Cookieの有効期限:セッションCookieと永続Cookieの違い
- 「ログインを保持する」機能の仕組み(Remember me)
- CookieのセキュリティとHttpOnly・Secure属性
- CSRF攻撃とは? Cookieの弱点と対策
- ログアウトするとCookieはどうなるか?
- 補足:JWTとlocalStorageを使った方法について
- まとめ
まず前提として:HTTPはリクエストのたびに「初対面」
ウェブブラウザとサーバーのやりとりは「HTTP」というプロトコルで行われています。このHTTPには重要な特徴があります。それはステートレス(状態を持たない)という性質です。
どういうことかというと、HTTPは1回のリクエスト(ページを取得する操作)とレスポンス(その返答)が完結した時点で、通信の「文脈」をすべて忘れてしまいます。つまり、あなたがサーバーに「このページを見せてください」とリクエストを送るたびに、サーバーはそのリクエストを完全に独立したものとして扱います。
これが何を意味するかというと、「前のリクエストでログインしたユーザーだ」という情報が、次のリクエストには引き継がれないということです。
ユーザー → サーバー:「トップページをください」(リクエスト1)
サーバー → ユーザー:「はい、どうぞ」(レスポンス1) ← ここで通信は終了、サーバーは忘れる
ユーザー → サーバー:「マイページをください」(リクエスト2)
サーバー → ユーザー:「あなたは誰ですか?」← これがHTTPの素の動作
この問題を解決するために生まれたのが「Cookie(クッキー)」です。
ログインの流れを確認する
仕組みの本題に入る前に、ログイン時にどのような処理が行われるかを確認しておきましょう。

この流れの中で、ログイン状態を「維持する」鍵を握っているのは⑥〜⑨のCookieを使ったやりとりです。
Cookieとは何か? どのドメインに送られるか?
Cookie(クッキー)とは、ウェブサーバーがブラウザに保存させる小さなテキストデータのことです。
Cookieが生まれた目的の一つは、まさにHTTPのステートレスという問題を補うことです。サーバーはブラウザにCookieを保存させ、ブラウザは次回からそのサイトにアクセスするたびに、自動的にそのCookieをサーバーへ送り返します。
サーバー → ブラウザ:「このデータを覚えておいてください(Cookie)」
ブラウザ → サーバー:「以前もらったデータはこれです(Cookie送信)」
Cookieは具体的には名前=値の形式で保存されます。ログインの文脈では、session_id=abc123xyzのような形で、ユーザーを特定するための「証明書」として機能します。
Cookieはどのドメインに送られるか?
ブラウザがCookieを送る相手は、Cookieを発行したサーバーのドメインに限定されます。example.comで発行されたCookieがother.comに送られることはありません。
ドメインの判定は次のように行われます。
① CookieのDomain属性
サーバーがCookieを発行するとき、Domain属性を指定できます。
Set-Cookie: session_id=abc123; Domain=example.com
この場合、ブラウザはexample.comはもちろん、サブドメイン(shop.example.comやlogin.example.com)にもこのCookieを送ります。同じ組織のサービスが複数のサブドメインに分かれている場合に、ログイン状態を共有するために使われます。
Domain属性を省略した場合は、発行したサーバーのホスト名とまったく同じドメインにだけCookieが送られます(サブドメインには送られません)。
② 「同一サイト」の定義:eTLD+1
のちに説明するSameSite属性で「同じサイト」「別のサイト」という判定をするとき、ブラウザはドメイン全体ではなく、eTLD+1(イーティーエルディープラスワン)という単位で判定します。
公開サフィックス(eTLD)とは何か
eTLD(effective Top Level Domain)は「公開サフィックス」とも呼ばれ、一言で言うと「ドメイン名の末尾にある、誰でも自由に変えられない固定部分」のことです。
ドットの数ではなく、「どこまでが変えられない固定部分か」で決まります。
.comの場合
.comは、その左側に好きな名前を付けてドメインを登録できます(example.com、shop.comなど)。ですから.com全体が1つの公開サフィックスです。
↓ここが固定(公開サフィックス)
example .com
↑
ここは自由に決められる(登録できる部分)
.co.jpの場合
.jpだけを見ると、anything.jpのように.jpの左に名前を付けて登録できるように思えます。しかし日本のドメイン制度では、企業向けには.co.jpという2段構造が使われており、.co.jpの部分全体が公開サフィックスとして扱われています。つまり.coの部分も固定されており、その左側(exampleの部分)だけが登録できます。
↓ここが固定(公開サフィックス)
example .co.jp
↑
ここは自由に決められる(登録できる部分)
.co.jpと.comはドットの数が違いますが、「どこまでが固定か」という意味では同じ役割を果たしています。.co.jpの.coは国別の用途分類であり、登録者が自由に変えられる部分ではありません。
eTLD+1の判定
eTLD+1とは、公開サフィックス(eTLD)の左隣の名前ラベルを1つ加えたものです。これがブラウザがSameSite判定で「同一サイト」と見なす単位になります。
| URL | eTLD(公開サフィックス) | eTLD+1(サイト単位) |
|---|---|---|
www.example.com | .com | example.com |
shop.example.com | .com | example.com |
example.co.jp | .co.jp | example.co.jp |
shop.example.co.jp | .co.jp | example.co.jp |
この表を見ると、www.example.comとshop.example.comはサブドメインが違っても、SameSite判定の上では同じサイト(eTLD+1がexample.com)として扱われます。これはSameSite属性の「同一サイト」を判定するためのルールであり、Cookieを実際にどのドメインに送るかを決めるDomain属性とは別の話です。
補足:SameSite判定とDomain属性は独立した仕組み
「www.example.comとshop.example.comがSameSite判定で同じサイトとされる」のは、どちらもeTLD+1がexample.comだからです。Cookie自体がshop.example.comにも送られるかどうかは、CookieのDomain属性の設定によります(Domain=example.comを指定した場合はサブドメインにも送られます)。
Cookieの主な特徴
- ブラウザが自動的に送信する:設定したドメインへのリクエストには、毎回自動でCookieが付いてきます。
- 有効期限を設定できる:一定時間が過ぎると自動的に削除されるように設定できます。
- 特定のドメインにのみ送られる:
example.comのCookieがother.comに送られることはありません。
セッションCookieとセッション管理の仕組み
ログイン状態の維持に使われる最も一般的な方法は「セッション管理」です。「セッション(Session)」とは、ユーザーがサイトを訪れてから離れるまでの一連のやりとりをひとかたまりとして扱う概念です。
セッション管理の具体的な仕組み
ログインが成功すると、サーバーは次のことを行います。
① セッションIDの生成
サーバーは推測されにくいランダムな文字列(セッションID)を生成します。例:a8f3b2c9d7e1f456...
② セッション情報をサーバー側に保存
サーバーは「セッションID:a8f3b2… = ユーザーID:12345(山田太郎)」という対応関係を、データベースやメモリ上に保存します。
③ セッションIDをCookieとしてブラウザへ送信
サーバーはHTTPレスポンスに以下のようなヘッダーを付けて、ブラウザにCookieを保存するよう指示します。
Set-Cookie: session_id=a8f3b2c9d7e1f456; HttpOnly; Secure; Path=/
④ ブラウザが以降のリクエストにCookieを自動付与
ブラウザは次のページにアクセスするたびに、以下のヘッダーを自動的に付けてリクエストを送ります。
Cookie: session_id=a8f3b2c9d7e1f456
⑤ サーバーがCookieを照合してユーザーを特定
サーバーは受け取ったセッションIDをサーバー側に保存しているデータと照合し、「このリクエストは山田太郎さんからのものだ」と判断します。
ポイント:ブラウザにはセッションIDしか渡されていない
重要なのは、ブラウザのCookieに保存されているのはセッションIDだけだということです。「山田太郎」「メールアドレス」「パスワード」といった個人情報はサーバー側にのみ保存されており、Cookieにはそれらを参照するための「番号」しか入っていません。これはセキュリティ上の重要な設計です。
実際にCookieを確認してみる(Windows 11 / Chrome・Edge)
ここまでの説明を「見てみる」ことで、理解が深まります。Windows 11に標準インストールされているChrome・Edgeのどちらでも、以下の手順でCookieを確認できます。
手順
① 確認したいウェブサービスにログインした状態でページを開く
② 開発者ツールを開く
以下のいずれかの方法で開きます。
- キーボードの
F12キーを押す - ページ上で右クリック →「検証」(Edgeの場合は「要素の検証」)
③「Application」タブを選ぶ(Chromeの場合)または「Storage」タブ(Edgeの場合)
④ 左側のメニューから「Cookies」→ サイトのURLをクリック
Application(またはStorage)
└── Cookies
└── https://www.example.com ← ここをクリック
⑤ 一覧にCookieが表示される
右側の一覧に、そのサイトのCookieが表示されます。以下のような列があります。
| 列名 | 意味 |
|---|---|
| Name | Cookieの名前(例:session_id、auth_token) |
| Value | Cookieの値(セッションIDなど) |
| Domain | このCookieが送られるドメイン |
| Expires / Max-Age | 有効期限(「Session」と表示されているものがセッションCookie) |
| HttpOnly | チェックが入っていればJavaScriptから読めない |
| Secure | チェックが入っていればHTTPSのみで送信 |
| SameSite | Lax / Strict / None のいずれか |
確認のポイント
「Expires / Max-Age」の列が 「Session」 と表示されているCookieは、ブラウザを閉じると削除されるセッションCookieです。具体的な日時が表示されているものは永続Cookieです。
「HttpOnly」と「Secure」にチェックが入っているCookieは、セキュリティ上適切に設定されたものです。逆にどちらもチェックがないCookieは、比較的重要でないデータ(表示設定など)である場合が多いです。
注意: 「Value」列に表示されているセッションIDは、あなたのアカウントに相当する重要なデータです。このスクリーンショットを他人に見せたり、インターネット上に公開したりしないようにしてください。
Cookieの有効期限:セッションCookieと永続Cookieの違い
Cookieには有効期限を設定できます。この設定の違いによって、ログイン状態がどれくらい続くかが変わります。
セッションCookie(有効期限なし)
有効期限を指定しないCookieは「セッションCookie」と呼ばれます。この種類のCookieは、ブラウザを閉じると自動的に削除されます。
Set-Cookie: session_id=a8f3b2c9d7e1f456; HttpOnly; Secure
→ タブを閉じてもCookieは残りますが、ブラウザ全体を終了するとCookieが消え、次回起動時にはログアウトした状態になります。
ただし、現代のブラウザは「セッション復元」機能を持っており、ブラウザを再起動してもタブを復元した場合にCookieが維持されることがあります。これはブラウザの機能であり、サーバーのCookie設定とは別の話です。
永続Cookie(有効期限あり)
有効期限(ExpiresまたはMax-Age)を指定したCookieは「永続Cookie」と呼ばれます。ブラウザを閉じても削除されず、指定した期限まで保持されます。
Set-Cookie: session_id=a8f3b2c9d7e1f456; Max-Age=2592000; HttpOnly; Secure
(Max-Age=2592000は秒数で、30日分です)
→ この設定では、30日間ブラウザを閉じても再度開いたときにログイン状態が維持されます。
「ログインを保持する」機能の仕組み(Remember me)
ログイン画面でよく見かける「ログインを保持する」「ログイン状態を維持する」というチェックボックスがあります。これはまさにCookieの有効期限と直結した機能です。
チェックを入れない場合
サーバーはセッションCookie(有効期限なし)を発行します。ブラウザを閉じるとログアウト状態になります。
チェックを入れた場合
サーバーは有効期限付きの永続Cookieを発行します。期限は7日、30日、90日など、サービスによって異なります。

セキュリティ上の注意
「ログインを保持する」機能は便利ですが、使用する端末によっては注意が必要です。自分だけが使うパソコンであれば問題は少ないですが、共有端末や公共のパソコンでこの機能を有効にすると、後から使う人があなたのアカウントにアクセスできてしまう可能性があります。
CookieのセキュリティとHttpOnly・Secure属性
セッションIDを格納するCookieは、盗まれると第三者があなたのアカウントを乗っ取れてしまう重要なデータです。そのため、サーバーはCookieにセキュリティ上の属性を付けて安全性を高めます。
HttpOnly属性
Set-Cookie: session_id=a8f3b2c9d7e1f456; HttpOnly
HttpOnlyを付けると、JavaScriptからそのCookieの値を読み取れなくなります。
ウェブページに悪意のあるJavaScriptが埋め込まれて読み込まれた場合(XSS攻撃と呼ばれます)、CookieをJavaScriptで読み取れると盗まれてしまいます。HttpOnlyはこれを防ぐための属性です。
HttpOnlyなし | HttpOnlyあり |
|---|---|
JavaScript から document.cookie で値を読める | JavaScriptからは読み取れず、ブラウザがページを開いたり移動したりするときに、裏側で自動的にサーバーとやり取りするためだけに使われる属性です。 |
Secure属性
Set-Cookie: session_id=a8f3b2c9d7e1f456; Secure
Secureを付けると、HTTPS通信のときだけCookieが送信されます。HTTP(暗号化なし)の通信ではCookieが送られないため、通信の傍受によってCookieが盗まれるリスクを低減できます。
現代のウェブサービスでは、HttpOnlyとSecureの両方を設定するのがほぼ標準となっています。
SameSite属性
Set-Cookie: session_id=a8f3b2c9d7e1f456; SameSite=Lax
SameSite属性を理解するには、まず「リクエストを送るのは常にブラウザ」という前提を押さえた上で、「そのリクエストはどのページを表示しているときに発生したか」という点に注目する必要があります。
リクエストはいつも「ブラウザが送る」。では何が違うのか?
リクエストとはブラウザがサーバーに送るものです。ではSameSite属性でいう「他のサイトからのリクエスト」とは何を指しているのでしょうか。
ポイントは、「あなたが今どのサイトのページを見ているか」によって、ブラウザがCookieを付けるかどうかが変わるという点です。
ウェブページは、ページを表示するだけでなく、表示してるページの他のドメインのサーバーへリクエストを発生させることができます。
言い換えると、evil.comのページ(他のサイト)を表示してるときbank.comのサーバーへリクエストを発生させることができます。
たとえば:
evil.comのページ(他のサイト)内の<img src="https://bank.com/some-image">タグがあれば、ブラウザはbank.comに画像のリクエストを送るevil.comのページ(他のサイト)内のJavaScriptがbank.com/api/...にデータを送信することができるevil.comのページ(他のサイト)内のフォームがbank.comへ向かって自動送信されることもある
これらはすべて「ブラウザが送るリクエスト」です。しかし、そのリクエストを引き起こしたのはevil.comのページ(他のサイト)です。
SameSite属性はこの「リクエストを引き起こしたページが、どのサイトか」を見て、Cookieを付けるか付けないかを判断します。
[SameSite=Lax の場合の動作]
① あなたが example.com のページを閲覧中
→ example.com へのリクエスト発生
→ ブラウザは example.com の Cookie を付けて送信 ✅(同じサイトなのでOK)
② あなたが evil.com のページを閲覧中
→ evil.com のページが example.com へのリクエストを発生させる
→ ブラウザは example.com の Cookie を付けない ❌(別サイトが引き起こしたのでNG)
SameSite属性の各設定値の意味はこのようになります。
| 値 | ブラウザの動作 |
|---|---|
Strict | 別のサイトのページから引き起こされたリクエストには、一切Cookieを付けない |
Lax | 別サイトから引き起こされたリクエストのうち、リンクをクリックして移動する(GETリクエスト)場合だけCookieを付ける。それ以外(フォーム送信や非表示リクエストなど)は付けない。現在のブラウザの標準設定 |
None | どのサイトのページから引き起こされたリクエストにもCookieを付ける(Secure属性が必須) |
CSRF攻撃とは? Cookieの弱点と対策
Cookieはブラウザが自動的に送信するという便利な特性を持っていますが、これが同時に弱点にもなります。
CSRF(クロスサイトリクエストフォージェリ)とは
SameSite属性の説明で整理した「どのページを表示しているときにリクエストが発生したか」という概念を使うと、CSRF攻撃はシンプルに理解できます。
前提:この攻撃が成立する条件
次の2つが重なったときにCSRF攻撃が成立します。
bank.comのCookieにSameSite属性が設定されていない、またはSameSite=Noneになっている
SameSite=Lax(現代のブラウザのデフォルト)またはStrictが設定されていれば、後述する攻撃の手口はブラウザによって原則ブロックされます。evil.comのページに、bank.comへリクエストを送るコードが書かれているevil.comを開くだけでは何も起きません。そのページのHTML・JavaScriptの中に、bank.comへリクエストを送る仕掛け(隠しフォームの自動送信など)が仕込まれていることが攻撃の前提です。
攻撃の流れを具体的に追ってみます。
あなたがあるサービス(bank.com)にログイン済みの状態で、別のページ(evil.com)を開いたとします。bank.comのCookieはSameSiteが設定されていないものとします。
[CSRF攻撃が成立するまでの流れ]
1. あなたは bank.com にログインしている
→ ブラウザには bank.com の Cookie(セッションID)が保存されている
2. 別のタブやリンクで evil.com を開く
3. evil.com のページに次のような仕掛けが書かれていたとする:
<form action="https://bank.com/transfer" method="POST">
<input type="hidden" name="amount" value="10000">
...
</form>
<script>document.forms[0].submit();</script>
→ ページを開いた瞬間、JavaScriptがフォームを自動送信する
4. このリクエストはブラウザが bank.com に送る。
SameSite属性がないため、ブラウザは bank.com の Cookie を自動的に付けてしまう。
5. bank.com のサーバーは Cookie を確認する。
正規のセッションIDが付いているため「あなた本人からのリクエスト」と判断してしまう。
→ あなたの意図しない操作が成立してしまう = CSRF攻撃成功

ここで重要なのは「リクエストを送ったのはブラウザ」ですが、「そのリクエストを引き起こしたのは evil.com のページに書かれたコード」だという点です。ブラウザはCookieを自動的に付けることに従順なため、誰が引き起こしたかを区別しません。
現代のブラウザでは原則ブロックされる
現代のブラウザ(Chrome・Edge・Firefox・Safariなど)はCookieのデフォルトをSameSite=Laxとしています。このため、上記のようなフォーム自動送信(POST)は別サイトが引き起こしたとみなされ、Cookieが付かなくなります。その結果、bank.comサーバーはCookieを受け取れず「未ログイン」として扱い、攻撃が成立しません。
ただし、古いサービスやSameSite=Noneを明示的に設定しているサービスでは依然としてリスクがあるため、次の対策が組み合わせて使われています。
対策
- SameSite Cookie属性(
LaxまたはStrict):前のセクションで説明した仕組みにより、evil.com のページが引き起こしたリクエストにはCookieを付けなくなる。現在のブラウザはこれをデフォルトで適用している。 - CSRFトークン:サーバーがフォームに秘密の文字列(トークン)を埋め込み、リクエスト受信時にそのトークンを照合する。evil.com のコードはこのトークンを知る手段がないため、不正リクエストを弾ける。SameSiteの対策と組み合わせることでより確実になる。
ログアウトするとCookieはどうなるか?
ログアウト操作を行うと、ログイン状態を終了するために次の処理が行われます。

ポイント:サーバー側の削除が重要
たとえブラウザのCookieを手動で削除したとしても、サーバー側のセッション情報が残っている場合、そのセッションIDを何らかの方法で入手した第三者はまだそれを使える可能性があります。
適切なログアウト処理とは、ブラウザのCookie削除とサーバー側のセッション削除の両方を行うことです。多くの安全なサービスでは、ログアウト時にサーバー側のセッションを必ず無効化しています。
補足:JWTとlocalStorageを使った方法について
ここまではCookieとサーバー側セッションを使う方法を中心に解説しました。しかし近年では、異なるアーキテクチャも広く使われています。それぞれの仕組みと、なぜそれが使われるのかを説明します。
JWT(JSON Web Token)とは
JWTは、ユーザー情報(補足参照)や認証状態をトークン(文字列)としてエンコード(特定のルールに従って、ひとまとめの文字列に変換)し、そのトークン自体に必要な情報を含める仕組みです。
従来のセッション管理では、サーバーがセッション情報を保存しておく必要がありました。JWTを使う場合、サーバー側には何も保存せず、発行したトークンをブラウザが保持してリクエストのたびに送ります。
従来(サーバー側セッション):
ブラウザ → セッションID → サーバー → 「このIDに対応するユーザーはDB確認してみよう」
JWT:
ブラウザ → JWT(ユーザー情報入り) → サーバー → 「JWTの署名を検証するだけでOK」
JWTにはデジタル署名が付いているため、サーバーはその署名を検証することで「このトークンは本物か?」を確認できます。
JWTのメリット:「サーバーに保存しなくていい」とはどういうことか
「サーバーにセッション情報を保存しなくていい」という点のメリットが分かりにくいかもしれません。これは、サービスの規模が大きくなると見えてくる問題です。
多くのサービスでは、アクセス数が増えたとき、サーバーを複数台に増やして処理を分散させます(スケールアウトと呼びます)。
[従来のセッション管理で複数サーバーがある場合の問題]
リクエスト1(ログイン)→ サーバーA が処理 → セッション情報はサーバーAに保存
次のリクエスト(ページ閲覧)→ 今度はサーバーB が処理
→ サーバーBにはセッション情報がない
→ 「あなたは誰ですか?」になってしまう
この問題を解決するには、全サーバーがアクセスできる共有データベースにセッション情報を保存する設計が必要です。それは可能ですが、リクエストのたびにDBへの問い合わせが発生するため、処理の負荷になります。
[JWTの場合]
リクエスト1(ログイン)→ サーバーA がJWTを発行(サーバーには何も保存しない)
次のリクエスト(ページ閲覧)→ サーバーB が処理
→ ブラウザから届いたJWTの署名を検証するだけ
→ DBへの問い合わせ不要 → そのままログイン済みと判定できる
JWTは「秘密鍵」を使って検証しますが、この「秘密鍵の共有方法」にはいくつかの方式があります。これはセキュリティ上の重要な設計です。
秘密鍵の共有方式:対称鍵と非対称鍵
対称鍵方式(HMAC-SHA256など)
1つの秘密鍵(同じ鍵)でJWTの署名と検証の両方を行います。複数のサーバーがこの鍵を共有します。
認証サーバー:秘密鍵Kを使ってJWTに署名する
APIサーバーA:同じ秘密鍵Kを使って署名を検証する
APIサーバーB:同じ秘密鍵Kを使って署名を検証する
- 設定がシンプルで処理が速い
- 秘密鍵Kが漏洩すると、攻撃者が自分でJWTを偽造できてしまうため非常に危険
- 鍵を共有するサーバーが増えるほど、鍵を安全に保管・管理するコストが上がる
非対称鍵方式(RS256・ES256など)
署名に使う「秘密鍵(Private Key)」と、検証に使う「公開鍵(Public Key)」を分けます。
認証サーバーのみ:秘密鍵(Private Key)でJWTに署名する
APIサーバーA:公開鍵(Public Key)で署名を検証する
APIサーバーB:公開鍵(Public Key)で署名を検証する
- 公開鍵は名前の通り公開してよい情報。漏洩しても攻撃者はJWTを偽造できない(検証はできるが署名できない)※補足参照
- 秘密鍵は認証サーバー1台のみが持ち、他のサーバーには渡さない。これが最も安全な設計
- 設定はやや複雑だが、大規模システムやマイクロサービスでは非対称鍵が広く採用されている
| 方式 | 署名できるサーバー | 検証できるサーバー | 漏洩した場合のリスク |
|---|---|---|---|
| 対称鍵(HMAC) | 鍵を持つ全サーバー | 鍵を持つ全サーバー | JWTの偽造が可能になる(高リスク) |
| 非対称鍵(RS256など) | 秘密鍵を持つ認証サーバーのみ | 公開鍵を持つ全サーバー | 公開鍵が漏洩しても偽造不可。秘密鍵漏洩は高リスク |
いずれの方式でも、秘密鍵は環境変数や鍵管理サービスで厳重に保護します。万一漏洩した場合は、鍵を即座に無効化して新しい鍵を再発行する運用が必要になります。
補足:JWTの内容(ペイロード)は誰でも読める
ここで一点注意が必要です。「公開鍵が漏洩しても安全」というのは「JWTを偽造できない」という意味であり、「JWTの内容が読めない」という意味ではありません。
JWTのペイロード部分はbase64urlというフォーマットで記録されているだけで、暗号化はされていません。JWTトークンを入手した人は、鍵がなくても内容を読むことができます。
これが問題にならない理由は2点あります。①JWTはHTTPS通信でしか送受信しないため、通信中に盗まれることを防いでいる。②ペイロードにはユーザーIDや有効期限のみを入れ、パスワードや個人情報は入れない設計にする。この2つを守ることが、JWTを安全に使う上での大前提です。
なお、ペイロード自体も暗号化したい場合はJWE(JSON Web Encryption)という別の仕様がありますが、通常のログイン認証では使われないことが多いです。
なぜCookieではなく、localStorageにJWTを保存することがあるのか
「JWTをCookieに保存する」方法も存在しますが、「localStorageに保存する」方法が選ばれることもあります。その理由は、CookieがドメインをまたいでCookieを送ることができないという特性にあります。
Cookieは発行されたドメインにのみ自動送信されます。しかし、次のようなケースでは、この特性が制約になります。
[ドメインをまたいだAPI呼び出しの例]
ウェブアプリのドメイン: https://app.myservice.com
APIサーバーのドメイン: https://api.myservice.com
→ これらはサブドメインが違う。
Cookieの Domain 属性を工夫すれば送れるが、さらに別の会社のAPIを使う場合は無理。
例:
ウェブアプリ: https://myapp.com
外部APIサーバー:https://api.partner.com
→ Cookie は myapp.com のドメインにしか送られない。
api.partner.com には Cookie を送る手段がない。
このような場合、JWTをlocalStorageに保存しておき、リクエスト時にJavaScriptで取り出して、HTTPヘッダーに手動で付けて送る方法が使われます。
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...(JWT)
このヘッダーはCookieとは無関係なので、どんなドメインのサーバーにでも送れます。また、モバイルアプリなどはそもそもCookieの仕組みが標準的でないため、このヘッダー方式と組み合わせたlocalStorage/トークン保存がよく使われます。
ただし、localStorageはJavaScriptから自由に読み書きできるため、悪意のあるJavaScriptが読み込まれると中身を盗まれます(XSS攻撃)。セキュリティ要件が高いサービスでは、JWTをCookieに保存する(HttpOnly属性付き)設計が選ばれることもあります。
| 種類 | 保持期間 | JavaScriptからのアクセス |
|---|---|---|
localStorage | 明示的に削除するまで永続 | 可能(XSSリスクあり) |
sessionStorage | タブを閉じるまで | 可能(XSSリスクあり) |
Cookie(HttpOnly付き) | 設定による | 不可(XSS対策になる) |
まとめ:どの方法が使われるかはサービスによって異なる
| 方法 | 保存場所 | 主な用途 |
|---|---|---|
| セッションCookie + サーバーセッション | Cookie(セッションID)/ サーバーDB | 一般的なウェブサービス(ECサイト、SNSなど) |
| JWT + Cookie(HttpOnly付き) | Cookie(JWTトークン) | セキュリティを重視したSPA |
| JWT + localStorage | ブラウザのlocalStorage | 異なるドメインのAPIを呼び出すSPA・モバイルアプリなど(XSSに注意) |
一般的なウェブサービス(ECサイト、SNSなど)の多くはセッションCookieとサーバーセッションを使っています。
まとめ
この記事の内容を整理します。
- HTTPはリクエストのたびに状態を忘れるため、Cookieを使って「前回のリクエストと同じユーザー」であることを伝える仕組みが必要
- ログインするとサーバーが「セッションID」を発行し、CookieとしてブラウザへCookieに保存させる
- 以降のリクエストでは、ブラウザが自動的にCookieをサーバーへ送り、サーバーはそれを照合してログイン状態を確認する
- Cookieには有効期限があり、「セッションCookie(ブラウザを閉じると消える)」と「永続Cookie(期限まで保持)」の2種類がある
- 「ログインを保持する」機能は、Cookieの有効期限を延長することで実現している
- セキュリティ向上のため、CookieにはHttpOnly・Secure・SameSiteなどの属性が設定される
- ログアウトとは、ブラウザのCookie削除とサーバー側のセッション無効化を両方行うことで完了する
ログイン状態の維持は、一見すると当たり前のように見えて、実は複数の仕組みが連携して成り立っています。Cookieとセッションの関係を理解しておくことは、ウェブサービスを安全に使うための基礎的な知識として、エンジニアに限らず多くの人に役立ちます。
この記事を書いたイチゲを応援する(質問でもokです)
Vプリカでのお支払いがおすすめです。