Spring SecurityでWeb APIのAPIキー認証を作成する

# やりたいこと
Spring SecurityでWeb API呼び出し時などで、リクエスト毎にヘッダのAPIキーで認証する。

Javaによる例は個人ブログの記事[【Spring】Spring Securityを使ってWeb APIの認可を行う](https://sebenkyo.com/2020/07/22/post-1186/)に載せています。この記事では@PreAuthorizeによる権限チェックの例も載せています。

# 実現方法
1. セッションを使用せずリクエスト毎に認証を実施する設定を行う
2. リクエストヘッダ中のAPIキーによる認証処理を行うためのフィルター、サービスを作成する
3. 2.で作成したフィルター、サービスを登録

# 1.リクエスト毎に認証を実施する設定
WebSecurityConfigurerAdapterのconfigureメソッドでセッションのポリシーをステートレスに設定する。
具体的には以下のようにSessionCreationPolicy.STATELESSを設定する。

```kotlin
@Configuration
@EnableWebSecurity
class SecurityConfig: WebSecurityConfigurerAdapter() {

    override fun configure(http: HttpSecurity?) {
        http
                ?.authorizeRequests()
                ?.antMatchers("/hello/**")?.hasAnyRole("USER", "ADMIN")
                ?.antMatchers("/admin/**")?.hasRole("ADMIN")
                ?.anyRequest()
                ?.authenticated()
                ?.and()
                ?.addFilter(preAuthenticatedProcessingFilter())
                ?.exceptionHandling()
                ?.authenticationEntryPoint(http401UnauthorizedEntryPoint())

        // リクエスト毎に認証を実施
        http?.sessionManagement()?.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
    }
    ....
```

# 2. リクエストヘッダ中のAPIキーによる認証処理を作成
Spring Securityの[Pre-Authentication](https://docs.spring.io/spring-security/site/docs/4.2.x-SNAPSHOT/reference/html/preauth.html)を使用する。AbstractPreAuthenticatedProcessingFilterとAuthenticationUserDetailsServiceを使用しリクエストヘッダーからAPIキーを取得するフィルターとAPIキーによる認証を行うサービスを作成する。

## フィルターの作成
AbstractPreAuthenticatedProcessingFilterを継承したクラスを作成し、getPreAuthenticatedCredentialsメソッドにリクエストヘッダ中のAPIキーを取り出す処理を作成する。

 ```kotlin
 class MyPreAuthenticatedProcessingFilter: AbstractPreAuthenticatedProcessingFilter() {

    // APIキーを取得
    override fun getPreAuthenticatedCredentials(request: HttpServletRequest?): Any {
        return request?.getHeader("X-Auth-Token") ?: ""
    }

    override fun getPreAuthenticatedPrincipal(request: HttpServletRequest?): Any {
        return ""
    }
}
 ```
  
## サービスの作成  
以下のようにAuthenticationUserDetailsServiceを継承したクラスを作成し、取得したAPIキーによる認証処理を作成する。
型パラメータにはPreAuthenticatedAuthenticationTokenを指定する。
ここでは、テストのため適当な処理を書いているが、外部APIやDBを使ってAPIキーの検証する処理を書く。

```kotlin
class MyAuthenticationUserDetailService: AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken> {

    override fun loadUserDetails(token: PreAuthenticatedAuthenticationToken?): UserDetails {
    
        // MyPreAuthenticatedProcessingFilterで取得したAPIキー
        val credential = token?.credentials
        
        if (credential.toString() == "") {
            throw UsernameNotFoundException("User not found")
        }

        return when (credential) {
            "token1" -> User("user", "", AuthorityUtils.createAuthorityList("ROLE_USER"))
            "token2" -> User("admin", "", AuthorityUtils.createAuthorityList("ROLE_ADMIN"))
            else -> User("user", "", AuthorityUtils.createAuthorityList("ROLE_INVALID") )
        }
    }
}
```

# 3. フィルター、サービスを登録
作成したフィルータ、サービスがDIされるように以下のように　Bean定義を追加する。

```kotlin
@Configuration
@EnableWebSecurity
class SecurityConfig: WebSecurityConfigurerAdapter() {

    override fun configure(http: HttpSecurity?) {
        http
                ?.authorizeRequests()
                ?.antMatchers("/hello/**")?.hasAnyRole("USER", "ADMIN")
                ?.antMatchers("/admin/**")?.hasRole("ADMIN")
                ?.anyRequest()
                ?.authenticated()
                ?.and()
                ?.addFilter(preAuthenticatedProcessingFilter())

        http?.sessionManagement()?.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
    }

    // サービス
    @Bean
    fun authenticationUserDetailsService(): AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken> {
        return MyAuthenticationUserDetailService()
    }

    // フィルター登録
    @Bean
    fun preAuthenticatedAuthenticationProvider(): PreAuthenticatedAuthenticationProvider {
        return PreAuthenticatedAuthenticationProvider().apply {
            setPreAuthenticatedUserDetailsService(authenticationUserDetailsService())
            setUserDetailsChecker(AccountStatusUserDetailsChecker())
        }
    }

    // フィルター
    @Bean
    fun preAuthenticatedProcessingFilter(): AbstractPreAuthenticatedProcessingFilter {
        return MyPreAuthenticatedProcessingFilter().apply {
            setAuthenticationManager(authenticationManager())
        }
    }
}
```