はじめに
ログイン認証時に発生した例外によって表示するViewを変えようと思い、改めてSpring Securityについて勉強したので備忘録としてまとめます。
ざっくり概要
認証時の例外ハンドリングをするにはいくつか方法があります。
- SimpleUrlAuthenticationFailureHandlerを継承したクラスを作成、onAuthenticationFailure()メソッドをOverrideして発生したExceptionに応じてリダイレクト先を変更
- ExceptionMappingAuthenticationFailureHandlerを定義、Exceptionとそれに応じたリダイレクト先を設定
- 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は認証時に起こる例外に実装されているインターフェースです。
以下のように実装しました。
@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に指定しました。
@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に指定します。
@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に指定します。
@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を指定できるので、より拡張性が高いと感じました。