1. 概述
本教程將學習如何使用 <em @ExceptionHandler</em> 和 <em @ControllerAdvice</em> 全局處理 Spring Security 異常。<strong>控制器建議(Controller Advice)是一種攔截器,它允許我們在應用程序中共享相同的異常處理邏輯。</strong>
2. Spring Security 異常
Spring Security 核心異常,如 <em >AuthenticationException</em > 和 <em >AccessDeniedException</em >>,是運行時異常。由於這些異常由位於DispatcherServlet之後,在調用控制器方法之前執行的身份驗證過濾器拋出,因此 `@ControllerAdvice> 無法捕獲這些異常。
Spring Security 異常可以直接通過添加自定義過濾器和構建響應主體來處理。要通過 <em >@ExceptionHandler</em >> 和@ControllerAdvice> 在全局級別處理這些異常,我們需要一個自定義的 <em >AuthenticationEntryPoint</em >> 實現。AuthenticationEntryPoint> 用於發送一個 HTTP 響應,要求客户端提供憑據。 儘管存在多個內置實現用於安全入口點,但我們需要為發送自定義響應消息而編寫自定義實現。
讓我們首先看看在不使用 `@ExceptionHandler> 的情況下全局處理安全異常。
3. 不使用 @ExceptionHandler
Spring Security 異常在 AuthenticationEntryPoint 中啓動。 讓我們編寫一個實現,該實現攔截 Security 異常,該實現將攔截 Security 異常。
3.1. 配置 AuthenticationEntryPoint
讓我們實現 AuthenticationEntryPoint 並覆蓋 commence() 方法:
@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();
}
}在此,我們使用了 ObjectMapper 作為響應體的消息轉換器。
3.2. 配置 SecurityConfig
接下來,我們將配置 SecurityConfig 以攔截路徑進行身份驗證。在這裏,我們將將 ‘/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 控制器
現在,讓我們編寫一個監聽該端點 ‘/login’ 的 Rest 控制器:
@PostMapping(value = "/login", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<RestResponse> login() {
return ResponseEntity.ok(new RestResponse("Success"));
}
3.4. 測試
最後,讓我們使用模擬測試來測試此端點。
首先,讓我們為成功的身份驗證編寫一個測試用例:
@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())));
}
現在,讓我們看看如何使用 @ControllerAdvice 和 @ExceptionHandler 達到相同的效果。
4. 使用 @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 控制器
以下配置 Rest 控制器,用於 ‘login-handler</em/>’ 端點:
@PostMapping(value = "/login-handler", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<RestResponse> loginWithExceptionHandler() {
return ResponseEntity.ok(new RestResponse("Success"));
}
4.5. 測試
現在讓我們測試此端點:
@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 註解。此外,我們還創建了一個完整的示例,幫助我們理解所解釋的概念。