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つを組み合わせます。

  1. サーバーが利用者ごとに番号札(セッションID)を発行し、状態をサーバー側に保存する(セッション)
  2. 番号札をブラウザに持たせ、リクエストのたびに見せてもらう(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期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?