Spring Bootで認証を実装しようと調べると、セッション認証・JWT・OAuth2・API Keyと選択肢が多くて、どれを選べばいいのか迷いますよね。

実は認証方式選びは技術の優劣ではなく、クライアントの種類と失効要件 でほぼ決まります。この記事ではフローチャートと比較表で方式を1つに絞り、各方式の最小設定を確認したら、あとは詳細な実装記事へ進めるようにまとめました。

結論:認証方式の選定フローチャート

まず結論からいきましょう。自分のアプリを上から当てはめてみてください。

Q1. Googleなど外部IdPでログインさせたい?
 ├─ Yes → OAuth2ログイン(oauth2Login)
 └─ No ↓
Q2. ユーザーが関与しないマシン間通信?(バッチ・Webhook等)
 ├─ Yes → API Key(本格運用ならOAuth2クライアントクレデンシャル)
 └─ No ↓
Q3. クライアントはThymeleafなどのSSR画面?
 ├─ Yes → セッション認証
 └─ No(SPA・モバイル + API)↓
Q4. 強制ログアウトなど即時失効が最重要?
 ├─ Yes → セッション認証 + Redis共有も検討
 └─ No → JWT認証

迷ったらまずセッション認証で始めるのがおすすめです。Spring Securityのデフォルトに乗れて実装コストが最小ですし、ステートレスが本当に必要になってからJWTへ移行しても遅くありません。

4方式の比較表

フローチャートの分岐理由を表で裏付けておきます。

セッションJWTOAuth2API Key
状態管理ステートフルステートレスIdP依存ステートレス
即時失効◎ 簡単△ 苦手○ IdP側で制御○ キー無効化
スケールアウトセッション共有が必要◎ 容易◎ 容易◎ 容易
CSRF対策必須不要な構成が多いログイン側は必須不要
実装コスト中〜高
向く構成SSR画面SPA・モバイルAPI外部IdP連携マシン間通信

JWTの「即時失効が苦手」と、セッションの「CSRF対策必須」は、後述する落とし穴に直結するポイントなので覚えておいてください。

セッション認証:SSR画面アプリの第一候補

Thymeleafなどでサーバーサイドレンダリングする画面アプリなら、セッション認証が第一候補です。ログインすると JSESSIONID がCookieに入り、サーバー側でセッションを管理する昔ながらの方式ですね。

