Spring SecurityのBasic認証をちょっぴりカスタマイズする

# はじめに
Spring BootでWeb APIのアプリケーション構築するにあたり、APIの認証としてBasic認証を採用した。
Spring Securityを利用し、諸先輩方の有益な情報を参考にすることで、いとも簡単にBasic認証を実現できたのだが、認証処理をほんの少しだけカスタマイズしたくなり、いろいろ試してみた。

# 環境
Java: 1.8.0
Spring Framework: 5.1
Spring Boot: 2.1
Spring Security: 5.1
Lombok: 1.18

# 設定ファイルに登録した認証情報でBasic認証する
application.propertiesに設定したユーザーID・パスワードでBasic認証するSpring Securityの設定は、Spring Bootの[ConfigurationProperties](https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-external-config.html#boot-features-external-config-typesafe-configuration-properties)を使ってこんな感じで実装。

```Java:CustomSecurityConfigurer.java
@Configuration
@EnableWebSecurity
public class CustomSecurityConfigurer extends WebSecurityConfigurerAdapter {
    
    @Autowired
    private CustomAuthInfo authInfo; // application.propertiesで設定した認証情報

    @Override
    protected void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
        // application.propertiesで設定したユーザID・パスワードをinMemoryにもつ
        // application.propertiesに設定するパスワードはBCryptでハッシュ化しておく
        for (UserCredential credential: authInfo.getCredentials()) {
            authenticationManagerBuilder.inMemoryAuthentication().withUser(credential.getUserName())
            .password(credential.getPassword()).roles("USER");
        }
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {  
        http.httpBasic().realmName("Custom Realm"); // Basic 認証を有効にする
        http.authorizeRequests().anyRequest().authenticated(); // 全てのリクエスト対し、認証を行う
        http.csrf().disable(); // あえてCSRF対策は無効に設定
        // アクセスの都度、認証を行うこととし、セッション管理は行わない
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(); // パスワードはBCryptでハッシュ化する
    }
}
```
```Java:CustomAuthInfo.java
@Getter
@Setter
@Component
@ConfigurationProperties(prefix="custom-auth")
public class CustomAuthInfo {

    private List<UserCredential> credentials;
    
    @Getter
    @Setter
    public static class UserCredential {
        
        private String userName;      
        private String password; 
    }
}
```
```application.properties
custom-auth.credentials[0].user-name=user1
custom-auth.credentials[0].password=[BCryptでハッシュ化したパスワード]
custom-auth.credentials[1].user-name=user2
custom-auth.credentials[1].password=[BCryptでハッシュ化したパスワード]
custom-auth.credentials[2].user-name=user3
custom-auth.credentials[2].password=[BCryptでハッシュ化したパスワード]
```

# Spring Security の Basic認証フィルターをカスタマイズしてみる
認証成功時と失敗時に追加したい処理があったので、Spring Securityの[Post Processing](https://docs.spring.io/spring-security/site/docs/current/reference/html5/#post-processing-configured-objects)とやらを使って、[BasicAuthenticationFilter](https://docs.spring.io/spring-security/site/docs/current/reference/html5/#basic-processing-filter)をちょっと強引にカスタマイズしてみた。


```Java:CustomSecurityConfigurer.java
//[前略]
    private static final String CUSTOM_REALM = "Custom realm";

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // Basic 認証を有効にする
        // 認証フィルターは認証成功、失敗時の追加処理を実装したカスタムフィルターを使用するため、標準フィルターをカスタムフィルターに差し替える
        http.httpBasic().realmName(CUSTOM_REALM)
            .withObjectPostProcessor(new ObjectPostProcessor<BasicAuthenticationFilter>() {
                @SuppressWarnings("unchecked")
                @Override
                public BasicAuthenticationFilter postProcess(BasicAuthenticationFilter filter) {
                    return new CustomBasicAuthenticationFilter(
                                    http.getSharedObject(AuthenticationManager.class),
                                    authenticationEntryPoint());
                }

                private BasicAuthenticationEntryPoint authenticationEntryPoint() {
                    BasicAuthenticationEntryPoint authenticationEntryPoint =
                                    new BasicAuthenticationEntryPoint();
                    authenticationEntryPoint.setRealmName(CUSTOM_REALM);
                    return authenticationEntryPoint;
                }
            });
//[後略]
```

```Java:CustomBasicAuthenticationFilter.java
public class CustomBasicAuthenticationFilter extends BasicAuthenticationFilter {
    
    public CustomBasicAuthenticationFilter(AuthenticationManager authenticationManager) {
        
        super(authenticationManager);
    }

    public CustomBasicAuthenticationFilter(AuthenticationManager authenticationManager,
                    AuthenticationEntryPoint authenticationEntryPoint) {

        super(authenticationManager, authenticationEntryPoint);
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request,
            HttpServletResponse response, FilterChain chain)
                    throws IOException, ServletException {
        String header = request.getHeader("Authorization");
        // Basic認証のリクエストヘッダーが無ければ、認証失敗時の処理を実行
        if (header == null || !header.toLowerCase().startsWith("basic ")) {
            onUnsuccessfulAuthentication(request, response, null);
        }
        super.doFilterInternal(request, response, chain);
    }

    @Override
    protected void onSuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, Authentication authResult) throws IOException {

        // 認証成功時にやりたいことをゴニョゴニョする
    }

    @Override
    protected void onUnsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException {

        // 認証失敗時にやりたいことをゴニョゴニョする
    }
}
```

# その他に試してみたこと
- [RememberMeServices](https://docs.spring.io/spring-security/site/docs/current/reference/html5/#remember-me-impls)を利用する→目的外使用になっていたため不採用
- [addFilterAt](https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/config/annotation/web/builders/HttpSecurity.html#addFilterAt-javax.servlet.Filter-java.lang.Class-)で差し替える→addFilterAtは差し替えではなく併立だったので不採用
- Spring Frameworkの[OncePerRequestFilter](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/filter/OncePerRequestFilter.html)を継承していることに目をつけて、BeanNameを上書きして、[addFilterBefore](https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/config/annotation/web/builders/HttpSecurity.html#addFilterBefore-javax.servlet.Filter-java.lang.Class-)で差し替える→本来の意図と違う気がして不採用
- [addFilterBefore](https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/config/annotation/web/builders/HttpSecurity.html#addFilterBefore-javax.servlet.Filter-java.lang.Class-)、[addFilterAfter](https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/config/annotation/web/builders/HttpSecurity.html#addFilterAfter-javax.servlet.Filter-java.lang.Class-)で独自フィルターを追加する→大掛かりになりすぎるため不採用