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()の設定オプション

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()を適用し、セキュリティと利便性のバランスを取ることが重要です。

参考リンク