@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/login", "/css/**").permitAll()
            .anyRequest().authenticated())
        .formLogin(form -> form.loginPage("/login").permitAll());
    return http.build();
}

これだけで動きます。即時失効(強制ログアウト)はサーバー側のセッションを消すだけなので、退会や権限剥奪を即座に反映したい業務システムに向いています。

「セッションはスケールしないから古い」と言われることがありますが、これは誤解です。複数インスタンス構成でも Spring Session + Redisでセッションを共有 すれば解決します。

ユーザー情報をDBから引く実装は UserDetailsServiceによるDB認証の記事 を、認証の仕組み自体を最小構成で試したい場合は Basic認証の記事 をどうぞ。

JWT認証:SPA・モバイル向けAPIの定番

ReactなどのSPAやモバイルアプリからREST APIを叩く構成なら、JWTが定番です。サーバーに状態を持たないので、インスタンスを何台に増やしてもそのまま動きます。

@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .csrf(csrf -> csrf.disable())
        .sessionManagement(session ->
            session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/api/auth/**").permitAll()
            .anyRequest().authenticated())
        // JWT検証フィルタを差し込む(実装は詳細記事へ)
        .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
    return http.build();
}

フィルタの中身(トークン生成・検証)は JWT認証の実装記事 で全文解説しているので、ここでは骨格だけにとどめます。

注意したいのは弱点の 即時失効 です。発行済みJWTは有効期限まで生き続けるので、アクセストークンを短命(5〜15分)にして リフレッシュトークンのローテーション で補う構成が実質必須になります。

もうひとつ。同一ドメインで完結するSSRアプリに「モダンだから」とJWTを入れるのはアンチパターンです。セッションで得られる即時失効を捨ててリスクだけ増えるので、やめておきましょう。

OAuth2:外部IdP連携は2つの立場を区別する

OAuth2は「1つの方式」というより2つの立場があり、ここを混同すると選定を間違えます。

1つ目は ログインさせる側(クライアント) です。Googleアカウントでログインさせたい、といったケースですね。パスワード管理を外部に委譲できるのが最大のメリットで、設定はプロパティと oauth2Login() だけで済みます。

@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
        .oauth2Login(Customizer.withDefaults());
    return http.build();
}

手順の全体像は Googleソーシャルログインの記事 にまとめています。

2つ目は トークンを検証する側(リソースサーバー) です。Auth0やKeycloakなどのIdPが発行したJWTを、API側で受け取って検証する立場ですね。

@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
        .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));
    return http.build();
}

自前でJWTを発行しない分、実装はかなり薄くなります。判断基準はシンプルで、すでにIdPがあるならリソースサーバー、無いなら自前JWT発行 が素直です。詳細は リソースサーバーでのJWT検証記事 をどうぞ。

API Key認証:マシン間通信の割り切った選択

社内バッチやWebhook受信、サーバー間連携など「ユーザーの概念がない」通信なら、API Keyで割り切るのも十分ありです。ヘッダーを検証する自作フィルタを差し込むだけで実現できます。

public class ApiKeyFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest req,
            HttpServletResponse res, FilterChain chain)
            throws ServletException, IOException {
        if (!expectedKey.equals(req.getHeader("X-API-KEY"))) {
            res.sendError(HttpServletResponse.SC_UNAUTHORIZED);
            return;
        }
        chain.doFilter(req, res);
    }
}

ただし限界もはっきりしています。キーのローテーションや利用者ごとの権限分離、監査が弱いので、連携先が増えてきたらOAuth2のクライアントクレデンシャルフローへの移行を検討しましょう。

また、エンドユーザー向けの認証にAPI Keyを使うのはやめてください。ユーザーごとの失効管理も認証の証跡も持てず、漏洩したときの影響範囲を制御できません。

選定時にハマりやすい3つの落とし穴

1. ステートレス構成でのCSRF設定。セッション認証ではCSRF対策が必須ですが、AuthorizationヘッダーでJWTを送る構成ではCookieを使わないため csrf.disable() が定石です。ただしJWTをCookieに入れるなら話が変わります。判断基準は CSRF対策の記事 で整理しています。

2. JWTの即時失効問題を後から知る。退会や強制ログアウト要件があるのにJWTを選ぶと、結局ブラックリストなどサーバー側の状態が必要になり、ステートレスの利点が消えます。失効要件は選定の最初に確認しましょう。

3. 認証と認可の混同。この記事で選ぶのは「誰か」を確認する認証です。「何ができるか」のロール制御は方式に関わらず @PreAuthorize などで共通に実現できるので、メソッドセキュリティの記事 を参照してください。

併用パターン:1つに絞らなくていいケース

実務では「管理画面はセッション、公開APIはJWT」のような併用が普通にあります。SecurityFilterChain@Order 付きで複数定義し、securityMatcher でパスを分ければ実現できます。

@Bean
@Order(1)
SecurityFilterChain apiChain(HttpSecurity http) throws Exception {
    http.securityMatcher("/api/**")
        .csrf(csrf -> csrf.disable())
        .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
        .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
        .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
    return http.build();
}

@Bean
@Order(2)
SecurityFilterChain webChain(HttpSecurity http) throws Exception {
    http.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
        .formLogin(Customizer.withDefaults());
    return http.build();
}

「OAuth2ソーシャルログインで認証して、自前JWTを発行してAPIアクセスに使う」というSPAの定番構成も、この延長線上です。併用時はチェーンの適用順序とマッチ条件の設計が肝になるので、@Order の小さいチェーンから評価される点だけ押さえておきましょう。

まとめ:方式を決めたら読む記事マップ

認証方式は クライアント種別と失効要件で決める。これが本記事の結論です。方式が決まったら、次の記事マップで実装に進んでください。

まだ迷っているなら、最小構成のセッション認証から始めて段階的に育てていくのが現実的です。要件が固まってから方式を乗り換えても、Spring Securityなら設定の骨格は共通なので無駄になりませんよ。