初めてSpring Securityを導入したら、突然ログイン画面が出てきて混乱した。そんな経験をした開発者は少なくないでしょう。Spring Securityは強力ですが、その設定の多様さゆえに「どこから手をつければいいのか」と戸惑うことも多いものです。
この記事では、Spring Securityを初めて使う開発者向けに、認証機能の基本を段階的に解説します。最小構成から始めて、Basic認証、フォーム認証へと順を追って実装することで、各設定の意味と初心者がつまずきやすいポイントを丁寧に説明します。
読み終えた後には、Spring Securityの認証の仕組みを理解し、自分のプロジェクトに合った認証方式を選択・実装できる状態になることを目指します。
Spring Securityとは
Spring Securityは、Springアプリケーションのセキュリティを担うフレームワークです。認証(誰か) と 認可(何ができるか) の機能を提供しますが、この記事では認証に焦点を当てます。
主な認証方式はBasic認証、フォーム認証、OAuth2、JWTなどがあります。この記事では、Basic認証とフォーム認証を段階的に実装していきましょう。
Spring Securityのデフォルト動作を確認する
まずは、Spring Securityを導入したときのデフォルト動作を確認しましょう。pom.xmlに以下の依存関係を追加するだけで、自動的に全エンドポイントが保護されます。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
アプリケーションを起動すると、コンソールに以下のようなログが表示されます。
Using generated security password: 8e557245-73e2-4286-969a-ff57fe326336
このパスワードは 起動ごとに変わります。デフォルトユーザー名のuserと組み合わせてログインできます。ブラウザで任意のエンドポイントにアクセスすると、自動生成されたログインページが表示されます。
セキュリティ上の注意: このデフォルトパスワードは開発時のみ使用してください。固定したい場合はapplication.ymlで以下のように設定できますが、本番環境では必ず無効化する必要があります。
spring:
security:
user:
name: user
password: dev-password
この自動設定の背後には、SecurityFilterChain という仕組みがあります。次のセクションでこの概念を理解しましょう。
SecurityFilterChainの基本
Spring Securityの中核となるのが SecurityFilterChain です。これは、どのURLを保護するか、どの認証方式を使うかなどのセキュリティルールを定義します。
Spring Security 6以降では、SecurityFilterChainを@Beanとして登録し、Lambda DSL記法で設定するのが標準です。基本的なパターンは以下の通りです。
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import static org.springframework.security.config.Customizer.withDefaults;
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated()
)
.httpBasic(withDefaults()); // この時点で認証方式を指定
return http.build();
}
}
ここでは、@Configurationアノテーションを使って設定クラスを作成し、SecurityFilterChainを@Beanとして登録しています。HttpSecurityオブジェクトを使ったビルダーパターンで、セキュリティルールを設定します。
この仕組みは、DIの実践的な応用例でもあります。Spring BootがこのBeanを自動的に検出し、アプリケーション全体のセキュリティ設定として適用します。
.httpBasic(withDefaults())の部分で認証方式を指定しています。この例ではBasic認証を使用しますが、次のセクションで詳しく見ていきましょう。
Basic認証の最小実装
それでは、InMemoryユーザーを使ったBasic認証を実装してみましょう。
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import static org.springframework.security.config.Customizer.withDefaults;
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated()
)
.httpBasic(withDefaults());
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public UserDetailsService userDetailsService(PasswordEncoder passwordEncoder) {
UserDetails user = User.builder()
.username("user")
.password(passwordEncoder.encode("password"))
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
}
}
.httpBasic(withDefaults())メソッドでBasic認証を有効にしています。UserDetailsServiceのBeanでは、InMemoryUserDetailsManagerを使ってメモリ上にテストユーザーを作成しています。
重要: 最初からBCryptPasswordEncoderを使っています。古いコード例ではUser.withDefaultPasswordEncoder()が使われていることがありますが、これは Spring Security 5.7以降で完全に非推奨(deprecated) となっており、開発環境でも使用すべきではありません。
Basic認証では、ユーザー名とパスワードをBase64エンコードしてAuthorizationヘッダーで送信します。
curl -u user:password http://localhost:8080/api/hello
または明示的にヘッダーを指定することもできます(以下のdXNlcjpwYXNzd29yZA==はuser:passwordをBase64エンコードした文字列です)。
curl -H "Authorization: Basic dXNlcjpwYXNzd29yZA==" http://localhost:8080/api/hello
ブラウザでアクセスすると、ブラウザ標準の認証ダイアログが表示されます。
パスワードエンコーダーの重要性
上記のコードでBCryptPasswordEncoderを使っていますが、これは必須です。パスワードを平文で保存すると、データベースが漏洩した際に全ユーザーのパスワードが露出してしまいます。
PasswordEncoderを@Beanとして登録すると、Spring Securityが認証時に自動的にこのBeanを使ってパスワードを検証してくれます。BCryptPasswordEncoderでエンコードされたパスワードは$2a$10$...のような形式で、バージョン、ソルト、ハッシュ値を含んでいます。
フォーム認証への移行
Basic認証はシンプルですが、ブラウザの認証ダイアログは使い勝手がよくありません。一般的なWebアプリケーションでは、フォーム認証がより適しています。
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated()
)
.formLogin(form -> form
.defaultSuccessUrl("/home", true)
.permitAll()
);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public UserDetailsService userDetailsService(PasswordEncoder passwordEncoder) {
UserDetails user = User.builder()
.username("user")
.password(passwordEncoder.encode("password"))
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
}
}
.httpBasic(withDefaults())を.formLogin()に置き換えました。Lambda DSL記法を使い、form -> form...の形で設定を記述しています。defaultSuccessUrl("/home", true)でログイン成功時のリダイレクト先を指定し、.permitAll()でログインページ自体へのアクセスを許可しています(これがないと無限リダイレクトが発生します)。
デフォルトのログインページは/loginで自動生成されます。ブラウザでアクセスすると、Spring Securityが提供する標準的なログインフォームが表示されます。
ログインページのカスタマイズ
独自のログインページを使う場合は、以下のように設定します。
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/custom-login", "/css/**", "/js/**").permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/custom-login")
.defaultSuccessUrl("/home", true)
.permitAll()
)
.logout(logout -> logout
.logoutSuccessUrl("/custom-login?logout")
.permitAll()
);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public UserDetailsService userDetailsService(PasswordEncoder passwordEncoder) {
UserDetails user = User.builder()
.username("user")
.password(passwordEncoder.encode("password"))
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
}
}
.loginPage("/custom-login")でカスタムログインページを指定し、.requestMatchers()で静的リソースとログインページへのアクセスを許可しています。
.requestMatchers("/css/**")の**は Ant形式のパターンマッチング で、「/css/以下の全てのパス」を意味します。たとえば/css/style.cssや/css/admin/layout.cssなどすべてにマッチします。
ログアウト機能の実装
.logout()メソッドで、ログアウト機能を設定しています。logoutSuccessUrl("/custom-login?logout")により、ログアウト後はログインページにリダイレクトされ、?logoutパラメータでログアウト成功メッセージを表示できます。
Thymeleafを使ったログインページの例は以下の通りです(src/main/resources/templates/custom-login.htmlに配置)。
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>ログイン</title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<div class="login-container">
<h1>ログイン</h1>
<div th:if="${param.error}" class="error">
ユーザー名またはパスワードが正しくありません。
</div>
<div th:if="${param.logout}" class="success">
ログアウトしました。
</div>
<form th:action="@{/custom-login}" method="post">
<div>
<label for="username">ユーザー名:</label>
<input type="text" id="username" name="username" required>
</div>
<div>
<label for="password">パスワード:</label>
<input type="password" id="password" name="password" required>
</div>
<button type="submit">ログイン</button>
</form>
</div>
</body>
</html>
重要なポイント:
- フォームの
action属性はth:actionを使ってThymeleafのURL式で指定します。 method="post"は必須です。name属性はusernameとpasswordである必要があります(カスタマイズも可能ですが、デフォルトはこの名前)。${param.error}でログインエラーを検出し、エラーメッセージを表示できます。
CSRFトークンについて: Thymeleafを使用している場合、POSTメソッドのフォームには自動的に以下のような隠しフィールドが追加されます。
<input type="hidden" name="_csrf" value="ランダムなトークン値"/>
これはThymeleafがth:actionを処理する際に自動的に行うもので、手動で追加する必要はありません。
静的リソースの配置: CSSファイル(/css/style.css)は、src/main/resources/static/css/style.cssに配置してください。Spring Bootはstaticフォルダ以下のファイルを自動的に静的リソースとして公開します。
初心者がつまずきやすい設定エラーと解決法
Spring Securityを使い始めると、いくつかの典型的なエラーに遭遇します。主なものを紹介します。
1. “There is no PasswordEncoder mapped for the id “null"" エラー
パスワードエンコーダーを設定せずに平文パスワードを使おうとすると発生します。PasswordEncoderを@Beanとして登録し、パスワードをエンコードしてください。
2. ログインページへの無限リダイレクト
ログインページ自体が認証を要求すると無限リダイレクトが発生します。.formLogin()内で.permitAll()を呼び出し、さらに.requestMatchers("/custom-login").permitAll()で明示的に許可してください。
3. CSRFトークンエラー
Thymeleafのth:actionを使えば自動的にCSRFトークンが埋め込まれます。手動でHTMLを書く場合は、<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>を追加してください。
デバッグログの有効化
トラブルシューティングには、デバッグログが非常に役立ちます。application.ymlに以下を追加してください。
logging:
level:
org.springframework.security: DEBUG
これにより、Spring Securityの内部動作が詳細にログ出力されます。
認証方式の選択基準
Basic認証とフォーム認証、どちらを選ぶべきでしょうか。
Basic認証が適している場面
- REST APIのエンドポイント保護
- 簡易的な管理画面や開発用ツール
- ステートレスなアプリケーション
- curlやPostmanなどのツールからのアクセスが主な場合
フォーム認証が適している場面
- 一般ユーザー向けのWebアプリケーション
- ブラウザからのアクセスが主な場合
- ログイン画面をカスタマイズしたい場合
- セッション管理が必要な場合
実際のプロジェクトでは、Spring BootのProfilesを使って環境によって設定を切り替えることもできます。例えば、開発環境ではBasic認証、本番環境ではフォーム認証といった使い分けも可能ですよ。
次のステップ
この記事では、Spring Securityの基本的な認証機能を段階的に実装しました。ここから先、以下のようなステップに進むことができます。
- データベース連携:
UserDetailsServiceを実装してデータベースからユーザー情報を取得 - OAuth2/OpenID Connect: GoogleやGitHubなどの外部サービスと連携したログイン
- JWT認証: トークンベースの認証でステートレスなAPIを構築
- 認可(Authorization): ロールベースのアクセス制御(
@PreAuthorizeなど)
これらの発展的トピックについては、今後の記事で扱う予定です。まずは、この記事で学んだ基本をしっかり理解し、自分のプロジェクトで試してみることをお勧めします。