SpringSecurityのログイン認証におけるエラーハンドリング

## はじめに

ログイン認証時に発生した例外によって表示するViewを変えようと思い、改めてSpring Securityについて勉強したので備忘録としてまとめます。

## ざっくり概要

認証時の例外ハンドリングをするにはいくつか方法があります。

1. SimpleUrlAuthenticationFailureHandlerを継承したクラスを作成、onAuthenticationFailure()メソッドをOverrideして発生したExceptionに応じてリダイレクト先を変更
2. ExceptionMappingAuthenticationFailureHandlerを定義、Exceptionとそれに応じたリダイレクト先を設定
3. SecurityConfigのformLoginを定義、Exceptionとそれに応じたHandlerを設定

今回のようにシンプルな場合は、２がベストプラクティスのような気がしています。

今回はユーザーを有効化していない際に起こるDisabledExceptionが起きた場合のみ専用のViewを表示させ、それ以外は元のログイン画面に戻すように実装します。

## SimpleUrlAuthenticationFailureHandlerを継承するver

認証時の例外ハンドリングを行うインターフェースAuthenticationFailureHandlerを実装しているクラスの一つがSimpleUrlAuthenticationFailureHandlerです。
認証時に例外が発生すると、onAuthenticationFailure()メソッドが呼び出され、setDefaultFailureUrl()メソッドで指定したURLにリダイレクトされます。
SpringSecurityのformLoginで指定する.failureUrl()で指定しているURLはこのクラスによってリダイレクトされます。

似たハンドラーにForwardAuthenticationFailureHandlerがあります。
こちらのonAuthenticationFailure()メソッドが呼び出されると指定したURLにフォワードされます。
SpringSecurityのformLoginで指定する.failureForwardUrl()で指定しているURLはこのクラスによってフォワードされます。

ブラウザに表示されるURLは変わってほしいので、リダイレクトを行うSimpleUrlAuthenticationFailureHandlerを使用することにしました。

### ハンドリング方法

onAuthenticationFailure()メソッドは引数で HttpServletRequest HttpServletResponse AuthenticationException 指定します。
AuthenticationExceptionは認証時に起こる例外に実装されているインターフェースです。

以下のように実装しました。

```java:CustomAuthenticationFailureHandler

@Component
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler{

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
            AuthenticationException exception) throws IOException, ServletException {
        if(exception instanceof DisabledException){
            setDefaultFailureUrl("/user/unAuthorized");
        } else {
            setDefaultFailureUrl("/user/toLogin?error");
        }
        super.onAuthenticationFailure(request, response, exception);
    }
}

```

@Componentアノテーションを貼っているので、SecurityConfigで@Autowiredして failureHandlerに指定しました。

```java:CustomAuthenticationFailureHandler

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {

@Autowired
private CustomAuthenticationFailureHandler handler;

@Bean
    public SecurityFilterChain SecurityFilterChain(HttpSecurity http) throws Exception {
        
        http.formLogin(login -> login
                .loginProcessingUrl("/login")
                .loginPage("/user/toLogin")
                .defaultSuccessUrl("/top")
                .failureHandler(handler)
                .permitAll()
        ).logout(logout -> logout
                .logoutSuccessUrl("/top")
                .clearAuthentication(true)
        ).authorizeHttpRequests(authz -> authz
                .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
                .requestMatchers("/").permitAll() 
                .requestMatchers("/user/**").hasAnyRole("USER", "ADMIN")
                .requestMatchers("/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
        );
        return http.build();
    }
}

```

強引ですがこれでハンドリング可能でした。

## ExceptionMappingAuthenticationFailureHandlerを使うver

ExceptionMappingAuthenticationFailureHandlerはSimpleUrlAuthenticationFailureHandlerを継承したハンドラーです。
setExceptionMappings()メソッドで設定された内容に応じて、例外にあったURLにリダイレクトします。

### ハンドリング方法

SecurityConfigで例外とURLを指定したMapをセットしたExceptionMappingAuthenticationFailureHandlerを返すメソッドを作成し、failureHandlerに指定します。

