認証まわりの設計は、一度リリースすると後から方式を変えるのが非常に難しい領域です。「とりあえずJWTで」と流行に乗って導入した結果、ログアウトやアカウント停止を即時反映できずに苦労する——という相談は今でもよく受けます。
この記事では、セッション方式とJWT方式の本質的な違い、トークンの保存場所、有効期限とリフレッシュの設計、そして認可ロジックをどこに置くかという4つの論点を整理します。
セッション方式とJWT方式の違いは「状態を持つ場所」
両者の違いは、認証状態をサーバー側に持つか、トークン自体に埋め込むかです。
- セッション方式: サーバー側(Redis等)に状態を保存し、クライアントにはセッションIDだけ渡す。即時無効化が容易で、漏洩時もサーバー側で破棄できる。毎リクエストでストアへの参照が発生
- JWT方式: 署名付きトークンに情報を埋め込み、サーバーは署名検証だけで認証できる。ストア参照が不要でサービス間連携に向くが、発行済みトークンを即時無効化できないのが本質的な弱点
実務の目安として、単一のWebアプリならセッション方式で十分なことが多く、複数サービスで認証を共有する構成や外部APIの認可ではJWT(OAuth 2.0 / OpenID Connect)が適します。「JWTのほうがモダン」という理由だけで選ぶのは避けるべきです。無効化の要件(強制ログアウト、アカウント凍結の即時反映)があるなら、JWTでもブロックリスト等の状態管理が結局必要になり、セッション方式との差が縮まります。
トークンの保存場所——localStorageは避ける
ブラウザでのトークン保存場所は、XSSへの耐性で選びます。
- localStorage: JavaScriptから読めるため、XSSが1箇所あればトークンごと盗まれる。長期トークンの置き場所には不向き
- Cookie(HttpOnly + Secure + SameSite): スクリプトから読めず、XSSでの窃取を防げる。CSRFはSameSite属性とCSRFトークンで対策する
SPAでもリフレッシュトークンはHttpOnly Cookieに置き、アクセストークンはメモリ上でのみ保持するのが現在の定石です。「SPAだからlocalStorage」と機械的に決めているコードを見かけたら、見直しをおすすめします。
有効期限とリフレッシュトークンの設計
「即時無効化できない」JWTの弱点は、有効期限を短くすることで緩和します。基本形は次の2段構成です。
- アクセストークン: 短命(15分〜1時間程度)。漏洩しても被害時間を限定する
- リフレッシュトークン: 長命(数日〜数週間)。サーバー側で管理し、失効させられるようにしておく
リフレッシュトークンをサーバー側で管理していれば、強制ログアウトは「リフレッシュトークンの失効」で実現でき、影響はアクセストークンの残り寿命だけに抑えられます。さらにリフレッシュのたびにトークンを使い捨てで再発行するローテーション方式にすると、盗まれた古いトークンの再利用を検知できます。
認可の実装場所を一箇所に集める
認証(誰か)より事故が多いのは認可(何をしてよいか)です。特に「他人のリソースIDを指定したら見えてしまう」IDOR系の脆弱性は、認可チェックが各コントローラに散らばっていて漏れることで生まれます。
- ロール単位の粗い制御(管理者のみ等)は、ミドルウェアやフィルタ層に集約する
- リソース単位の所有チェックは、コントローラではなくサービス層・リポジトリ層で強制する(クエリ条件に必ず所有者IDを含める等)
- フロントエンドでの出し分けはUXのためのもので、認可はサーバー側で必ず再検証する
-- 「取得してからチェック」ではなく、クエリ自体に認可を織り込む
SELECT * FROM documents
WHERE id = ? AND owner_id = ?; -- 0件なら404を返す
まとめ
認証・認可の設計は、方式の新しさではなく要件——特に「無効化をどれだけ即時に反映したいか」——から逆算するのが失敗しないコツです。単一アプリならセッション方式を第一候補に、JWTを使うなら短命アクセストークン+管理されたリフレッシュトークンの2段構成に。トークンはHttpOnly Cookieを基本とし、認可チェックは層を決めて一箇所に集約する。この原則を押さえておけば、後から取り返しのつかない設計を避けられます。
