本ページはプロモーションが含まれています

Java入門

Spring Security入門|CSRF・JWTまで実装解説

CATEGORY
REV.
DRAWN BY
TOM
SHEETS
8枚
Spring Security(CSRF・JWTまで実装)

トム

SESで約8年、環境を変えられないもどかしさの中にいました。32歳で自社開発企業に転職し、今はClaude Codeと自動化で、仕事も時間も組み直している途中です。
・Javaバックエンドエンジニア(Java歴12年以上)
・資格:基本情報技術者/応用情報技術者/Java Silver/Python3エンジニア認定基礎
詳細なプロフィール

𝕏

Spring Bootでログイン機能を作ろうとして、Spring Securityの設定でいきなりつまずいた経験はないでしょうか。

私も最初は「なぜ全部のURLがログイン画面にリダイレクトされるんだ」と混乱しました。

この記事を読めば、認証・認可の基本概念からSecurityFilterChainの設定、CSRF対策、JWT・OAuth2による認証までが実装コード付きでわかります。

初心者がつまずきやすいエラーの対処法もあわせて紹介します。

この記事でわかること

  • 認証と認可の基本概念とSecurityFilterChainの設定(Spring Security 7対応)
  • ロールベースの認可・パスワードハッシュ化の実装
  • CSRF対策の仕組みと、無効化・一部除外・SPA向けの設定方法
  • JWT・OAuth2によるモダンな認証
  • 初心者がつまずきやすいエラーの対処法
トム
トム

「なぜ全部ログイン画面にリダイレクトされるんだ」と最初は本気で焦りました。

Spring Securityとは?認証と認可の基本

Spring Securityとは、Spring Bootアプリケーションに認証・認可の仕組みを組み込むためのセキュリティフレームワークです。

ログイン処理やアクセス制御を自前で実装しなくても、設定を書くだけでセキュアな仕組みを構築できます。

まず押さえておきたいのが、認証と認可という2つの用語の違いです。

認証と認可の違い
AUTHENTICATION / 認証「あなたは誰?」を確認する例:ID・パスワードでログイン失敗 → 401
AUTHORIZATION / 認可「あなたは何をしてよい?」を確認する例:管理者だけ管理画面に入れる権限なし → 403
NOTE認証が先、認可があと。

認証(Authentication)と認可(Authorization)の違い

認証は「あなたは誰ですか」を確認する処理です。ユーザー名とパスワードを照合し、本人であることを確かめます。

一方の認可は「あなたに何が許可されていますか」を判定する処理です。

認証を通過したユーザーでも、管理者専用ページには認可でアクセスを拒否する、という使い分けをします。

認証と認可を混同すると設計がぶれるため、最初に明確に区別しておくことが大切です。

Spring Bootとの連携でできること

Spring Bootにspring-boot-starter-securityを追加するだけで、フォームログイン・Basic認証・CSRF対策がデフォルトで有効になります。

DBと連携したユーザー管理、ロールベースのアクセス制御、パスワードのハッシュ化、JWT・OAuth2によるAPI認証まで幅広くカバーできます。

実務で必要になるセキュリティ機能をほぼすべて備えている守備範囲の広さが特徴です。

あわせて読む

Spring Securityの導入と基本設定

導入は依存関係を1行追加するだけですが、追加した瞬間にアプリの挙動が大きく変わります。最初にこの挙動を理解しておかないと、後の設定で混乱します。

spring-boot-starter-securityを追加した直後の挙動

pom.xmlまたはbuild.gradleにspring-boot-starter-securityを追加すると、全てのエンドポイントがログイン必須になります。

起動時のログにランダムなパスワードが出力され、ユーザー名userでログインできる状態です。

これはデフォルトの安全側の挙動で、意図的にすべて保護してから必要な箇所だけ開放していく設計思想がここに表れています。

Spring Securityがリクエストを守る流れ
Security Filter Chain
リクエスト
STEP 1認証誰かを確認
STEP 2認可権限を確認
Controller
NOTESpring Securityは、コントローラーの手前に置いたフィルタの連なりでリクエストを検査する。

SecurityFilterChainでURL認可を設定する

URLごとのアクセス制御はSecurityFilterChainという@Beanで定義します。