```java:CustomAuthenticationFailureHandler

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {

@Autowired
private CustomAuthenticationFailureHandler handler;

    @Bean
    public SecurityFilterChain SecurityFilterChain(HttpSecurity http) throws Exception {
        
        http.formLogin(login -> login
                .loginProcessingUrl("/login")
                .loginPage("/user/toLogin")
                .defaultSuccessUrl("/top")
                .failureHandler(exceptionHandler())
                .permitAll()
        ).logout(logout -> logout
                .logoutSuccessUrl("/top")
                .clearAuthentication(true)
        ).authorizeHttpRequests(authz -> authz
                .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
                .requestMatchers("/").permitAll() 
                .requestMatchers("/user/**").hasAnyRole("USER", "ADMIN")
                .requestMatchers("/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
        );
        return http.build();
    }

    public ExceptionMappingAuthenticationFailureHandler exceptionHandler(){
        ExceptionMappingAuthenticationFailureHandler handler = new ExceptionMappingAuthenticationFailureHandler();
        Map<String,String> urls = new HashMap<>();
        urls.put(DisabledException.class.getName(), "/user/unAuthorized");
        handler.setExceptionMappings(urls);
        handler.setDefaultFailureUrl("/user/toLogin?error");
        return handler;
    }
}

```

ハンドリングしたい例外のみをMapにすればOKです。
ExceptionMappingAuthenticationFailureHandlerのonAuthenticationFailureメソッド内部では発生した例外名でgetをし、URLが見つかればそこへリダイレクト、nullだった場合はデフォルトのURLにリダイレクトするようになっています。


## DelegatingAuthenticationFailureHandlerを使用するver

DelegatingAuthenticationFailureHandlerはAuthenticationFailureHandlerを実装したハンドラーです。
ExceptionMappingAuthenticationFailureHandlerと似ていますが、こちらは例外に応じてハンドラーを切り替えます。

### ハンドリング方法

SecurityConfigで例外とハンドラーを指定したMapをセットしたDelegatingAuthenticationFailureHandlerを返すメソッドを作成し、failureHandlerに指定します。

```java:CustomAuthenticationFailureHandler

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {

@Autowired
private CustomAuthenticationFailureHandler handler;

    @Bean
    public SecurityFilterChain SecurityFilterChain(HttpSecurity http) throws Exception {
        
        http.formLogin(login -> login
                .loginProcessingUrl("/login")
                .loginPage("/user/toLogin")
                .defaultSuccessUrl("/top")
                .failureHandler(exceptionHandler())
                .permitAll()
        ).logout(logout -> logout
                .logoutSuccessUrl("/top")
                .clearAuthentication(true)
        ).authorizeHttpRequests(authz -> authz
                .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
                .requestMatchers("/").permitAll() 
                .requestMatchers("/user/**").hasAnyRole("USER", "ADMIN")
                .requestMatchers("/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
        );
        return http.build();
    }

    public SimpleUrlAuthenticationFailureHandler defaultHandler(){
        return new SimpleUrlAuthenticationFailureHandler("/user/toLogin?error");
    }

    public SimpleUrlAuthenticationFailureHandler unAuthorizedHandler(){
        return new SimpleUrlAuthenticationFailureHandler("/user/unAuthorized");
    }

    public DelegatingAuthenticationFailureHandler handler(){
        LinkedHashMap<Class<? extends AuthenticationException>, AuthenticationFailureHandler> handlers = new LinkedHashMap<>();
        handlers.put(DisabledException.class, unAuthorizedHandler());
        return new DelegatingAuthenticationFailureHandler(handlers, defaultHandler());
    }
}

```

ハンドリングしたい例外のみをMapにすればOKです。
DelegatingAuthenticationFailureHandlerのonAuthenticationFailureメソッド内部ではセットされたMapを拡張for文で回し、例外が一致すると設定したHandlerを使用してリダイレクト、一致するものがなかった場合はデフォルトのHandlerを使用してリダイレクトするようになっています。

こちらの場合はHandlerを指定できるので、より拡張性が高いと感じました。