要点
-
セッションID方式:サーバがユーザーごとの状態(セッション)を持ち、Cookie(例:
JSESSIONID)でユーザーを識別する。- 特徴:状態あり(stateful)。CSRF対策が必須。フロントは
credentials:'include'が必要。
- 特徴:状態あり(stateful)。CSRF対策が必須。フロントは
-
JWT方式:サーバは状態を持たず、 署名付きトークン(JWT) を毎リクエストの
Authorization: Bearerで送る。- 特徴:状態なし(stateless)。毎回トークンの署名・期限・発行者を検証する必要がある(=ただ付けるだけではダメ)。
- 「外部IdP(Keycloak/Cognito/Auth0)」=公開鍵(JWK)で検証するRS256が一般的。
- 「自前発行」=共有シークレットで検証するHS256が手軽。
- 403 は権限不足/検証OKだが許可されない、401 は未認証/検証NGのことが多い。
1. まず“何が違うのか”をざっくり図で
1-1. セッションID(Cookie)方式
- サーバ側に「誰がログイン中か」の状態を保存する(メモリ/DB/Redisなど)。
- ブラウザは Cookie を自動送信。CORS + 認証つきのときは
allowCredentials(true)が必要。 - CSRFトークン運用が必要(Cookieは自動送信されるため)。
1-2. JWT(Bearer)方式
-
サーバは状態を持たない(= stateless)。高速・スケールしやすい。
-
トークンは改ざんされていないことを、署名検証で毎回チェックする。
ここで 公開鍵(JWK) or 共有シークレット が必要になる。
2. いつどっちを選ぶ?
| セッションID(Cookie) | JWT(Bearer) | 補足 | |
|---|---|---|---|
| サーバ状態 | あり(セッションストア) | なし(stateless) | サーバ側にメモリ/DBでセッションを保持するか、署名済みトークンで自己完結するかの違い |
| スケール | セッション共有の仕組みが必要(Sticky/Redis等) | 容易(署名検証のみ) | マイクロサービスやマルチサーバ構成ではJWTの方が親和性が高い |
| CSRF | 必要(Cookieは自動送信されるため) | ほぼ不要(Authorizationヘッダは自動送信されない) | JWTでもCookie格納方式にすると再びCSRF対策が必要になる点に注意 |
| CORS |
allowCredentials(true) が絡みがち |
Authorization ヘッダ許可でシンプル |
Cookieを伴うとドメイン・サブドメインの扱いが厄介になりやすい |
| 実装の手軽さ | Spring既定の世界観に乗れば簡単 | 署名・検証・更新(Refresh)が設計必要 | JWTは仕組みを自作するとバグや脆弱性を埋め込みやすいのでライブラリ利用推奨 |
| 失効 | サーバで即時無効化しやすい | TTLまで有効(短命+リフレッシュ設計で補う) | JWTで即時失効を実現する場合はブラックリストやトークンリポジトリが必要 |
| 外部IdP連携 | 可能だがCookie設計が複雑 | 最適(RS256/JWKで検証が定石) | OpenID Connect / OAuth 2.0 の標準フローはJWT前提で設計されている |
3. 実装の違い(Spring Boot + Vueの最小構成)
3-1. フロントエンド共通(Vue/TS)の呼び出し
- セッション方式:
credentials: 'include'を必ず付ける(Cookie送受信用) - JWT方式:
Authorization: Bearer <token>を付ける
// セッション方式
fetch(`${API}/api/userinfo`, {
method: 'GET',
credentials: 'include', // ← 重要
})
// JWT方式
fetch(`${API}/api/userinfo`, {
method: 'GET',
headers: { Authorization: `Bearer ${token}` },
})
3-2. セッションID方式(Spring Security)
SecurityConfig(最小)
@Bean
SecurityFilterChain security(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/login").permitAll()
.requestMatchers(HttpMethod.GET, "/api/userinfo").authenticated()
.anyRequest().authenticated())
.cors(cors -> cors.configurationSource(req -> {
var c = new CorsConfiguration();
c.setAllowedOrigins(List.of("http://localhost:5173"));
c.setAllowedMethods(List.of("GET","POST","PUT","DELETE","OPTIONS"));
c.setAllowedHeaders(List.of("*"));
c.setAllowCredentials(true); // 認証付きCORS
return c;
}))
.formLogin(Customizer.withDefaults()); // ※SPAならRESTの/loginを自作も可
return http.build();
}
ポイント
- Cookieの
SameSite=Lax(デフォルト)なら、http://localhost:5173→http://localhost:8080でも同サイト扱いなのでOK。 -
POST/PUT/DELETEにはCSRFトークンをヘッダ送付(X-XSRF-TOKEN等)。 - サーバは
JSESSIONIDを発行。セッションストア(メモリ/Redis)をどうするかも設計に含める。
3-3. JWT方式:外部IdPの公開鍵で検証(RS256)
ポイント
- API側は秘密鍵を持たず、IdPの 公開鍵(JWKS) で検証=鍵管理の責務が軽い
issuer-uri指定で 署名/iss/exp は自動検証、aud はカスタム検証を足すのが実運用の定石- 権限は
scope/scpやrolesをSCOPE_ROLE_にマッピングしてhasAuthority/hasRoleで保護
SecurityConfig
@Bean
SecurityFilterChain security(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable()) // APIのみなら一旦OFF(必要に応じて調整)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/login").permitAll() // IdPに投げる/自前でトークン発行はシステム構成依存
.requestMatchers(HttpMethod.GET, "/api/userinfo").authenticated()
.anyRequest().authenticated())
.cors(cors -> cors.configurationSource(req -> {
var c = new CorsConfiguration();
c.setAllowedOrigins(List.of("http://localhost:5173"));
c.setAllowedMethods(List.of("GET","POST","PUT","DELETE","OPTIONS"));
c.setAllowedHeaders(List.of("*")); // Authorization を含む
return c;
}))
.oauth2ResourceServer(o -> o.jwt()); // ← これが肝
return http.build();
}
application.properties(どちらか一方)
spring.security.oauth2.resourceserver.jwt.issuer-uri=https://<your-issuer>/
# or
# spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://<issuer>/.well-known/jwks.json
これだけで Spring が IdP の 公開鍵(JWK)を取得し、署名/exp/iss を検証してくれます。
その後、必要なら
roles/scopeをROLE_SCOPE_にマッピングしてhasRole/hasAuthorityを使います。
(権限マッピング例)
@Bean
Converter<Jwt, ? extends AbstractAuthenticationToken> jwtAuthConverter() {
var scopes = new JwtGrantedAuthoritiesConverter();
scopes.setAuthoritiesClaimName("scope"); // IdPに合わせて "scp" などに変更
scopes.setAuthorityPrefix("SCOPE_");
var conv = new JwtAuthenticationConverter();
conv.setJwtGrantedAuthoritiesConverter(jwt -> {
var list = new ArrayList<GrantedAuthority>(scopes.convert(jwt));
Map<String,Object> realm = jwt.getClaim("realm_access");
if (realm != null && realm.get("roles") instanceof Collection<?> roles) {
roles.forEach(r -> list.add(new SimpleGrantedAuthority("ROLE_" + r)));
}
return list;
});
return conv;
}
3-4. JWT方式:自前発行(HS256)
ポイント
- 署名・検証の鍵を自サービスで保持(HS256は共有秘密鍵の厳格管理とローテーションが必須)
- RS256で自前発行も可能だが、その場合は 自前の鍵管理/公開鍵配布(JWKS) を用意する必要あり
- 失効・ロール更新の即時反映をしたい場合はトークン短寿命+リフレッシュやブラックリスト設計が必要
発行(例:JJWT)
String token = Jwts.builder()
.subject(username)
.issuer("authjava")
.claim("roles", List.of("USER"))
.expiration(Date.from(Instant.now().plusSeconds(3600)))
.signWith(Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)), Jwts.SIG.HS256)
.compact();
検証(Resource Server 化)
@Bean
JwtDecoder jwtDecoder() {
var key = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
return NimbusJwtDecoder.withSecretKey(key).build();
}
@Bean
SecurityFilterChain security(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/login").permitAll()
.requestMatchers(HttpMethod.GET, "/api/userinfo").authenticated()
.anyRequest().authenticated())
.oauth2ResourceServer(o -> o.jwt()); // JwtDecoder が使われる
return http.build();
}
フロント
await fetch(`${API}/api/userinfo`, {
headers: { Authorization: `Bearer ${accessToken}` }
})
4. セキュリティ実務の要点
-
トークン保存場所
-
localStorageはXSSに弱い。メモリ保持(リロードで消えるが安全)か、Refresh TokenのみをHttpOnly Cookieに入れて回すパターンが安全寄り。
-
-
トークン寿命
- Access Tokenは短命(5分〜15分)。Refreshで再発行。
-
ログアウト
- セッション方式:サーバで即無効化可。
- JWT:サーバは状態を持たないので、短命化+ブラックリスト/トークンローテーションで対処。
-
CORS
- JWT:
Authorizationヘッダを許可する。 - セッション:
allowCredentials(true)とAccess-Control-Allow-Originの具体値(禁止)をセット。
- JWT:
-
CSRF
- セッション方式は必須(
CookieCsrfTokenRepository等)。 - JWT(Authorizationヘッダ)は基本不要(ただしCookie運用に絡む場合は別途検討)。
- セッション方式は必須(
5. よくあるエラーの見分け方
-
401 Unauthorized:未認証(トークンが無い/期限切れ/署名不正/
iss不一致など)。 -
403 Forbidden:認証はOKだが権限不足(もしくはフィルタで拒否)。
-
デバッグ:
logging.level.org.springframework.security=DEBUG-
No Authentication→ JWT検証が動いていない(oauth2ResourceServer().jwt()/Decoder未設定) -
Authenticated but access is denied→ 権限マッピングのズレ
-
6. どれを採用すべき?
-
外部IdP採用 / 将来マイクロサービス化 / モバイル/他クライアントも想定
→ JWT(RS256, issuer-uri 設定) が王道。 -
単一Webアプリで手早く
→ セッション方式でも十分。 -
モノリスで自前完結
→ JWT(HS256) も選択肢(ただしRefresh/失効設計は忘れずに)。
了解しました。絵文字を使わずに整理した形に直しました。
最小チェックリスト(方式別)
セッション方式(Cookie & セッションストア)
-
フロント
-
credentials: 'include'を付けて fetch しているか
-
-
サーバ設定
-
allowCredentials(true)+ 明示的なAllowedOriginを設定しているか - CSRFトークン送受信を組み込んでいるか(
CookieCsrfTokenRepository等)
-
-
Cookie属性
-
Secure; SameSiteの値を本番環境に合わせて調整しているか(LaxまたはNone+ HTTPS)
-
JWT方式(Bearer トークン)
-
フロント
-
Authorization: Bearer <access_token>を必ず送っているか
-
-
サーバ設定
-
oauth2ResourceServer().jwt()を有効化しているか -
issuer-uriorjwk-set-uri(RS256)/ Secret(HS256)を設定しているか
-
-
トークン検証
-
iss / aud / expを正しく検証しているか - (必要に応じて)
roles/scopeをROLE_SCOPE_にマッピングしているか
-
-
運用設計
- Access Tokenは短命(数分〜十数分)にしているか
- Refresh Token やブラックリストで更新・失効戦略を設けているか
まとめ
-
セッション方式
- 状態あり。サーバにセッションを保持するので即時失効が容易。
- 注意点:スケール時はセッション共有が必要。Cookie利用なのでCSRF対策必須。
-
JWT方式
- 状態なし。サーバは署名検証のみでスケールに強い。
- 注意点:トークン失効は即時反映できないため短命化+リフレッシュ設計が必要。毎回署名検証が必須。
選び方の目安
- 単一アプリ・社内向けならセッション方式がシンプルで安全。
- 外部IdP連携やマイクロサービスを見据えるならJWT方式(RS256)が適切。