【Spring Boot】フォームログイン機能を実装する

Java

こんにちは、かつコーチです。

前回の記事でSpring Securityの基本を解説しましたが、.formLogin(Customizer.withDefaults())だけでは、Spring Securityが自動生成した無機質なログイン画面しか使えません。

実務では自作のログイン画面を用意し、データベースに保存したユーザー情報で認証したいはずです。

この記事では、独自のログイン画面とユーザー認証を実装する手順を解説します。

実装手順

手順1:ユーザー情報を取得するUserDetailsServiceを実装する

Spring Securityは、認証時にUserDetailsServiceというインターフェースを通じてユーザー情報を取得します。

@Service
public class CustomUserDetailsService implements UserDetailsService {

    private final UserRepository userRepository;

    public CustomUserDetailsService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByEmail(username)
                .orElseThrow(() -> new UsernameNotFoundException("ユーザーが見つかりません: " + username));

        return org.springframework.security.core.userdetails.User
                .withUsername(user.getEmail())
                .password(user.getPasswordHash())
                .roles(user.getRole())
                .build();
    }
}

loadUserByUsernameの中でデータベースを検索し、Spring Securityが理解できるUserDetailsオブジェクトに変換して返す、というのがこのクラスの役割です。

手順2:自作のログイン画面を指定する

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/login", "/css/**").permitAll()
                .anyRequest().authenticated()
            )
            .formLogin(form -> form
                .loginPage("/login")
                .loginProcessingUrl("/login")
                .defaultSuccessUrl("/dashboard", true)
                .failureUrl("/login?error")
                .permitAll()
            );

        return http.build();
    }
}

loginPageは画面表示用のURL、loginProcessingUrlはフォーム送信を受け付けるURLで、この2つを混同すると正しく動作しません。

.permitAll()をformLoginの設定内で呼ぶことで、ログイン画面自体には未認証でもアクセスできるようになります。

手順3:ログイン画面のテンプレートを用意する

<!-- login.html(Thymeleaf) -->
<form th:action="@{/login}" method="post">
    <div th:if="${param.error}">メールアドレスまたはパスワードが違います</div>
    <input type="text" name="username" placeholder="メールアドレス" />
    <input type="password" name="password" placeholder="パスワード" />
    <button type="submit">ログイン</button>
</form>

Spring Securityはデフォルトで、フォームの入力項目名をusernamepasswordとして受け取る仕様になっています。

つまずきやすい設定・注意点

name="username"name="email"のように変えてしまうと、Spring Securityは値を受け取れず、常に認証失敗になります。

筆者は実際に、フォームのname属性を「メールアドレスなのだから」とemailに変えてしまい、何度試してもログインできないというトラブルに数時間はまったことがあります。

原因はSpring Securityが探している項目名とフォームの項目名が一致していなかったことでした。

項目名をどうしても変えたい場合は、.usernameParameter("email")のように明示的に設定する必要があります。

.formLogin(form -> form
    .loginPage("/login")
    .usernameParameter("email")
    .passwordParameter("password")
)

よくあるつまずきポイント・エラー対処

❌ Before:ログインページ自体が認証を要求してしまう

http.authorizeHttpRequests(auth -> auth
    .anyRequest().authenticated()
)
.formLogin(form -> form.loginPage("/login"));

/loginpermitAll()に含めていないため、ログイン画面を表示しようとするとログイン画面にリダイレクトされる、という無限ループが発生します。

✅ After:ログインページを明示的に許可する

http.authorizeHttpRequests(auth -> auth
    .requestMatchers("/login").permitAll()
    .anyRequest().authenticated()
)
.formLogin(form -> form.loginPage("/login"));

自作のログイン画面を使う場合、必ずそのURLをpermitAll()の対象に含める、と覚えておいてください。

応用・一歩先の使い方

ログアウト処理を設定する

ログアウトもHttpSecurityの中でまとめて設定できます。

http.logout(logout -> logout
    .logoutUrl("/logout")
    .logoutSuccessUrl("/login?logout")
    .invalidateHttpSession(true)
    .deleteCookies("JSESSIONID")
);

invalidateHttpSession(true)はデフォルトで有効ですが、明示しておくとセッション情報が確実に破棄されることが読み取りやすくなります。

ログイン後にユーザー情報を取得する

ログイン中のユーザー情報は、Controllerの引数に@AuthenticationPrincipalを付けるだけで取得できます。

@GetMapping("/dashboard")
public String dashboard(@AuthenticationPrincipal UserDetails userDetails, Model model) {
    model.addAttribute("username", userDetails.getUsername());
    return "dashboard";
}

SecurityContextHolderから自分で取り出すよりも簡潔に書けるため、Controllerでは基本的にこちらを使うとよいでしょう。

まとめ

この記事のポイント

  • UserDetailsServiceを実装し、データベースのユーザー情報を認証に使う
  • loginPageloginProcessingUrlは役割が異なる
  • ログイン画面のフォーム項目名はusernamepasswordがデフォルト
  • ログイン画面のURLは必ずpermitAll()に含める

次に読むべき記事

UserDetailsServiceの中で扱ったパスワードは、平文ではなく暗号化して保存する必要があります。

→ 次の記事:パスワードエンコーダ(BCrypt)の使い方

タグ: #SpringBoot #中級者向け #認証

タイトルとURLをコピーしました