Spring Securityのフォームログインは、Webアプリケーションで最も一般的な認証方式です。本記事では、formLogin()の詳細な設定オプション、カスタムログインページの作成方法、認証成功・失敗時のハンドリング、そしてRemember-Me機能の実装まで、ユーザーフレンドリーなフォームログイン機能を構築するための実践的なガイドを解説します。
実行環境と前提条件#
本記事の内容を実践するにあたり、以下の環境を前提としています。
| 項目 |
バージョン・要件 |
| Java |
17以上 |
| Spring Boot |
3.4.x |
| Spring Security |
6.4.x |
| テンプレートエンジン |
Thymeleaf 3.x |
| ビルドツール |
Maven または Gradle |
| IDE |
VS Code または IntelliJ IDEA |
事前に以下の知識があると理解がスムーズです。
- Spring Securityの基本概念(認証・認可の違い)
- SecurityFilterChainの基本的な仕組み
- UserDetailsServiceとPasswordEncoderの基本
フォームログインの基本動作#
Spring Securityのフォームログインは、UsernamePasswordAuthenticationFilterによって処理されます。まず、認証が必要なリソースへの未認証アクセスから、ログイン完了までの流れを確認します。
sequenceDiagram
participant User as ユーザー
participant Browser as ブラウザ
participant Filter as UsernamePasswordAuthenticationFilter
participant AuthManager as AuthenticationManager
participant Handler as SuccessHandler/FailureHandler
User->>Browser: 保護されたリソースにアクセス
Browser->>Filter: リダイレクト(/login)
Filter-->>Browser: ログインページを表示
User->>Browser: ユーザー名/パスワードを入力
Browser->>Filter: POST /login
Filter->>AuthManager: 認証を委譲
alt 認証成功
AuthManager-->>Filter: Authentication
Filter->>Handler: AuthenticationSuccessHandler
Handler-->>Browser: リダイレクト(成功URL)
else 認証失敗
AuthManager-->>Filter: AuthenticationException
Filter->>Handler: AuthenticationFailureHandler
Handler-->>Browser: リダイレクト(/login?error)
endフォームログインのデフォルト動作は以下のとおりです。
| 項目 |
デフォルト値 |
説明 |
| ログインページURL |
/login(GET) |
ログインフォームを表示 |
| ログイン処理URL |
/login(POST) |
認証処理を実行 |
| ユーザー名パラメータ |
username |
フォームのユーザー名フィールド名 |
| パスワードパラメータ |
password |
フォームのパスワードフィールド名 |
| 認証成功時 |
元のリクエストURLへリダイレクト |
SavedRequestを使用 |
| 認証失敗時 |
/login?error |
エラーパラメータ付きでリダイレクト |
formLogin()メソッドを使用して、フォームログインの動作を詳細にカスタマイズできます。
基本的な設定#
最もシンプルなフォームログイン設定は以下のとおりです。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/public/**", "/css/**", "/js/**").permitAll()
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults());
return http.build();
}
}
|
Customizer.withDefaults()を使用すると、Spring Securityがデフォルトのログインページを自動生成します。
カスタムログインページの設定#
実際のアプリケーションでは、独自のログインページを使用するのが一般的です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/login", "/public/**", "/css/**", "/js/**").permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login") // カスタムログインページのURL
.loginProcessingUrl("/authenticate") // 認証処理を行うURL
.usernameParameter("email") // ユーザー名パラメータ名
.passwordParameter("passwd") // パスワードパラメータ名
.permitAll() // ログインページへのアクセスを許可
);
return http.build();
}
|
各設定オプションの詳細は以下のとおりです。
| メソッド |
説明 |
デフォルト値 |
loginPage(String) |
ログインページのURL |
/login |
loginProcessingUrl(String) |
認証処理を受け付けるURL |
/login |
usernameParameter(String) |
ユーザー名のリクエストパラメータ名 |
username |
passwordParameter(String) |
パスワードのリクエストパラメータ名 |
password |
permitAll() |
ログイン関連URLへの匿名アクセスを許可 |
- |
リダイレクト先の設定#
認証成功・失敗時のリダイレクト先を設定できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/login", "/public/**").permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.defaultSuccessUrl("/dashboard", false) // 認証成功時のデフォルトURL
.failureUrl("/login?error=true") // 認証失敗時のURL
.permitAll()
);
return http.build();
}
|
defaultSuccessUrl()の第2引数について解説します。
| 値 |
動作 |
false(デフォルト) |
元のリクエストURLがあればそこへ、なければ指定URLへリダイレクト |
true |
常に指定URLへリダイレクト(SavedRequestを無視) |
元のリクエストを常に尊重したい場合はfalseを、ログイン後は必ず特定ページに遷移させたい場合はtrueを指定します。
カスタムログインページの作成#
Thymeleafを使用してカスタムログインページを作成します。
ログインページのテンプレート#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
|
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ログイン</title>
<link rel="stylesheet" th:href="@{/css/login.css}">
</head>
<body>
<div class="login-container">
<h1>ログイン</h1>
<!-- エラーメッセージ -->
<div th:if="${param.error}" class="alert alert-error">
ユーザー名またはパスワードが正しくありません。
</div>
<!-- ログアウトメッセージ -->
<div th:if="${param.logout}" class="alert alert-success">
ログアウトしました。
</div>
<!-- セッションタイムアウトメッセージ -->
<div th:if="${param.timeout}" class="alert alert-warning">
セッションがタイムアウトしました。再度ログインしてください。
</div>
<form th:action="@{/login}" method="post">
<div class="form-group">
<label for="username">ユーザー名</label>
<input type="text"
id="username"
name="username"
placeholder="ユーザー名を入力"
autocomplete="username"
required>
</div>
<div class="form-group">
<label for="password">パスワード</label>
<input type="password"
id="password"
name="password"
placeholder="パスワードを入力"
autocomplete="current-password"
required>
</div>
<div class="form-group remember-me">
<input type="checkbox" id="remember-me" name="remember-me">
<label for="remember-me">ログイン状態を保持する</label>
</div>
<button type="submit" class="btn btn-primary">ログイン</button>
</form>
</div>
</body>
</html>
|
フォームの重要なポイントは以下のとおりです。
th:action="@{/login}"によりCSRFトークンが自動的に含まれる
name="username"とname="password"はSpring Securityのデフォルトパラメータ名
method="post"でPOSTリクエストを送信
ログインコントローラーの作成#
ログインページを表示するコントローラーを作成します。
1
2
3
4
5
6
7
8
9
10
11
|
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class LoginController {
@GetMapping("/login")
public String login() {
return "login";
}
}
|
Spring MVCを使用する場合、コントローラーでGETリクエストを処理してテンプレートを返す必要があります。POSTリクエスト(/login)はSpring Securityが自動的に処理します。
ログイン成功時のカスタムハンドリング#
認証成功時に独自の処理を実行するには、AuthenticationSuccessHandlerを実装します。
AuthenticationSuccessHandlerの実装#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
|
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Component
public class CustomAuthenticationSuccessHandler
extends SavedRequestAwareAuthenticationSuccessHandler {
public CustomAuthenticationSuccessHandler() {
// デフォルトのリダイレクト先を設定
setDefaultTargetUrl("/dashboard");
// 元のリクエストがない場合のみデフォルトURLを使用
setAlwaysUseDefaultTargetUrl(false);
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication)
throws IOException, ServletException {
// ログイン成功時のカスタム処理
String username = authentication.getName();
logLoginSuccess(username, request);
// ロールに応じてリダイレクト先を変更
String targetUrl = determineTargetUrl(authentication);
if (targetUrl != null) {
getRedirectStrategy().sendRedirect(request, response, targetUrl);
return;
}
// デフォルトの処理(SavedRequestへのリダイレクト)を実行
super.onAuthenticationSuccess(request, response, authentication);
}
private String determineTargetUrl(Authentication authentication) {
for (GrantedAuthority authority : authentication.getAuthorities()) {
if ("ROLE_ADMIN".equals(authority.getAuthority())) {
return "/admin/dashboard";
}
}
return null; // デフォルト処理を使用
}
private void logLoginSuccess(String username, HttpServletRequest request) {
String ipAddress = request.getRemoteAddr();
String userAgent = request.getHeader("User-Agent");
// ログ出力やデータベースへの記録などを実行
System.out.printf("Login success: user=%s, ip=%s%n", username, ipAddress);
}
}
|
セキュリティ設定への適用#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final CustomAuthenticationSuccessHandler successHandler;
public SecurityConfig(CustomAuthenticationSuccessHandler successHandler) {
this.successHandler = successHandler;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/login", "/public/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.successHandler(successHandler)
.permitAll()
);
return http.build();
}
}
|
ログイン失敗時のカスタムハンドリング#
認証失敗時に詳細なエラー情報を提供するには、AuthenticationFailureHandlerを実装します。
AuthenticationFailureHandlerの実装#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
|
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
@Component
public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException exception)
throws IOException, ServletException {
String errorMessage = determineErrorMessage(exception);
String username = request.getParameter("username");
// ログイン失敗の記録
logLoginFailure(username, exception, request);
// アカウントロック処理(連続失敗時)
handleFailedAttempt(username);
// エラーメッセージをURLエンコードしてリダイレクト
String encodedMessage = URLEncoder.encode(errorMessage, StandardCharsets.UTF_8);
response.sendRedirect("/login?error=true&message=" + encodedMessage);
}
private String determineErrorMessage(AuthenticationException exception) {
if (exception instanceof BadCredentialsException) {
return "ユーザー名またはパスワードが正しくありません";
} else if (exception instanceof LockedException) {
return "アカウントがロックされています。管理者にお問い合わせください";
} else if (exception instanceof DisabledException) {
return "アカウントが無効化されています";
} else if (exception instanceof UsernameNotFoundException) {
// セキュリティ上、BadCredentialsと同じメッセージを返す
return "ユーザー名またはパスワードが正しくありません";
}
return "認証に失敗しました";
}
private void logLoginFailure(String username,
AuthenticationException exception,
HttpServletRequest request) {
String ipAddress = request.getRemoteAddr();
System.out.printf("Login failed: user=%s, ip=%s, reason=%s%n",
username, ipAddress, exception.getClass().getSimpleName());
}
private void handleFailedAttempt(String username) {
// 連続失敗回数のカウントやアカウントロック処理を実装
// 実際のアプリケーションではデータベースで管理
}
}
|
セキュリティ設定への適用#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/login", "/public/**").permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.successHandler(successHandler)
.failureHandler(failureHandler)
.permitAll()
);
return http.build();
}
|
ログインページでのエラーメッセージ表示#
1
2
3
4
5
6
7
8
9
|
<!-- カスタムエラーメッセージの表示 -->
<div th:if="${param.error}" class="alert alert-error">
<span th:if="${param.message}" th:text="${param.message}">
認証に失敗しました。
</span>
<span th:unless="${param.message}">
ユーザー名またはパスワードが正しくありません。
</span>
</div>
|
Remember-Me機能の実装#
Remember-Me機能を使用すると、ブラウザを閉じてもログイン状態を維持できます。Spring Securityは2種類のRemember-Me実装を提供しています。
Remember-Meの仕組み#
flowchart TB
A[ログイン成功] --> B{Remember-Me<br/>チェック?}
B -->|Yes| C[Remember-Meトークン生成]
B -->|No| D[セッション認証のみ]
C --> E[Cookieに保存]
E --> F[次回アクセス時]
F --> G{セッション有効?}
G -->|Yes| H[通常認証]
G -->|No| I[Remember-Me Cookie確認]
I --> J{トークン有効?}
J -->|Yes| K[自動ログイン]
J -->|No| L[ログインページへ]シンプルハッシュベースのRemember-Me#
最も簡単な実装方法です。トークンはCookieにハッシュ化して保存されます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/login", "/public/**").permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.permitAll()
)
.rememberMe(remember -> remember
.key("uniqueAndSecretKey") // トークン生成に使用する秘密鍵
.tokenValiditySeconds(86400 * 14) // 14日間有効
.rememberMeParameter("remember-me") // フォームのパラメータ名
.rememberMeCookieName("remember-me") // Cookie名
);
return http.build();
}
|
シンプルハッシュベースのトークン構造は以下のとおりです。
1
2
|
base64(username + ":" + expirationTime + ":" + algorithmName + ":" +
algorithmHex(username + ":" + expirationTime + ":" + password + ":" + key))
|
| メソッド |
説明 |
デフォルト値 |
key(String) |
トークン署名に使用する秘密鍵 |
ランダム生成 |
tokenValiditySeconds(int) |
トークンの有効期間(秒) |
14日 |
rememberMeParameter(String) |
フォームのパラメータ名 |
remember-me |
rememberMeCookieName(String) |
Cookie名 |
remember-me |
永続化トークンベースのRemember-Me#
より安全なアプローチとして、トークンをデータベースに保存する方法があります。
データベーステーブルの作成#
1
2
3
4
5
6
|
CREATE TABLE persistent_logins (
username VARCHAR(64) NOT NULL,
series VARCHAR(64) PRIMARY KEY,
token VARCHAR(64) NOT NULL,
last_used TIMESTAMP NOT NULL
);
|
セキュリティ設定#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import javax.sql.DataSource;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final DataSource dataSource;
public SecurityConfig(DataSource dataSource) {
this.dataSource = dataSource;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/login", "/public/**").permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.permitAll()
)
.rememberMe(remember -> remember
.tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(86400 * 30) // 30日間有効
.userDetailsService(userDetailsService)
);
return http.build();
}
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
// 起動時にテーブルを自動作成する場合は以下を有効化
// tokenRepository.setCreateTableOnStartup(true);
return tokenRepository;
}
}
|
2つのアプローチの比較は以下のとおりです。
| 項目 |
ハッシュベース |
永続化トークン |
| セキュリティ |
中 |
高 |
| パスワード変更時 |
トークン無効化 |
トークン有効 |
| トークン漏洩時 |
有効期限まで使用可能 |
個別に無効化可能 |
| インフラ要件 |
なし |
データベース必要 |
| 複数デバイス |
シリーズ管理なし |
シリーズで管理 |
Remember-Meの注意事項#
Remember-Me機能を実装する際は、以下のセキュリティ考慮事項を把握しておく必要があります。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/login", "/public/**").permitAll()
// 機密性の高い操作は完全認証を要求
.requestMatchers("/account/password", "/account/delete").fullyAuthenticated()
// 通常のリソースはRemember-Me認証でもアクセス可能
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.permitAll()
)
.rememberMe(remember -> remember
.key("uniqueAndSecretKey")
.tokenValiditySeconds(86400 * 14)
);
return http.build();
}
|
fullyAuthenticated()とauthenticated()の違いは以下のとおりです。
| メソッド |
説明 |
使用場面 |
authenticated() |
Remember-Me認証を含む |
一般的なコンテンツ |
fullyAuthenticated() |
Remember-Me認証を除外 |
パスワード変更、決済など |
ログアウト機能の設定#
フォームログインと合わせてログアウト機能を設定します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/login", "/public/**").permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.permitAll()
)
.logout(logout -> logout
.logoutUrl("/logout") // ログアウトURL
.logoutSuccessUrl("/login?logout=true") // ログアウト成功時のリダイレクト先
.invalidateHttpSession(true) // セッションを無効化
.deleteCookies("JSESSIONID", "remember-me") // Cookieを削除
.clearAuthentication(true) // 認証情報をクリア
.permitAll()
)
.rememberMe(remember -> remember
.key("uniqueAndSecretKey")
);
return http.build();
}
|
ログアウトボタンはフォームで実装します(CSRFトークンが必要なため)。
1
2
3
|
<form th:action="@{/logout}" method="post">
<button type="submit" class="btn btn-logout">ログアウト</button>
</form>
|
実践的な統合設定例#
これまでの設定をすべて統合した実践的な設定例を示します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
|
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.config.annotation.web.configuration.EnableWebSecurity;
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.web.SecurityFilterChain;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import javax.sql.DataSource;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final DataSource dataSource;
private final UserDetailsService userDetailsService;
private final CustomAuthenticationSuccessHandler successHandler;
private final CustomAuthenticationFailureHandler failureHandler;
public SecurityConfig(DataSource dataSource,
UserDetailsService userDetailsService,
CustomAuthenticationSuccessHandler successHandler,
CustomAuthenticationFailureHandler failureHandler) {
this.dataSource = dataSource;
this.userDetailsService = userDetailsService;
this.successHandler = successHandler;
this.failureHandler = failureHandler;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// 認可設定
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/login", "/register", "/public/**").permitAll()
.requestMatchers("/css/**", "/js/**", "/images/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/account/password", "/account/delete").fullyAuthenticated()
.anyRequest().authenticated()
)
// フォームログイン設定
.formLogin(form -> form
.loginPage("/login")
.loginProcessingUrl("/login")
.usernameParameter("username")
.passwordParameter("password")
.successHandler(successHandler)
.failureHandler(failureHandler)
.permitAll()
)
// ログアウト設定
.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/login?logout=true")
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID", "remember-me")
.clearAuthentication(true)
.permitAll()
)
// Remember-Me設定
.rememberMe(remember -> remember
.tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(86400 * 14)
.userDetailsService(userDetailsService)
.rememberMeParameter("remember-me")
)
// セッション管理
.sessionManagement(session -> session
.maximumSessions(1) // 同時セッション数を制限
.expiredUrl("/login?expired=true") // セッション期限切れ時のリダイレクト
);
return http.build();
}
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
return tokenRepository;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
|
動作確認#
設定が正しく動作するか確認するためのテスト観点です。
| テスト観点 |
期待される動作 |
| 未認証でのアクセス |
/loginへリダイレクト |
| 正しい認証情報でログイン |
成功URLへリダイレクト |
| 誤った認証情報でログイン |
/login?errorへリダイレクト、エラーメッセージ表示 |
| Remember-Meチェックあり |
Cookieが設定される |
| ブラウザ再起動後のアクセス |
Remember-Meで自動ログイン |
| ログアウト |
セッション・Cookie削除、ログインページへリダイレクト |
| 機密操作へのアクセス |
Remember-Me認証では再ログイン要求 |
まとめ#
本記事では、Spring Securityのフォームログイン機能について、以下の内容を解説しました。
formLogin()の設定オプションと各パラメータの役割
- Thymeleafを使用したカスタムログインページの作成
AuthenticationSuccessHandlerによる認証成功時のカスタム処理
AuthenticationFailureHandlerによる認証失敗時のエラーハンドリング
- ハッシュベースと永続化トークンベースのRemember-Me機能
- ログアウト機能との統合設定
フォームログインはWebアプリケーションの基本的な認証方式ですが、適切なカスタマイズにより、セキュアでユーザーフレンドリーな認証体験を提供できます。特にRemember-Me機能を使用する際は、機密性の高い操作にはfullyAuthenticated()を適用し、セキュリティと利便性のバランスを取ることが重要です。
参考リンク#