0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

はじめに

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

ざっくり概要

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

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

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

今回はユーザーを有効化していない際に起こる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は認証時に起こる例外に実装されているインターフェースです。

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

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に指定しました。

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に指定します。

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に指定します。

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を指定できるので、より拡張性が高いと感じました。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?