Spring Bootでログイン機能を作ろうとして、Spring Securityの設定でいきなりつまずいた経験はないでしょうか。
私も最初は「なぜ全部のURLがログイン画面にリダイレクトされるんだ」と混乱しました。
この記事を読めば、認証・認可の基本概念からSecurityFilterChainの設定、CSRF対策、JWT・OAuth2による認証までが実装コード付きでわかります。
初心者がつまずきやすいエラーの対処法もあわせて紹介します。
「なぜ全部ログイン画面にリダイレクトされるんだ」と最初は本気で焦りました。
Spring Securityとは?認証と認可の基本
Spring Securityとは、Spring Bootアプリケーションに認証・認可の仕組みを組み込むためのセキュリティフレームワークです。
ログイン処理やアクセス制御を自前で実装しなくても、設定を書くだけでセキュアな仕組みを構築できます。
まず押さえておきたいのが、認証と認可という2つの用語の違いです。
認証(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でログインできる状態です。
これはデフォルトの安全側の挙動で、意図的にすべて保護してから必要な箇所だけ開放していく設計思想がここに表れています。
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が公開されてしまいます。
ワイルドカードを使う場合は、より限定的なパターンを先に書くのが基本です。
ロールベースのアクセス制御(@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()とformLogin()を切り替えるだけで実装できるため、要件に応じて選べます。
フォーム認証の実装とログイン画面のカスタマイズ
formLogin()を呼ぶだけでデフォルトのログイン画面が使えますが、実務では自前のログイン画面に差し替えるケースがほとんどです。
loginPage("/login")でカスタムのログインページURLを指定し、対応するControllerとThymeleafテンプレートを用意すれば、デザインを自由に組み込めます。
UserDetailsServiceでDBユーザー認証を行う
実際のアプリでは、ユーザー情報はメモリではなくDBに保存します。
UserDetailsServiceインターフェースを実装し、loadUserByUsernameメソッドでDBからユーザーを検索してSpring Securityに渡す仕組みを作ります。
この実装を1本自作すれば、あとはSpring Security側が認証処理をすべて肩代わりしてくれます。
パスワードの安全な管理とCSRF対策
認証を実装するうえで避けて通れないのが、パスワードの保管方法とCSRF対策です。
どちらも省略するとセキュリティ事故に直結するため、最初から正しい実装を身につけておく価値があります。
BCryptPasswordEncoderでパスワードをハッシュ化する
パスワードは平文でDBに保存してはいけません。
Spring Securityが標準で提供するBCryptPasswordEncoderを使えば、ソルト付きのハッシュ化を数行で実装できます。
ユーザー登録時にハッシュ化して保存し、ログイン時は入力値をハッシュ化して比較する、という流れをSpring Securityが自動で行ってくれます。
CSRF保護の仕組みとREST APIでの無効化
CSRF(クロスサイトリクエストフォージェリ)対策は、画面遷移型のアプリではデフォルトで有効にしておくべき機能です。
一方、外部クライアントから呼ばれるREST APIでは、CSRFトークンをやり取りする前提がないためcsrf(AbstractHttpConfigurer::disable)で無効化します。
無効化する際は、その代わりにJWT等の別の認証手段でリクエストの正当性を担保する設計になっているかを必ず確認してください。
CSRFを無効化しただけで満足し、代替の防御策を用意しないまま本番稼働させてしまうケースは珍しくありません。
無効化は保護をやめる判断ではなく、別の保護に置き換える判断だと捉えるのが安全です。
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」というエラーは、PasswordEncoderのBeanを定義せずに認証を実装したときに発生します。
BCryptPasswordEncoderを@Beanとして登録し忘れていないか、まず確認してください。
ログインページへの無限リダイレクト
カスタムログインページをpermitAll()し忘れると、ログインページ自体が認証必須になり、無限リダイレクトが発生します。
loginPageに指定したパスは、authorizeHttpRequestsの設定で必ず許可リストに加えておく必要があります。
CSRFトークンエラーの対処法
フォーム送信時に「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の場合はCSRF自体を無効化し、別の認証手段で保護する設計になっているか見直します。
まとめ|Spring Securityは認証・認可を任せて設計に集中する
Spring Securityの要点をまとめると、次の3点に集約されます。
- 認証(誰か)と認可(何ができるか)を明確に区別して設計する
- SecurityFilterChainでURL単位の認可を定義し、パスワードは必ずBCryptでハッシュ化する
- 用途に応じてセッション認証・JWT・OAuth2を使い分ける
Spring Securityは設定項目が多く最初はとっつきにくく感じますが、認証・認可のロジックを自分でゼロから書くよりも、実績のあるフレームワークに任せたほうが結果的に安全です。
まずは最小構成のSecurityFilterChainから動かしてみて、必要な機能を1つずつ足していくのがおすすめです。