【Spring Boot 3 / Security 6】JWTで守るAPIサーバー最小構成:SecurityFilterChain・CORS・CSRF・401/403まで全部つなげる
JWTでAPIを守るとき、最小構成の考え方はシンプルです
- 認証方式:
Authorization: Bearer <JWT>を検証する(Resource Server) - セッション:使わない(STATELESS)
- CSRF:基本的に「ブラウザフォーム+セッション」向けの仕組みなので、ステートレスAPIではOFFにすることが多い(ただし要件次第)
- CORS:SPA/フロントから呼ぶなら必須(preflightの
OPTIONSに注意)
まずこれだけ入れる(依存関係)
JWTを検証するResource Serverとして動かすなら、少なくとも以下が必要です(BootならstarterでOK)
spring-boot-starter-securityspring-boot-starter-oauth2-resource-server
この構成はSpring Security側のユースケースとして明記されています
application.yml:issuer-uri(基本)+ audiences(必要なら)
JWT検証の入り口はissuer-uriが定番です
Resource Serverは起動時にメタデータからJWKs URL等を解決し、署名検証に必要な鍵情報を取得します
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://idp.example.com
# audiences: https://my-resource-server.example.com # aud検証したい場合audiencesでaudクレームを検証する設定も用意されています
【コピペOK】SecurityFilterChain:JWT + ステートレス + CORS + CSRF(最小)
Spring Security 6(Boot 3)ではSecurityFilterChainをBean定義して組み立てるのが基本です
package com.example.security;
import java.util.List;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// 1) API中心ならステートレス
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// 2) ブラウザから呼ぶならCORSは必須(下にCorsConfigurationSourceを用意)
.cors(Customizer.withDefaults())
// 3) ステートレスAPIではCSRFを無効化することが多い(要件により)
.csrf(csrf -> csrf.disable())
// 4) 認可ルール
.authorizeHttpRequests(auth -> auth
// ヘルスチェックやSwagger等は公開しがち(要件に合わせて調整)
.requestMatchers("/actuator/health", "/swagger-ui/**", "/v3/api-docs/**").permitAll()
// preflight(OPTIONS)は通す(CORSで詰まる人が多い)
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
// それ以外はJWT必須
.anyRequest().authenticated()
)
// 5) JWT(Bearer Token)を検証して認証する(Resource Server)
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));
return http.build();
}
@Bean
CorsConfigurationSource corsConfigurationSource() {
var config = new CorsConfiguration();
// 例:ローカル開発のフロント
config.setAllowedOrigins(List.of("http://localhost:3000"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"));
config.setAllowedHeaders(List.of("*"));
config.setAllowCredentials(true);
var source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
}401/403/CORSで詰まったら、まずここを疑う(原因トップ4)
401(Unauthorized):そもそもJWTが届いてない
フロントがAuthorizationヘッダを送っていない/送れていない
- APIクライアント(Postman/curl)では通るのに、ブラウザからだけ401 → CORSでヘッダが落ちてる可能性
チェック:ブラウザのNetworkタブで Authorization: Bearer ... が載っているか確認
401:issuer-uriやJWKs解決で失敗してる
issuer-uriを指定すると、Resource Serverはメタデータを辿ってJWKs URLなどを決定し、検証戦略を構成します
ここがズレると起動時/リクエスト時に検証が失敗します
対処:
- issuerのURL(末尾スラッシュなど)をIDP側の発行値に合わせる
- IDPのメタデータ公開(.well-known)が有効か確認
403(Forbidden):CSRFが効いている(APIなのに)
GETは通るがPOST/PUT/DELETEで403
ステートレスAPIなら csrf.disable() が必要になることが多いです(ただし要件次第)
CORS:preflightのOPTIONSが弾かれている
ブラウザからだけ失敗(コンソールにCORSエラー)
対処:
http.cors()を有効にするOPTIONS /**をpermitAll()しておくallowedOrigins/allowedHeadersをフロントに合わせる
役割(ROLE)や権限(SCOPE)で認可したい場合の最短例
JWTを使うと「このAPIはログイン必須」だけでなく、「管理者だけ」や「特定スコープだけ」にしたくなります
例(スコープに応じてアクセス制御したいイメージ):
.authorizeHttpRequests(auth -> auth
.requestMatchers("/admin/**").hasAuthority("SCOPE_admin")
.requestMatchers("/api/**").hasAnyAuthority("SCOPE_read", "SCOPE_write")
.anyRequest().authenticated()
)※ どのクレームを GrantedAuthority に変換するかはIDPやトークン設計次第です。まずは JWTが認証されてPrincipalが作れるところまでを最短で通し、次に変換(Converter)へ進むのが事故りにくいです
(JWT Resource Serverの基本フローは公式が最も正確です)
開発中だけ「認証なし」にしたい(localプロファイル切替)
ローカルで詰まり続けると開発速度が落ちます。プロファイルでSecurityFilterChainを切り替えるのが安全です
@Bean
@Profile("local")
SecurityFilterChain localSecurity(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth.anyRequest().permitAll());
return http.build();
}依存を入れるだけでデフォルトのセキュリティが有効になり、デフォルトユーザーやパスワードの挙動もあります(開発向け)
ただし、JWT(Resource Server)で運用するなら、この記事のように 自分のアプリ要件に合わせてSecurityFilterChainを明示していくのが前提になります
ぜひ、ご参考くださいっ!
是非フォローしてください
最新の情報をお伝えします
