1. 概述
在本教程中,我們將學習如何使用 @ExceptionHandler 和 @ControllerAdvice 全局處理 Spring Security 異常。ControllerAdvice 是一個攔截器,允許我們在應用程序中共享相同的異常處理。
2. Spring Security 異常
Spring Security 的核心異常,如 AuthenticationException 和 AccessDeniedException,是運行時異常。由於這些 異常由位於 DispatcherServlet 之後且在調用控制器方法之前執行的身份驗證過濾器拋出,因此 @ControllerAdvice 無法捕獲這些異常。
Spring Security 異常可以直接通過添加自定義過濾器和構建響應主體來處理。為了通過 @ExceptionHandler 和 @ControllerAdvice 在全局級別處理這些異常,我們需要自定義 AuthenticationEntryPoint 的實現。 AuthenticationEntryPoint 用於發送一個 HTTP 響應,要求客户端提供憑據。 儘管存在多個內置實現用於安全入口點,但我們需要編寫自定義實現來發送自定義響應消息。
首先,讓我們在不使用 @ExceptionHandler 的情況下處理安全異常。
3. Without
Spring security 異常在 啓動。 讓我們編寫一個攔截 security 異常的實現 。
3.1. 配置
讓我們實現 並覆蓋 方法:
@Component("customAuthenticationEntryPoint")
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)
throws IOException, ServletException {
RestError re = new RestError(HttpStatus.UNAUTHORIZED.toString(), "Authentication failed");
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
OutputStream responseStream = response.getOutputStream();
ObjectMapper mapper = new ObjectMapper();
mapper.writeValue(responseStream, re);
responseStream.flush();
}
}
這裏,我們使用了 作為響應體的消息轉換器。
3.2. 配置
接下來,讓我們配置 以攔截用於身份驗證的路徑。 我們將配置 ‘/login’ 作為上述實現的路徑。 此外,我們將配置 ‘admin’ 用户具有 ‘ADMIN’ 角色:
@Configuration
@EnableWebSecurity
public class CustomSecurityConfig {
@Autowired
@Qualifier("customAuthenticationEntryPoint")
AuthenticationEntryPoint authEntryPoint;
@Bean
public UserDetailsService userDetailsService() {
UserDetails admin = User.withUsername("admin")
.password("password")
.roles("ADMIN")
.build();
InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
userDetailsManager.createUser(admin);
return userDetailsManager;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/login")
.authenticated()
.anyRequest()
.hasRole("ADMIN"))
.httpBasic(basic -> basic.authenticationEntryPoint(authEntryPoint))
.exceptionHandling(Customizer.withDefaults());
return http.build();
}
}
3.3. 配置 Rest Controller
現在,讓我們編寫一個監聽該端點 ‘/login’ 的 rest controller:
@PostMapping(value = "/login", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<RestResponse> login() {
return ResponseEntity.ok(new RestResponse("Success"));
}
3.4. 測試
最後,讓我們使用 mock 測試測試此端點。
首先,讓我們編寫一個成功的身份驗證的測試用例:
@Test
@WithMockUser(username = "admin", roles = { "ADMIN" })
public void whenUserAccessLogin_shouldSucceed() throws Exception {
mvc.perform(formLogin("/login").user("username", "admin")
.password("password", "password")
.acceptMediaType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk());
}
接下來,讓我們看看失敗身份驗證的情況:
@Test
public void whenUserAccessWithWrongCredentialsWithDelegatedEntryPoint_shouldFail() throws Exception {
RestError re = new RestError(HttpStatus.UNAUTHORIZED.toString(), "Authentication failed");
mvc.perform(formLogin("/login").user("username", "admin")
.password("password", "wrong")
.acceptMediaType(MediaType.APPLICATION_JSON))
.andExpect(status().isUnauthorized())
.andExpect(jsonPath("$.errorMessage", is(re.getErrorMessage())));
}
現在,讓我們看看如何使用 和 達到相同的效果。
4. With @ExceptionHandler
這種方法允許我們使用相同的異常處理技術,但以更乾淨、更好的方式在控制器建議中使用帶有@ExceptionHandler>註解的方法。
4.1. 配置 AuthenticationEntryPoint
類似於上述方法,我們將實現AuthenticationEntryPoint,然後將異常處理委託給HandlerExceptionResolver:
@Component("delegatedAuthenticationEntryPoint")
public class DelegatedAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Autowired
@Qualifier("handlerExceptionResolver")
private HandlerExceptionResolver resolver;
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)
throws IOException, ServletException {
resolver.resolveException(request, response, null, authException);
}
}
在這裏,我們已注入DefaultHandlerExceptionResolver,並將處理程序委託給該解析器。此安全異常現在可以使用帶有異常處理方法的控制器建議進行處理。
4.2. 配置 ExceptionHandler
現在,對於主異常處理程序的配置,我們將擴展ResponseEntityExceptionHandler,並使用@ControllerAdvice註解此類:
@ControllerAdvice
public class DefaultExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler({ AuthenticationException.class })
@ResponseBody
public ResponseEntity<RestError> handleAuthenticationException(Exception ex) {
RestError re = new RestError(HttpStatus.UNAUTHORIZED.toString(),
"Authentication failed at controller advice");
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(re);
}
}
4.3. 配置 SecurityConfig
現在,讓我們為此委託的身份驗證入口點編寫安全配置:
@Configuration
@EnableWebSecurity
public class DelegatedSecurityConfig {
@Autowired
@Qualifier("delegatedAuthenticationEntryPoint")
AuthenticationEntryPoint authEntryPoint;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.requestMatchers()
.antMatchers("/login-handler")
.and()
.authorizeRequests()
.anyRequest()
.hasRole("ADMIN")
.and()
.httpBasic()
.and()
.exceptionHandling()
.authenticationEntryPoint(authEntryPoint);
return http.build();
}
@Bean
public InMemoryUserDetailsManager userDetailsService() {
UserDetails admin = User.withUsername("admin")
.password("password")
.roles("ADMIN")
.build();
return new InMemoryUserDetailsManager(admin);
}
}
對於“/login-handler”端點,我們已使用上述實現的DelegatedAuthenticationEntryPoint配置了異常處理程序。
4.4. 配置 Rest Controller
讓我們配置“/login-handler”端點的 rest controller:
@PostMapping(value = "/login-handler", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<RestResponse> loginWithExceptionHandler() {
return ResponseEntity.ok(new RestResponse("Success"));
}
4.5. Tests
現在讓我們測試此端點:
@Test
@WithMockUser(username = "admin", roles = { "ADMIN" })
public void whenUserAccessLogin_shouldSucceed() throws Exception {
mvc.perform(formLogin("/login-handler").user("username", "admin")
.password("password", "password")
.acceptMediaType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk());
}
@Test
public void whenUserAccessWithWrongCredentialsWithDelegatedEntryPoint_shouldFail() throws Exception {
RestError re = new RestError(HttpStatus.UNAUTHORIZED.toString(), "Authentication failed at controller advice");
mvc.perform(formLogin("/login-handler").user("username", "admin")
.password("password", "wrong")
.acceptMediaType(MediaType.APPLICATION_JSON))
.andExpect(status().isUnauthorized())
.andExpect(jsonPath("$.errorMessage", is(re.getErrorMessage())));
}
5. 結論
在本文中,我們學習瞭如何全局處理 Spring Security 異常,使用 @ExceptionHandler 機制。 此外,我們創建了一個完整的示例,幫助我們理解所解釋的概念。