以下は、トップページと静的リソースは誰でもアクセス可能にし、それ以外は認証を必須にする最小構成です。

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/", "/css/**", "/js/**").permitAll()
            .anyRequest().authenticated()
        )
        .formLogin(Customizer.withDefaults());
    return http.build();
}

requestMatchersで指定したパスは上から順に評価されるため、記述順を間違えると意図しないURLが公開されてしまいます。

ワイルドカードを使う場合は、より限定的なパターンを先に書くのが基本です。

SecurityFilterChainに登録されたフィルタが、認証・認可をどの順番で判定しているかを図にすると次のとおりです。

SecurityFilterChainの判定順と、止まったときの結果
REQUESTHTTPリクエスト
1
CsrfFilterCSRFトークンを検証NG トークン不正(POST等) → 403 Forbidden
2
認証フィルタ誰なのかを確認フォームログイン・JWTなど
3
AuthorizationFilterURLごとの認可ルールを評価NG 未ログイン → ログイン画面へリダイレクト(APIなら401)NG 権限不足 → 403 Forbidden
4
@PreAuthorizeメソッド単位の認可NG 権限不足 → 403 Forbidden
5
CONTROLLERControllerの処理を実行
NOTE「未ログイン」はログイン画面へのリダイレクト(APIなら401)、「権限不足」は403と、止まった理由で返る結果が違う。

CSRFの検証が認証より手前で行われる点と、「未ログイン」と「権限不足」で返ってくる結果が違う点を押さえておくと、エラーの原因を切り分けやすくなります。

4つのリクエストがフィルタのどこで止まるか
REQUEST / リクエスト
止まる場所
結果
① ログイン済み・CSRFトークンなしのPOST
CsrfFilterトークン不正
403 ForbiddenCSRFの検証は認証より手前で行われる
② 未ログインで保護されたページへGETCsrfFilter:GETは検証対象外
AuthorizationFilter未ログイン
ログイン画面へリダイレクトAPIなら401
③ ログイン済みUSERがADMIN用URLへGETCsrfFilter:GETは検証対象外
AuthorizationFilter権限不足
403 Forbidden「未ログイン」とは結果が違う
④ ログイン済みADMIN・トークン付きのPOST
止まらないトークンOK → 認可OK → hasRole OK
Controllerで処理を実行
NOTEすべて通過したリクエストだけがControllerの処理まで届く。

Spring Security 7(Spring Boot 4)での書き方の変更点

ネットで見つかるサンプルコードには、古いバージョンの書き方が今も多く残っています。Spring Boot 4に同梱されるSpring Security 7では、次の書き方が使えなくなりました。

  • .and()でつなぐ書き方が廃止され、authorizeHttpRequests(auth -> ...)のようなラムダDSLが必須になった
  • AntPathRequestMatcher・MvcRequestMatcherがサポート外になり、パス指定はPathPatternRequestMatcherベースに統一された
  • 独自DSLの適用は、apply()ではなくwith()を使うのが現在の書き方になっている
  • WebSecurityConfigurerAdapterの継承(6.0で削除済み)ではなく、SecurityFilterChainの@Beanで設定する

旧来の書き方と、Spring Security 7での書き方を並べると次のとおりです。

// Spring Security 5系までの書き方(Spring Security 7ではコンパイルエラー)
http
    .authorizeRequests()
        .antMatchers("/admin/**").hasRole("ADMIN")
        .anyRequest().authenticated()
        .and()
    .formLogin()
        .loginPage("/login")
        .permitAll();
// Spring Security 7(Spring Boot 4)の書き方:ラムダDSLで設定する
http
    .authorizeHttpRequests(auth -> auth
        .requestMatchers("/admin/**").hasRole("ADMIN")
        .anyRequest().authenticated()
    )
    .formLogin(form -> form
        .loginPage("/login")
        .permitAll()
    );

requestMatchers("/admin/**")のように文字列でパスを渡す書き方は、そのまま使えます。古い記事のコードを写してコンパイルエラーになったときは、まず.and()とantMatchersが残っていないかを確認してください。

バージョンごとの詳しい移行手順は、Spring Security公式リファレンスの移行ガイドにまとまっています。

ロールベースのアクセス制御(@PreAuthorize/hasRole)

URL単位のSecurityFilterChainだけでは、処理の中身に応じた細かい制御をしにくくなります。

同じURLの中で「一般ユーザーは閲覧のみ、管理者は編集も可能」のような使い分けが必要になるためです。そこで使うのがメソッド単位の認可です。

設定クラスに@EnableMethodSecurityを付けると、各メソッドに@PreAuthorize("hasRole('ADMIN')")を付与するだけでロールチェックができるようになります。

@PreAuthorize("hasRole('ADMIN')")
@DeleteMapping("/users/{id}")
public void deleteUser(@PathVariable Long id) {
    userService.delete(id);
}

URL認可とメソッド認可は併用できます。

URL認可で大まかにアクセスを絞り、メソッド認可でさらに細かい操作単位の制御を加える、という二段構えにするのが実務では一般的です。

@PreAuthorizeの条件式をControllerに書きすぎるとテストが書きにくくなるため、複雑な認可ロジックはServiceクラス側のメソッドに切り出しておくと保守しやすくなります。

認証方式の実装|Basic認証からフォーム認証まで

認証方式にはいくつか種類があり、用途によって使い分けます。

社内システムのように画面遷移があるWebアプリならフォーム認証、外部から呼ばれるAPIならJWT・OAuth2が向いています。

ここではまず画面型アプリの基本であるフォーム認証を扱います。

Basic認証とフォーム認証の使い分け基準

Basic認証はブラウザ標準のダイアログでユーザー名・パスワードを送るシンプルな方式で、API間通信や簡易的な社内ツールに向いています。

一方のフォーム認証は、ログイン画面を自前でデザインできるため、ユーザー体験を重視する画面遷移型のWebアプリに向いています。

どちらもSecurityFilterChain内でhttpBasic(Customizer.withDefaults())とformLogin(...)を切り替えるだけで実装できるため、要件に応じて選べます。

フォーム認証の実装とログイン画面のカスタマイズ

formLogin(Customizer.withDefaults())を呼ぶだけでデフォルトのログイン画面が使えますが、実務では自前のログイン画面に差し替えるケースがほとんどです。

loginPage("/login")でカスタムのログインページURLを指定し、対応するControllerとThymeleafテンプレートを用意すれば、デザインを自由に組み込めます。

http
    .authorizeHttpRequests(auth -> auth
        .requestMatchers("/login", "/css/**").permitAll()
        .anyRequest().authenticated()
    )
    .formLogin(form -> form
        .loginPage("/login")          // 自前のログイン画面
        .defaultSuccessUrl("/home")   // ログイン成功後の遷移先
        .permitAll()
    );

loginPageに指定したURLはpermitAll()で許可しておかないと、後述する無限リダイレクトの原因になります。

UserDetailsServiceでDBユーザー認証を行う

実際のアプリでは、ユーザー情報はメモリではなくDBに保存します。

UserDetailsServiceインターフェースを実装し、loadUserByUsernameメソッドでDBからユーザーを検索してSpring Securityに渡す仕組みを作ります。DBアクセス部分はSpring Data JPAのリポジトリで書くと簡潔です。

この実装を1本自作すれば、あとはSpring Security側が認証処理をすべて肩代わりしてくれます。

BCryptPasswordEncoderでパスワードをハッシュ化する

パスワードは平文でDBに保存してはいけません。

Spring Securityが標準で提供するBCryptPasswordEncoderを使えば、ソルト付きのハッシュ化を数行で実装できます。

ユーザー登録時には自分でencode()を呼んでハッシュ化して保存します。ログイン時の照合は、保存されたハッシュに含まれるソルトを使ってSpring Securityが自動で行ってくれます。

@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

// ユーザー登録時にハッシュ化して保存する
user.setPassword(passwordEncoder.encode(rawPassword));

PasswordEncoderを@Beanとして登録しておけば、ログイン時の照合にも同じエンコーダーが自動で使われます。

Spring SecurityのCSRF対策|設定と無効化の方法

CSRF(クロスサイトリクエストフォージェリ)とは、ログイン中のユーザーに、罠サイト経由で意図しないリクエストを送らせる攻撃です。

Spring SecurityではCSRF対策がデフォルトで有効になっており、画面遷移型のアプリなら追加の設定なしで守られます。設定が必要になるのは、REST APIやSPAと組み合わせる場面です。

CSRF保護の仕組みとデフォルト設定

Spring Securityは、セッションごとに推測できないCSRFトークンを発行し、状態を変更するリクエストにそのトークンが含まれているかを検証します。

検証の対象はPOST・PUT・PATCH・DELETEで、GETなどの安全なメソッドは対象外です。トークンはデフォルトではHttpSessionに保存されます。

Thymeleafでth:actionを使ってフォームを書けば、_csrfというhiddenフィールドにトークンが自動で埋め込まれるため、画面側で特別な実装は不要です。

REST APIでCSRFを無効化する設定

外部クライアントから呼ばれるREST APIでは、CSRFトークンをやり取りする前提がないためcsrf(AbstractHttpConfigurer::disable)で無効化します。

無効化する際は、その代わりにJWT等の別の認証手段でリクエストの正当性を担保する設計になっているかを必ず確認してください。

@Bean
public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {
    http
        .csrf(AbstractHttpConfigurer::disable)
        .sessionManagement(session -> session
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        )
        .authorizeHttpRequests(auth -> auth
            .anyRequest().authenticated()
        );
    return http.build();
}

セッションを使わないSTATELESS構成にして、JWTなどのトークンで認証することが前提の設定です。

CSRFを無効化しただけで満足し、代替の防御策を用意しないまま本番稼働させてしまうケースは珍しくありません。

無効化は保護をやめる判断ではなく、別の保護に置き換える判断だと捉えるのが安全です。

一部のURLだけCSRFを除外する(ignoringRequestMatchers)

画面はCSRF保護を効かせたまま、外部サービスから呼ばれるWebhookなど一部のURLだけを除外したい場合は、ignoringRequestMatchersを使います。

http
    .csrf(csrf -> csrf
        .ignoringRequestMatchers("/api/webhook/**")
    );

アプリ全体を無効化するより影響範囲を小さくできるため、除外したいURLが限られているならこちらを選ぶのが安全です。

SPAでCookieにCSRFトークンを渡す設定

ReactやVueなどのSPAからセッション認証のAPIを呼ぶ場合は、CSRFを無効化せず、トークンをCookie経由でJavaScriptに渡します。

// Spring Security 7系: CookieへのトークンとSPA向けのトークン処理をまとめて設定する
http
    .csrf(csrf -> csrf.spa());

// 6系ではCookieに保存するだけでは足りず、Cookieの値をそのまま受け取れるハンドラも指定する
http
    .csrf(csrf -> csrf
        .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
        .csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler())
    );

フロント側はCookie(XSRF-TOKEN)の値を読み取り、X-XSRF-TOKENヘッダーに付けて送信します。Spring Security 6以降はトークンにBREACH対策のランダム値を混ぜるのが標準のため、CookieCsrfTokenRepositoryを指定するだけではCookieの値が受け付けられず403になります。上のコードのように、spa()かCsrfTokenRequestAttributeHandlerの指定が必要です。Cookieでセッションを管理している限りCSRF攻撃は成立しうるため、SPAだからといって無効化してよいわけではありません。

JWT・OAuth2で実装するモダンな認証

フロントエンドとバックエンドが分離した構成や、外部サービス連携が必要な場合は、セッションベースの認証ではなくJWT・OAuth2が主流になります。

一方で、フロントエンドが分離していない小規模な個人開発アプリにまでJWTを導入するのは過剰設計になりやすい判断です。

画面遷移型のアプリならセッション認証で十分なケースも多く、必要になってから導入を検討しても遅くありません。

あわせて読む

JWTトークン認証を実装する流れ

JWT認証では、ログイン成功時にサーバーがトークンを発行し、以降のリクエストではそのトークンをヘッダーに乗せてサーバーへ送ります。

実装の要は、リクエストごとにトークンを検証するOncePerRequestFilterを自作し、SecurityFilterChainに組み込むことです。

セッションを持たないステートレスな認証にできるため、複数サーバーでのスケールがしやすくなります。

OAuth2クライアント連携(GitHub/Google)の基礎

spring-boot-starter-oauth2-clientを追加し、application.ymlにクライアントIDとシークレットを設定します。

これだけで、GitHubやGoogleアカウントでのログイン機能を数行の設定だけで実装できます。

自前でパスワード管理をしなくて済むため、個人開発のようにユーザー数が少ないうちは特に導入コストに見合いやすい選択肢です。

初心者がつまずきやすいエラーと対処法

Spring Securityは設定項目が多いぶん、初心者が同じところでつまずきやすい傾向があります。

代表的な3つのエラーを押さえておけば、詰まる時間をかなり減らせます。

PasswordEncoderが見つからないエラー

「There is no PasswordEncoder mapped for the id "null"」というエラーは、PasswordEncoderのBeanを定義しないまま、プレフィックス({bcrypt}など)のないパスワードで認証したときに出ます。Spring Security 6以降では、同じ原因でも「Given that there is no default password encoder configured, each password must have a password encoding prefix」というメッセージで出ます。

BCryptPasswordEncoderを@Beanとして登録し忘れていないか、まず確認してください。

ログインページへの無限リダイレクト

カスタムログインページをpermitAll()し忘れると、ログインページ自体が認証必須になり、無限リダイレクトが発生します。

loginPageに指定したパスは、authorizeHttpRequestsの設定で必ず許可リストに加えておく必要があります。

Invalid CSRF Tokenで403になるときの対処法

フォーム送信時に「Invalid CSRF Token」が出る場合、多くはフォームにCSRFトークンが含まれていないことが原因です。

Thymeleafテンプレートでth:actionを使ってフォームを書けば、CSRFトークンのhiddenフィールドが自動で埋め込まれます。

REST APIでこのエラーが出る場合は、そもそもCSRFトークンをやり取りする設計になっていないことが多いです。

csrf(AbstractHttpConfigurer::disable)で無効化した上で、JWT等の別の認証手段で保護する構成になっているか見直してください。

よくある質問

Q. Spring Securityは学習コストが高いですか?
A. 設定項目は多いですが、認証・認可の基本概念とSecurityFilterChainの書き方さえ押さえれば、実装の多くは同じパターンの応用で対応できます。

Q. セッション認証とJWT認証はどちらを選ぶべきですか?
A. サーバーサイドで画面を描画するアプリならセッション認証、フロントエンドが分離したSPAやモバイルアプリと連携するAPIならJWT認証が向いています。

Q. CSRF対策は常に有効にすべきですか?
A. 画面遷移を伴うアプリでは有効にすべきです。CookieベースのセッションでCSRFを無効化すると、意図しないリクエストを防げなくなります。

Q. Spring Securityを使わずに自作するのは可能ですか?
A. 技術的には可能ですが、パスワードハッシュ化やCSRF対策など考慮すべき点が多く、実務では既にテストされたSpring Securityを使うほうが安全です。

Q. ロールベースの認可はどう実装しますか?
A. @EnableMethodSecurityを有効化し、メソッドに@PreAuthorize("hasRole('ADMIN')")を付与します。

URL単位の認可と組み合わせることで、操作単位の細かい制御ができます。

Q. CSRFトークンエラーが出たときはどうすればいいですか?
A. フォームにCSRFトークンが含まれているか確認してください。Thymeleafのth:actionを使えば自動で埋め込まれます。REST APIでセッションやCookieを使わず、JWTなどをAuthorizationヘッダーで送る構成なら、CSRFを無効化したうえで別の認証手段で保護する設計になっているか見直します。Cookieのセッションで認証するAPIでは無効化せず、トークンをヘッダーで送ります。

あわせて読みたい

まとめ|Spring Securityは認証・認可を任せて設計に集中する

Spring Securityの要点は、次の3点です。

  • 認証(誰か)と認可(何ができるか)を明確に区別して設計する
  • SecurityFilterChainでURL単位の認可を定義し、パスワードは必ずハッシュ化する(BCryptが標準的な選択肢)
  • 用途に応じてセッション認証・JWT・OAuth2を使い分ける

Spring Securityは設定項目が多く最初はとっつきにくく感じますが、認証・認可のロジックを自分でゼロから書くよりも、実績のあるフレームワークに任せたほうが結果的に安全です。

まずは最小構成のSecurityFilterChainから動かしてみて、必要な機能を1つずつ足していくのがおすすめです。

REVISIONS / 改訂履歴
REV.DATE内容
32026-10-16図解を描き直し(図面のデザインにそろえた)
22026-10-04記載内容の事実確認と修正
12026-08-29初版
NEXT READ本棚から1本

AIで開発を組み直すなら、まずこの5冊から

入門・実践・設計まで、Claude Codeの本をレベル別に5冊選んで比べました。

→ おすすめ5冊を見る

  • この記事を書いた人
  • 最新記事

トム

SESで約8年、環境を変えられないもどかしさの中にいました。32歳で自社開発企業に転職し、今はClaude Codeと自動化で、仕事も時間も組み直している途中です。
・Javaバックエンドエンジニア(Java歴12年以上)
・資格:基本情報技術者/応用情報技術者/Java Silver/Python3エンジニア認定基礎
詳細なプロフィール

𝕏

Java入門