Spring Boot 3 x Spring Security 6 でログイン画面を複数作成する際のSecurityFilterChainの書き方

## 概要

ユーザー向け画面と管理画面の２つを作成したいケースがあると思います。
その際に、SecurityFilterChainの書き方で迷ったので備忘します。
SpringBoot3(SpringSecurity6)の記事が少なかった点と、
SpringBoot2の時と比べて差異があったため、記事にしました。

## まとめ

*  `SecurityFilterChain` で `securityMatcher` を利用する
*  `Order` の優先順位に注意する。

## サンプルコード

ユーザー向け画面のSecurityFilterChain。

```java
public class UserSecurityConfig {

    @Bean
    @Order(1)
    public SecurityFilterChain userSecurityFilterChain(HttpSecurity http) throws Exception {

        // URL認可
        http.securityMatcher("/user/**").authorizeHttpRequests((request) -> request

                // 常に許可
                .requestMatchers("/user/login").permitAll()
                .requestMatchers("/user/certification").permitAll()

                // ユーザー権限のみ許可
                .requestMatchers("/user/home").hasAuthority("USER")
        )

        // ログイン
        .formLogin(formLogin -> formLogin
                .loginPage("/user/login")
                .loginProcessingUrl("/user/certification")
                .defaultSuccessUrl("/user/home")
                .failureUrl("/user/login?error")
        )

        // ログアウト
        .logout(logout -> logout
                .logoutUrl("/user/logout")
                .logoutSuccessUrl("/user/login?logout=success")
                .deleteCookies("JSESSIONID")   // ログアウト後にCookie'JSESSIONID'を削除
                .invalidateHttpSession(true)   // ログアウト後にセッション無効化
        )

        return http.build();
    }
}
```

管理者向け画面のSecurityFilterChain。
`Order`の値を変更して優先度を整理する。

```java
public class AdminSecurityConfig {

    @Bean
    @Order(2)
    public SecurityFilterChain adminSecurityFilterChain(HttpSecurity http) throws Exception {

        // URL認可
        http.securityMatcher("/admin/**").authorizeHttpRequests((request) -> request

                // 常に許可
                .requestMatchers("/admin/login").permitAll()
                .requestMatchers("/admin/certification").permitAll()

                // 管理者権限のみ許可
                .requestMatchers("/admin/home").hasAuthority("ADMIN")
        )

        // ログイン
        .formLogin(formLogin -> formLogin
                .loginPage("/admin/login")
                .loginProcessingUrl("/admin/certification")
                .defaultSuccessUrl("/admin/home")
                .failureUrl("/admin/login?error")
        )

        // ログアウト
        .logout(logout -> logout
                .logoutUrl("/admin/logout")
                .logoutSuccessUrl("/admin/login?logout=success")
                .deleteCookies("JSESSIONID")   // ログアウト後にCookie'JSESSIONID'を削除
                .invalidateHttpSession(true)   // ログアウト後にセッション無効化
        )

        return http.build();
    }
}

```

ロールを付与するクラスを用意する。

```java
@Component
public class AuthenticationSuccessEventListener {

    @EventListener
    public void onInteractiveAuthenticationSuccessEvent(InteractiveAuthenticationSuccessEvent event) {

        // 認証情報を取得
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        UserDetails principal = (UserDetails) authentication.getPrincipal();
        Object credentials = authentication.getCredentials();

        // 権限情報を設定
        List<SimpleGrantedAuthority> authorities = new ArrayList<>();
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
        if (request.getServletPath().startsWith("/user")) {
            authorities.add(new SimpleGrantedAuthority("USER"));
        } else if (request.getServletPath().startsWith("/admin")) {
            authorities.add(new SimpleGrantedAuthority("ADMIN"));
        }

        // 認証情報を再設定
        Authentication newAuthentication = new UsernamePasswordAuthenticationToken(
                principal,
                credentials,
                authorities);
        SecurityContextHolder.getContext().setAuthentication(newAuthentication);
    }
}
```