- ネットワーク
Cookieとセッションの違いと仕組み|セッションIDの流れを図解
Cookie とは、サーバーがブラウザに保存させる小さなデータで、セッションとは、利用者ごとの状態(ログイン中かどうか、カートの中身など)をサーバー側で保持する仕組みです。 両者は対立するものではなく、セッションの「合言葉」に当たるセッションIDを、Cookie に入れてブラウザとサーバーの間で運ぶ、という組み合わせで使われます。データの本体はサーバーに、IDだけがブラウザにある、というのが基本の形です。
この記事では、ログインからセッションIDの発行・照合までの流れを、シーケンス図と実際に動かしたサーバーの HTTP ヘッダ(curl -v の出力)で追います。Node.js 22 の最小サーバー(追加パッケージなし)のコードをそのまま載せているので、手元で同じ結果を再現できます。Cookie の属性一覧、PHP・Express・Spring の既定の Cookie 名、ログイン時にセッションIDを作り直す理由、HttpOnly の確認、JWT や localStorage との違いも表で整理します。
Cookie とセッションの違い 早見表
「どこに何が保存されるか」を押さえると、違いがすっきり分かります。
| 観点 | Cookie | セッション |
|---|---|---|
| ひとことで | ブラウザに保存される小さなデータ | サーバーが利用者ごとに持つ状態 |
| 保存場所 | ブラウザ(利用者の端末) | サーバー(メモリ・ファイル・Redis・データベースなど) |
| 中身 | 名前と値の組(文字列) | 任意のデータ(ログイン中の会員ID、カートなど) |
| 容量の目安 | 1つあたり約 4KB まで | サーバーの保存先しだい(実質的に大きく取れる) |
| 寿命 | Expires / Max-Age で指定。指定なしはブラウザを閉じるまで | サーバー側の有効期限(例:最後のアクセスから30分) |
| 利用者による書き換え | できる(端末にあるため) | できない(サーバーにあるため) |
| 送受信 | リクエストのたびに自動で送られる | 送られるのはセッションIDだけ |
| 役割 | セッションIDの運び役、表示設定の保存など | ログイン状態など、書き換えられては困る情報の保持 |
| 関係 | セッションIDを入れる入れ物 | Cookie で届いたIDから状態を引き当てる |
| セッションIDとは | 説明 |
|---|---|
| 正体 | サーバーが作る、推測できない長いランダムな文字列 |
| 役割 | 「このリクエストは、さっきログインしたあの利用者のもの」と見分ける合言葉 |
| 運び方 | Set-Cookie でブラウザに渡し、以後は Cookie ヘッダで毎回戻ってくる |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Cookieとセッションの違いと仕組み|セッションIDの流れを図解」
なぜ Cookie とセッションが必要か:HTTP は前のやり取りを覚えない
HTTP は、1回のリクエストと1回のレスポンスで完結する通信です。サーバーは、前のリクエストを誰が送ったかを覚えていません。この性質を「ステートレス(状態を持たない)」と呼びます。
そのままでは、ログインに成功した次のページで、サーバーは「この人はログイン済み」と判断できません。そこで次の2つを組み合わせます。
- サーバーが利用者ごとに番号札(セッションID)を発行し、状態をサーバー側に保存する(セッション)
- 番号札をブラウザに持たせ、リクエストのたびに見せてもらう(Cookie)
受付で番号札を受け取り、荷物は預かり所に置いておく、という仕組みに近いものです。番号札そのものには荷物の中身は書かれていません。札を見せると、預かり所の人が対応する荷物を出してくれます。
セッションIDの流れを図で見る
ログイン前の初回アクセスから、ログイン、ログイン後のアクセス、ログアウトまでの流れです。
sequenceDiagram
participant B as ブラウザ
participant S as サーバー
participant D as セッションの保存先
B->>S: GET /(Cookie なし)
S->>D: 新しいセッションID「A」で保存
S-->>B: 200 OK + Set-Cookie: sid=A
Note over B: sid=A を保存
B->>S: POST /login(Cookie: sid=A、ID とパスワード)
S->>D: A を破棄し、新しいID「B」で保存(user=alice)
S-->>B: 200 OK + Set-Cookie: sid=B
Note over B: sid を B に上書き
B->>S: GET /me(Cookie: sid=B)
S->>D: B を検索
D-->>S: user=alice
S-->>B: 200 OK(alice さんのページ)
B->>S: POST /logout(Cookie: sid=B)
S->>D: B を削除
S-->>B: 200 OK + Set-Cookie: sid=; Max-Age=0
Note over B: sid を削除
ポイントは3つです。
- ブラウザが持つのはIDだけ。 「alice でログイン中」という情報はサーバー側にある
- Cookie は、ブラウザが自動で送る。 一度 Set-Cookie で受け取ると、同じサイトへのリクエストに毎回 Cookie ヘッダとして付く
- ログインに成功した時点で、IDを新しいものに取り替える。 理由は「ログイン時にセッションIDを作り直す」の節で実行結果とともに説明します
動かして確かめる:最小のセッションサーバー
図の流れを、実際に動くコードで確かめます。Node.js 22 の標準機能だけで書いた、セッション管理の最小サーバーです。仕組みを見るための学習用コードで、実際のサービスでは後述の express-session などの、よく使われているライブラリを使います。
// server.mjs
// 実行: node server.mjs (Node.js 22、追加パッケージなし)
// IDを作り直さない動きを見るとき: node server.mjs --no-regenerate
import http from 'node:http';
import { randomBytes } from 'node:crypto';
const REGENERATE = !process.argv.includes('--no-regenerate');
const sessions = new Map(); // セッションID -> データ(サーバーのメモリ)
const users = { alice: 'pass1234' }; // 例示用。本番ではハッシュ化して保存する
const newId = () => randomBytes(32).toString('base64url');
const parseCookie = (h = '') =>
Object.fromEntries(h.split(';').map(s => s.trim().split('=')).filter(p => p[0]));
const setSid = (res, sid) =>
res.setHeader('Set-Cookie', `sid=${sid}; Path=/; HttpOnly; SameSite=Lax; Max-Age=1800`);
http.createServer(async (req, res) => {
const url = new URL(req.url, 'http://x');
let body = ''; for await (const chunk of req) body += chunk;
const form = new URLSearchParams(body);
let sid = parseCookie(req.headers.cookie).sid;
let sess = sessions.get(sid);
if (url.pathname === '/') { // 初回アクセスでセッション開始
if (!sess) { sid = newId(); sess = { visits: 0 }; sessions.set(sid, sess); setSid(res, sid); }
sess.visits++;
return res.end(`visits=${sess.visits} user=${sess.user ?? '(未ログイン)'}\n`);
}
if (url.pathname === '/login' && req.method === 'POST') {
const name = form.get('user');
if (!name || users[name] !== form.get('pw')) { res.statusCode = 401; return res.end('NG\n'); }
if (REGENERATE || !sess) { // ログイン成功時にIDを作り直す
if (sid) sessions.delete(sid);
sid = newId(); sess = { visits: sess?.visits ?? 0 }; sessions.set(sid, sess); setSid(res, sid);
}
sess.user = name;
return res.end(`login ok: ${name}\n`);
}
if (url.pathname === '/me') {
if (!sess?.user) { res.statusCode = 401; return res.end('未ログイン\n'); }
return res.end(`user=${sess.user}\n`);
}
if (url.pathname === '/logout' && req.method === 'POST') {
sessions.delete(sid);
res.setHeader('Set-Cookie', 'sid=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0');
return res.end('logout\n');
}
if (url.pathname === '/page') { // HttpOnly の確認用
res.setHeader('Set-Cookie', ['theme=dark; Path=/', `sid2=${newId()}; Path=/; HttpOnly`]);
res.setHeader('Content-Type', 'text/html; charset=utf-8');
return res.end('<p id="out"></p><script>out.textContent = document.cookie</script>');
}
res.statusCode = 404; res.end('not found\n');
}).listen(7651, '127.0.0.1', () => console.log(`listening 7651 regenerate=${REGENERATE}`));
コードの要点は次のとおりです。
| 部分 | していること |
|---|---|
sessions(Map) |
セッションIDをキーに、利用者ごとのデータをサーバーのメモリに保存 |
newId() |
暗号用の乱数 32 バイトからIDを作る(推測できない長さにする) |
setSid() |
Set-Cookie ヘッダでIDをブラウザに渡す |
parseCookie() |
リクエストの Cookie ヘッダからIDを取り出す |
sessions.get(sid) |
届いたIDでデータを引き当てる。見つからなければ未ログイン扱い |
以下の実行結果は、Node.js 22.22.2 と curl 8.5.0 で取得したものです。ヘッダは説明に関係するものだけを抜き出しています。
1. 初回アクセス:Set-Cookie でセッションIDが発行される
curl -v http://127.0.0.1:7651/
> GET / HTTP/1.1
> Host: 127.0.0.1:7651
< HTTP/1.1 200 OK
< Set-Cookie: sid=6aVHgxiUqSBElh6tkdtaaGOOuXQxMXX7WAy-VBs415E; Path=/; HttpOnly; SameSite=Lax; Max-Age=1800
visits=1 user=(未ログイン)
> で始まる行がリクエスト、< で始まる行がレスポンスです。リクエストに Cookie ヘッダはなく、レスポンスの Set-Cookie ヘッダでセッションIDが渡されています。 sid=... が名前と値、その後ろの Path=/; HttpOnly; ... が属性です。
2. 2回目のアクセス:Cookie ヘッダでIDが戻ってくる
ブラウザは受け取った Cookie を自動で送りますが、curl は -b で明示します。
curl -v -b "sid=6aVHgxiUqSBElh6tkdtaaGOOuXQxMXX7WAy-VBs415E" http://127.0.0.1:7651/
> GET / HTTP/1.1
> Host: 127.0.0.1:7651
> Cookie: sid=6aVHgxiUqSBElh6tkdtaaGOOuXQxMXX7WAy-VBs415E
< HTTP/1.1 200 OK
visits=2 user=(未ログイン)
リクエストに Cookie ヘッダが付き、サーバーは同じ利用者だと判断して visits=2 を返しました。Cookie ヘッダで送られるのは「名前=値」だけで、Path や HttpOnly などの属性は送られません。また、すでにIDを持っているので、レスポンスに Set-Cookie はありません。
3. ログイン:セッションIDが新しくなる
curl -v -b "sid=6aVHgxiUqSBElh6tkdtaaGOOuXQxMXX7WAy-VBs415E" \
-d 'user=alice&pw=pass1234' http://127.0.0.1:7651/login
> POST /login HTTP/1.1
> Host: 127.0.0.1:7651
> Cookie: sid=6aVHgxiUqSBElh6tkdtaaGOOuXQxMXX7WAy-VBs415E
> Content-Length: 22
> Content-Type: application/x-www-form-urlencoded
< HTTP/1.1 200 OK
< Set-Cookie: sid=O6hUK2b1EG4no7qYHozsqYmECT7GsKWkt0SH9cSTumY; Path=/; HttpOnly; SameSite=Lax; Max-Age=1800
login ok: alice
ログインに成功すると、新しいセッションID(O6hU...)が Set-Cookie で渡されました。 ブラウザは同じ名前の Cookie を上書きします。
4. 照合:新しいIDは通り、前のIDと作ったIDは通らない
# ログイン前のID
curl -w '%{http_code}\n' -b "sid=6aVHgxiUqSBElh6tkdtaaGOOuXQxMXX7WAy-VBs415E" http://127.0.0.1:7651/me
# ログイン後のID
curl -w '%{http_code}\n' -b "sid=O6hUK2b1EG4no7qYHozsqYmECT7GsKWkt0SH9cSTumY" http://127.0.0.1:7651/me
# 適当に作ったID
curl -w '%{http_code}\n' -b "sid=abc" http://127.0.0.1:7651/me
未ログイン
401
user=alice
200
未ログイン
401
サーバーは、届いたIDが保存先にあるかどうかだけで判断しています。利用者が Cookie の値を書き換えても、サーバーに存在しないIDは未ログイン扱いになります。 これが「セッションのデータは利用者から書き換えられない」という性質の中身です。IDが十分に長いランダムな値であれば、他人のIDを言い当てることは現実的にはできません。
5. ログアウト:サーバーとブラウザの両方から消す
curl -D - -X POST -b "sid=O6hUK2b1EG4no7qYHozsqYmECT7GsKWkt0SH9cSTumY" http://127.0.0.1:7651/logout
curl -w '%{http_code}\n' -b "sid=O6hUK2b1EG4no7qYHozsqYmECT7GsKWkt0SH9cSTumY" http://127.0.0.1:7651/me
Set-Cookie: sid=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0
logout
未ログイン
401
Max-Age=0 の Set-Cookie は「この Cookie を今すぐ削除して」という指示です。ただし、ブラウザ側の削除だけでは、IDを控えていた人が使い続けられます。サーバー側のセッションを消す(sessions.delete)ことが、ログアウトの本体です。 2つ目のコマンドで同じIDを送っても 401 になっていることから、サーバー側で無効になったと分かります。
Cookie の属性一覧
Set-Cookie の 名前=値 の後ろに付ける属性の一覧です。定義と既定の動きは MDN の Set-Cookie の解説で確認しました(確認日 2026-10-02)。
| 属性 | 役割 | 省略したとき | セッションID用の Cookie でのおすすめ |
|---|---|---|---|
Expires |
有効期限を日時で指定 | ブラウザを閉じるまで(セッション Cookie) | Max-Age を使う |
Max-Age |
有効期限を秒数で指定。0 以下は即削除。Expires と両方あれば Max-Age が優先 | 同上 | サーバー側の有効期限に合わせる |
Domain |
送信先のドメイン。指定するとサブドメインにも送られる | 発行したホストだけに送る(サブドメインには送らない) | 省略する(範囲がいちばん狭い) |
Path |
送信先のパス。指定したパスとその配下に送る | リクエストした URL のパス部分 | / |
Secure |
HTTPS の通信でだけ送る | HTTP でも送る | 付ける(localhost は例外的に HTTP でも可) |
HttpOnly |
JavaScript(document.cookie)から読めなくする | JavaScript から読める | 付ける |
SameSite |
別サイトからのリクエストに付けるかどうか(Strict / Lax / None) | 多くのブラウザで Lax 相当 | Lax または Strict |
Partitioned |
埋め込まれた先のサイトごとに Cookie を分けて保存する(CHIPS)。Secure が必須 | 分けない | 他サイトに埋め込む部品でだけ使う |
セッションID用の Cookie の、おすすめの形はこうなります。
Set-Cookie: __Host-sid=ランダムな値; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=1800
名前の先頭の __Host- は「Cookie 名の接頭辞」と呼ばれる仕組みで、Secure 付き・Path=/・Domain なしの条件を満たさない Set-Cookie をブラウザが受け付けなくなります。サブドメインなど別の場所から同じ名前の Cookie を上書きされることを防げます。条件がゆるい __Secure-(Secure 付きであること)もあります。
属性について、つまずきやすい点を補足します。
- 「セッション Cookie」という言葉は2つの意味で使われます。 1つは「Expires も Max-Age もない、ブラウザを閉じると消える Cookie」、もう1つは「セッションIDを入れた Cookie」です。文脈でどちらかを見分けます
- Domain を指定すると、範囲が広がります。
Domain=example.comと書くとshop.example.comやblog.example.comにも送られます。省略するほうが範囲は狭くなります - Path はアクセス制限の仕組みではありません。 同じサイトの別のパスのページからも、工夫すれば読み取れるため、守りの手段としては HttpOnly や Secure を使います
- SameSite=None は Secure とセットで指定します。 SameSite の値ごとの送信の有無は「CSRF対策とSameSite」の記事で実測しているので、ここでは概要に留めます
- 有効期限には上限があります。 長い期限を指定しても、ブラウザ側で一定の日数(Chrome では約400日)に丸められます(一般に知られた仕様で、今回は未実行)
HttpOnly を付けた Cookie は JavaScript から読めない
HttpOnly の効果を、Chromium 141 で確かめます。先ほどのサーバーの /page は、次の2つの Cookie を発行し、ページ内の JavaScript で document.cookie を表示します。
Set-Cookie: theme=dark; Path=/
Set-Cookie: sid2=(ランダムな値); Path=/; HttpOnly
Playwright で開いて調べた結果です。
document.cookie : theme=dark
ブラウザの保存内容: theme httpOnly= False sameSite= Lax expires= -1
ブラウザの保存内容: sid2 httpOnly= True sameSite= Lax expires= -1
fetch('/') : visits=1 user=(未ログイン)
JSで上書き後: [('sid2', 'v8uWQ3Xnc', True)]
| 確認したこと | 結果 |
|---|---|
document.cookie に見える Cookie |
theme=dark だけ。HttpOnly の sid2 は出てこない |
| ブラウザには保存されているか | 2つとも保存されている |
JavaScript の fetch() で送られるか |
HttpOnly の Cookie も自動で送られる |
JavaScript から document.cookie = 'sid2=overwrite' で上書きできるか |
値は変わらず、HttpOnly のまま |
| SameSite を省略した Cookie | Chromium では Lax として扱われた |
| Expires / Max-Age を省略した Cookie | expires が -1(ブラウザを閉じるまで) |
HttpOnly は「JavaScript から読めない・書き換えられないが、通信には付く」属性です。 ページに意図しないスクリプトが入り込んだ場合でも、セッションIDを読み出されにくくなります。セッションID用の Cookie には必ず付けておきたい属性です。
セッションの保存先と、フレームワークの既定の Cookie 名
保存先の比較
セッションのデータをサーバーのどこに置くかは、構成によって選びます。
| 保存先 | 特徴 | 向いている場面 | 気をつける点 |
|---|---|---|---|
| メモリ(プロセス内) | 設定不要でいちばん速い | 学習、開発環境 | 再起動で消える。サーバーが複数台だと共有されない |
| ファイル | サーバー1台で手軽。再起動しても残る | 小規模なサイト(PHP の既定) | 複数台では共有の工夫が必要 |
| Redis などのメモリ型データストア | 速く、複数台で共有でき、有効期限を自動で扱える | サーバーが複数台の構成 | 別のサーバーの運用が加わる |
| データベース(RDB) | 既存の DB を使える。調査や一括での無効化がしやすい | すでに DB があり、アクセス数が中程度まで | アクセスのたびに読み書きが発生する |
| 署名付き Cookie(Cookie セッション) | サーバー側の保存先が不要 | 保存したい情報が少ない場合 | 約 4KB の上限。サーバー側から個別に無効化しにくい |
サーバーが2台以上あるときは、どのサーバーにリクエストが届いても同じセッションを引き当てられるよう、Redis やデータベースなど全サーバーから見える場所に保存するのが基本です。
PHP・Express・Spring の既定値
主要な3つの環境の既定値です。PHP と Express は今回実行して確かめ、Spring は一般に知られた仕様として記載しています(未実行)。
| 環境 | 既定の Cookie 名 | 既定の保存先 | 確認方法 |
|---|---|---|---|
PHP(session_start()) |
PHPSESSID |
ファイル | PHP 8.4.21 で実行 |
| Express(express-session) | connect.sid |
メモリ(MemoryStore。開発用) | express 5.2.1 / express-session 1.19.0 で実行 |
| Spring Boot(組み込み Tomcat) | JSESSIONID |
メモリ | 未実行(一般に知られた仕様) |
| Spring Session(Redis などに保存する追加モジュール) | SESSION |
Redis・JDBC など | 未実行(一般に知られた仕様) |
PHP 8.4 での実行結果です。
<?php
session_start();
$_SESSION['visits'] = ($_SESSION['visits'] ?? 0) + 1;
if (isset($_GET['login'])) { session_regenerate_id(true); $_SESSION['user'] = 'alice'; }
echo session_name(), ' visits=', $_SESSION['visits'], ' save=', ini_get('session.save_handler'), "\n";
Set-Cookie: PHPSESSID=2c827a7a45b56d8b5ce88a9ea85bd3a5; path=/
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
PHPSESSID visits=1 save=files
?login=1 を付けてアクセスすると、session_regenerate_id(true) によってIDが変わりました。
Set-Cookie: PHPSESSID=9db63bc21e1b2a4e7b0f812d224e72f1; path=/
PHPSESSID visits=2 save=files
保存先のファイル(この環境では /var/lib/php/sessions/sess_(セッションID))の中身は次のとおりで、データがサーバー側にあることが分かります。
visits|i:2;user|s:5:"alice";
ここで目を向けたいのは、PHP の既定の Set-Cookie には HttpOnly・Secure・SameSite が付いていないことです。この環境の設定値は session.cookie_httponly が空、session.cookie_samesite が空、session.use_strict_mode が 0、session.gc_maxlifetime が 1440(秒)でした。php.ini か session_start() の直前で、次のように指定しておきます。
<?php
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'secure' => true,
'httponly' => true,
'samesite' => 'Lax',
]);
ini_set('session.use_strict_mode', '1'); // サーバーが発行していないIDを受け付けない
session_start();
Express(express-session)での実行結果です。
// ex.mjs 実行: npm i express express-session && node ex.mjs
import express from 'express';
import session from 'express-session';
const app = express();
app.use(session({ secret: 'change-me', resave: false, saveUninitialized: false }));
app.get('/', (req, res) => {
req.session.visits = (req.session.visits ?? 0) + 1;
res.send(`visits=${req.session.visits}\n`);
});
app.get('/login', (req, res) =>
req.session.regenerate(() => { req.session.user = 'alice'; res.send('ok\n'); }));
app.listen(7653, '127.0.0.1');
Set-Cookie: connect.sid=s%3AHguUCa955mj8xKeP6tMYq7f43jKidSCJ.abUcRJEN8Ps4Df%2Bab%2BO3FzHP5eP9yKnBLraPbmbCbKc; Path=/; HttpOnly
visits=1
/login にアクセスすると、req.session.regenerate() によって別のIDが発行されました。
Set-Cookie: connect.sid=s%3A60K12JrKlajnbT4onmbUZtkIAjKbNW7d.qgl6SIgIFk%2Fi5r74CFg54yVeWcv%2FEmW5F9bnAn%2BeLAE; Path=/; HttpOnly
ok
express-session の Cookie の値は s:(セッションID).(署名) という形(%3A は : を URL 用に変換したもの)で、secret を使った署名が付いています。IDの部分を書き換えると署名が合わなくなり、サーバーが受け付けません。 既定で HttpOnly は付きますが、Secure と SameSite は付かないので、cookie: { secure: true, sameSite: 'lax' } を追加します。既定の保存先の MemoryStore は開発用なので、公開するサービスでは Redis などのストアを指定します。
既定の Cookie 名(PHPSESSID、connect.sid、JSESSIONID)は、そのままだと使っている技術が外から分かります。設定で sid のような一般的な名前に変えておくと、余計な手がかりを減らせます。
ログイン時にセッションIDを作り直す(セッション固定への備え)
セッション固定とは
セッション固定は、第三者があらかじめ用意したセッションIDを、利用者のブラウザに使わせることで成り立つ手口です。利用者がそのIDのままログインすると、「第三者が知っているID」がログイン済みの状態になり、第三者も同じIDでログイン後のページを開けてしまいます。
守る側の対策は明快で、ログインに成功した時点で、それまでのIDを捨てて新しいIDを発行することです。こうすると、事前に知られていたIDはログイン後には無効になります。
作り直さない場合を実行で確認
先ほどのサーバーを、IDを作り直さないモードで起動して確かめます。
node server.mjs --no-regenerate
# (1) 第三者が先にアクセスしてセッションIDを得る
curl -v http://127.0.0.1:7651/
# → Set-Cookie: sid=cF24dpsOwdANcccdYO7EKH_dQR21A4hU_kQqJpH6ccc
# (2) 利用者が、そのIDのままログインする
curl -D - -b "sid=cF24dpsOwdANcccdYO7EKH_dQR21A4hU_kQqJpH6ccc" \
-d 'user=alice&pw=pass1234' http://127.0.0.1:7651/login
# (3) 第三者が、最初に得たIDで /me を開く
curl -w '%{http_code}\n' -b "sid=cF24dpsOwdANcccdYO7EKH_dQR21A4hU_kQqJpH6ccc" http://127.0.0.1:7651/me
(2) の結果:
login ok: alice
(Set-Cookie は出ない=IDが変わっていない)
(3) の結果:
user=alice
200
IDがログインの前後で同じなので、(1) でIDを知っていた側が、パスワードを知らないまま alice さんのページを開けました。
作り直す場合
通常のモード(node server.mjs)では、前の節の「3. ログイン」「4. 照合」で見たとおりです。
| 手順 | 作り直さない場合 | 作り直す場合 |
|---|---|---|
| ログイン時のレスポンス | Set-Cookie なし(IDは同じ) | 新しいIDの Set-Cookie |
| ログイン前のIDで /me | 200(alice として開ける) | 401(未ログイン) |
| ログイン後のIDで /me | 200 | 200 |
ログイン成功時にIDを作り直すと、ログイン前のIDは 401 になります。 各環境での書き方は次のとおりです。
| 環境 | ID を作り直す書き方 |
|---|---|
| PHP | session_regenerate_id(true); |
| Express(express-session) | req.session.regenerate(callback) |
| Spring Security | 既定でログイン時にIDが変更される(未実行・一般に知られた仕様) |
| Jakarta Servlet | request.changeSessionId();(未実行・一般に知られた仕様) |
権限が変わる場面(ログイン、管理者への切り替えなど)ではIDを作り直す、と覚えておくと応用が利きます。あわせて、セッションIDを URL に含めない(Cookie だけで運ぶ)、サーバーが発行していないIDを受け付けない(PHP の session.use_strict_mode)ことも、同じ目的の対策です。
セッションの寿命は2種類決める
セッションの有効期限には、2つの考え方があります。
| 種類 | 意味 | 例 |
|---|---|---|
| アイドルタイムアウト | 最後のアクセスからの経過時間で切る | 30分操作がなければログアウト |
| 絶対タイムアウト | ログインからの経過時間で切る | 操作を続けていても12時間で再ログイン |
Cookie の Max-Age は「ブラウザがいつまで Cookie を持つか」で、サーバー側のセッションの期限とは別のものです。期限の判定はサーバー側で行い、Cookie の期限はそれに合わせる、という関係にしておくと、Cookie を書き換えて期限を延ばすことはできません。
セッションと JWT の比較
ログイン状態の持ち方には、セッションのほかに JWT(JSON Web Token)を使う方式もあります。JWT は、利用者の情報と有効期限を入れて署名したトークンで、サーバーは署名を検証するだけで内容を信頼できます。
| 観点 | セッション(サーバー側に状態) | JWT(トークン側に状態) |
|---|---|---|
| ブラウザが持つもの | 意味を持たないランダムなID | 利用者ID・期限などを含む署名付きトークン |
| サーバーの保存先 | 必要(メモリ・Redis など) | 基本は不要(検証用の鍵だけ) |
| リクエストごとの処理 | 保存先を検索する | 署名を検証する |
| 即時の無効化(強制ログアウト) | 保存先から消せばすぐ無効 | 期限まで有効。無効化には失効リストなどの追加の仕組みが必要 |
| 大きさ | 数十バイト | 数百バイト以上 |
| 複数サーバー・複数サービス | 保存先の共有が必要 | 鍵を共有すれば各サービスで検証できる |
| 向いている場面 | ブラウザから使う Web アプリ全般 | API 同士の連携、複数サービスにまたがる認証 |
ブラウザから使う一般的な Web アプリでは、セッション方式が扱いやすい選択です。 すぐにログアウトさせられること、ブラウザ側に意味のある情報を置かないことが利点になります。JWT の仕組みや署名の検証は「JWTとは」、失効の考え方は「JWTでログアウトを実装する方法」の記事で詳しく扱っています。
なお、JWT を Cookie に入れて運ぶこともできます。「Cookie か JWT か」ではなく、「状態をサーバーに置くか、トークンに置くか」と「何で運ぶか(Cookie か Authorization ヘッダか)」は別々に選べると考えると整理しやすくなります。
Cookie と localStorage の違い
ブラウザにデータを保存する方法には、Cookie のほかに localStorage もあります。
| 観点 | Cookie | localStorage |
|---|---|---|
| サーバーへの送信 | リクエストのたびに自動で送られる | 送られない(JavaScript で取り出して送る) |
| JavaScript からの読み取り | HttpOnly を付ければ読めない | 常に読める |
| 容量の目安 | 1つあたり約 4KB | オリジンごとに数 MB |
| 有効期限 | Expires / Max-Age で指定 | 消すまで残る |
| 向いている用途 | セッションID | 表示設定、下書きなど、サーバーに毎回送らなくてよいデータ |
セッションIDのように「サーバーに毎回届けたい・JavaScript からは触れさせたくない」値は、HttpOnly 付きの Cookie が合っています。 容量の実測やタブ間の共有など、localStorage と sessionStorage の詳しい比較は「localStorage・sessionStorage・Cookieの違い」の記事にまとめています。
名前が似ている sessionStorage は、タブを閉じるまでブラウザにデータを置く仕組みで、この記事で扱ったサーバー側の「セッション」とは別のものです。
どれを使うかの判断表
| 保存したいもの | おすすめ | 理由 |
|---|---|---|
| ログイン状態 | セッション + HttpOnly の Cookie | 利用者が書き換えられず、すぐ無効化できる |
| カートの中身(ログイン前から保持) | セッション(またはサーバーの DB) | 価格や数量をサーバー側で管理できる |
| 権限・会員ランク | セッション(サーバー側) | ブラウザ側に置くと書き換えられる |
| 表示テーマ・言語の設定 | Cookie または localStorage | 書き換えられても影響が小さい。サーバーで使うなら Cookie |
| 入力途中の下書き | localStorage | サーバーに毎回送る必要がない |
| 外部の API を呼ぶためのトークン | JWT など(サーバー側で保持) | 仕様に合わせる |
判断の軸は1つです。「利用者に書き換えられると困る情報」はサーバー側(セッション)に置き、ブラウザにはIDだけを持たせます。
よくあるつまずきと直し方
| 症状 | 原因 | 直し方 |
|---|---|---|
| ログインしても次のページで未ログインになる | Secure 付きの Cookie を HTTP のページで使っている | 開発環境も HTTPS にするか、開発時だけ Secure を外す(localhost は例外的に動く) |
| サブドメインに移動するとログアウトされる | Domain を省略しているので、発行したホストだけに送られている | 共有したい範囲を確認し、必要なら Domain を指定する |
| サーバーを再起動するとログアウトされる | セッションをメモリに保存している | ファイル・Redis・DB に保存する |
| 複数台構成で、ときどき未ログインになる | サーバーごとに別々のメモリに保存している | 全サーバーから見える保存先(Redis など)に変える |
| fetch() で別オリジンの API に Cookie が付かない | fetch の既定では別オリジンに Cookie を送らない | credentials: 'include' を指定し、サーバー側の CORS 設定も合わせる |
| ログアウトしたのに「戻る」で前の画面が見える | ブラウザがページをキャッシュしている | ログイン後のページに Cache-Control: no-store を付ける |
| Cookie を削除する Set-Cookie が効かない | 発行時と Path・Domain が違う | 発行時と同じ Path・Domain を付けて Max-Age=0 を送る |
| document.cookie にセッションIDが見えない | HttpOnly が付いている | 正しい動作。JavaScript からは読まず、サーバーに判定させる |
セッション管理のチェックリスト
- セッションIDは暗号用の乱数で作り、十分な長さがあるか(フレームワークの標準機能を使えば満たされる)
- セッションID用の Cookie に
HttpOnly・Secure・SameSiteを付けたか - Domain は省略し、
Path=/にしたか(可能なら__Host-接頭辞) - ログイン成功時にセッションIDを作り直しているか
- ログアウトでサーバー側のセッションを削除しているか
- アイドルタイムアウトと絶対タイムアウトをサーバー側で判定しているか
- 複数台構成で、保存先が共有されているか
- セッションIDを URL やログに出していないか
よくある質問
Q. Cookie を無効にしているブラウザでは、セッションは使えませんか? A. セッションIDを Cookie で運ぶ方式は使えなくなります。URL にIDを付ける方式もありますが、URL は履歴やリファラー、共有時に外へ出やすいため、現在はおすすめされません。ログインが必要な機能では、Cookie を有効にしてもらう案内を出すのが一般的です。
Q. セッションIDはどのくらいの長さが必要ですか? A. 暗号用の乱数で作られた、128 ビット以上(16 バイト以上)の長さが目安とされています。この記事のサンプルは 32 バイト(256 ビット)、PHP 8.4 の既定は 32 文字でした。自分で作るより、フレームワークの標準のセッション機能に任せるのが確実です。
Q. ブラウザを閉じたらログアウトされるのは、Cookie とセッションのどちらの働きですか? A. Cookie の働きです。Expires も Max-Age も指定していない Cookie は、ブラウザを閉じると消えます。IDが消えるので、サーバー側にセッションが残っていても引き当てられなくなります。ただし、ブラウザの「前回のタブを復元する」設定によっては、閉じても Cookie が残る場合があります。確実に切りたいときは、サーバー側のタイムアウトで判定します。
Q. 「ログイン状態を保持する」チェックボックスは、どう実現しますか? A. チェックがあるときだけ、Cookie に長めの Max-Age を付ける方法が基本です。サーバー側のセッションの期限も同じ長さにする必要があります。長く残る Cookie になるため、HttpOnly・Secure・SameSite を付け、パスワード変更などの重要な操作の前にはもう一度パスワードを確認する、という組み合わせにしておくと安心です。
まとめ
- Cookie はブラウザに保存される小さなデータ、セッションはサーバー側に持つ利用者ごとの状態。 セッションIDを Cookie で運ぶ、という組み合わせで使う
- サーバーは Set-Cookie でIDを渡し、ブラウザは Cookie ヘッダで毎回送り返す。 サーバーは届いたIDが保存先にあるかで判断するので、作ったIDは通らない(curl で確認)
- セッションID用の Cookie には HttpOnly・Secure・SameSite を付ける。 HttpOnly の Cookie は document.cookie から読めず、上書きもできない(Chromium 141 で確認)
- ログインに成功したらセッションIDを作り直す。 作り直さないと、事前に知られていたIDでログイン後のページを開けてしまう(実行で確認)
- 既定の Cookie 名は PHP が PHPSESSID、express-session が connect.sid、Spring Boot が JSESSIONID。 既定値のままでは Secure や SameSite が付かない環境があるので設定を確認する
- 保存先は、1台ならファイルやメモリ、複数台なら Redis や DB。 ログアウトではサーバー側のセッションを消す
- 書き換えられると困る情報はサーバー側に、ブラウザにはIDだけ。 表示設定などは Cookie や localStorage、サービス間の連携には JWT と使い分ける
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Cookieとセッションの違いと仕組み|セッションIDの流れを図解」
株式会社RJCでは、Cookie やセッションといった Web の基礎となる仕組みを仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。