Spring Security5.7の書き方（Kotlin × Spring Boot

kotlin × spring環境でアプリケーションの実装を行ってます。
なかなか文献が少ない構成ではあるので色々公開していきたいと思ってます。

今回はspring security周りの実装です

# 前提
言語はkotlin
spring boot 2.7.1
spring security 5.7.2

APIはアクセストークンを使って認証し、結果をJSONで返却するような感じです
MVC側は ID / password で認証しセッション管理してます

# API

```kotlin
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.autoconfigure.security.servlet.PathRequest
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.http.HttpMethod
import org.springframework.security.authentication.AccountStatusUserDetailsChecker
import org.springframework.security.authentication.AuthenticationManager
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.http.SessionCreationPolicy
import org.springframework.security.core.userdetails.AuthenticationUserDetailsService
import org.springframework.security.web.SecurityFilterChain
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken


@Configuration
@EnableWebSecurity
class SecurityConfig{
    @Autowired
    lateinit var authenticationEntryPoint: AuthenticationEntryPoint

    @Bean
     fun securityFilterChain(
        http: HttpSecurity,
        authenticationManager: AuthenticationManager
    ): SecurityFilterChain {
        http.csrf().disable()
        http.authorizeHttpRequests {
                it
                .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
                // 認証を必要としないパスはmvcMatchers("xx").permitAll()として定義する
                .mvcMatchers("/api/health-check").permitAll()
                // 同じendpointでもPOSTメソッドのみ認証が必要ないケースなどはhttpメソッドの指定も可能
                .mvcMatchers(HttpMethod.POST,"/api/tokens").permitAll()
                .anyRequest().authenticated()
            }
            // 詳しく解説しませんが、独自認証処理がここで設定されてます
            // Authorizationヘッダーからトークンとって、DB照会してみたいな処理です
            .addFilter(preAuthenticatedProcessingFilter(authenticationManager))
            // Filter実行中に発生したExceptionはここでキャッチして、定形のレスポンスを返却する
            // AuthenticationEntryPointクラスを継承した独自クラスがあります
            .exceptionHandling { ex ->
                ex.authenticationEntryPoint(authenticationEntryPoint)
            }
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        return http.build()
    }

    // 以下の定義はpreAuthenticatedProcessingFilter関連に必要なもの
    @Bean
    fun authenticationUserDetailsService(): AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken> {
        // ここで、AuthenticationUserDetailsServiceを継承した自前クラスを返却
        return AuthenticationUserService()
    }
    @Bean
    fun preAuthenticatedAuthenticationProvider(): PreAuthenticatedAuthenticationProvider {
        return PreAuthenticatedAuthenticationProvider().apply {
            setPreAuthenticatedUserDetailsService(authenticationUserDetailsService())
            setUserDetailsChecker(AccountStatusUserDetailsChecker())
        }
    }

    fun preAuthenticatedProcessingFilter(authenticationManager: AuthenticationManager): AbstractPreAuthenticatedProcessingFilter {
        return PreAuthenticatedProcessingFilter().apply {
            setAuthenticationManager(authenticationManager)
        }
    }

    @Bean
    fun authenticationManager(authenticationConfiguration: AuthenticationConfiguration): AuthenticationManager? {
        return authenticationConfiguration.authenticationManager
    }
```

# MVC

```kotlin
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.autoconfigure.security.servlet.PathRequest
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.authentication.AuthenticationProvider
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.web.SecurityFilterChain
import org.springframework.security.web.authentication.AuthenticationFailureHandler
import org.springframework.security.web.csrf.CsrfFilter
import org.springframework.web.filter.CharacterEncodingFilter


@Configuration
@EnableWebSecurity
class SecurityConfig {
    @Autowired
    lateinit var provider: AuthenticationProvider

    @Bean
    fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
        http
            // ログイン画面の定義
            .formLogin{
                // ログインの初期表示ページのパス
                it.loginPage("/admin/login")
                // 実際のログイン処理を行うパス
                .loginProcessingUrl("/admin/login")
                // ログイン情報に誤りがあるなどの場合にどのような振る舞いをするかを独自処理を差し込み
                .failureHandler(customAuthenticationFailureHandler())
                // ログイン画面のinputタグname名を指定してあげる
                .usernameParameter("login_id").passwordParameter("password")
                // ログインが成功した場合に遷移するパス
                .defaultSuccessUrl("/admin/home")
                .permitAll()
            }
            // ログアウト処理の定義
            .logout{
                it.logoutUrl("/admin/logout")
                .logoutSuccessUrl("/admin/login")
                .permitAll()
            }
            .authorizeRequests {
                // 静的リソースへのアクセスは認証を必要としない設定
                it.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
                .mvcMatchers("/img/**", "/admin/health-check").permitAll()
                // ログイン画面自体も認証は必要ない
                .mvcMatchers("/", "/admin/login").permitAll()
                .anyRequest().authenticated()
                // ここでも詳しく解説しませんが、独自認証処理がここで設定されてます
                // ログインIDとパスワード使ってDB照会してみたいなことをAuthenticationProviderを継承したクラスで実装してます
                .and().authenticationProvider(provider)
            }
            .addFilterBefore(
                CharacterEncodingFilter().apply {
                    this.encoding = "UTF-8"
                    this.setForceEncoding(true)
                },
                CsrfFilter::class.java
            )
        return http.build()
    }

    @Bean
    fun customAuthenticationFailureHandler(): AuthenticationFailureHandler? {
        return CustomAuthenticationFailureHandler()
    }
}
```

# 最後に

https://qiita.com/suke_masa/items/908805dd45df08ba28d8

こちらの記事にもある通り、WebSecurityConfigurerAdapterを使った書き方が非推奨となり移行作業等々やりました。
kotlin使ってるのもあってか、かなり記述がスッキリかつまとまりがあるようになりました！（良かった

mvcMatchersではなく、antMatchersというのもあるのですが、この記事を書くにあたって調べているとどうやらmvcMatchersのほうが安全でよいみたいですね
ぐぐるとすぐでてくるので気になる方は見てみると良いと思います！

あと、java系の記事を調べているとパッケージ名が書いてないことがあり、別のものをimportしていてハマったりしました
なので今回全部載せるようにしたのですがいかがでしょう（うざいですかね・・？

「もっと良い書き方があるよ」などのコメントいただけると嬉しいです！