使用 @ExceptionHandler 處理 Spring 安全異常
一、概述
在本教程中,我們將學習如何使用@ExceptionHandler
和@ControllerAdvice.
**控制器通知是一個攔截器,**它允許我們在整個應用程序中使用相同的異常處理。
2. Spring 安全異常
AuthenticationException
和AccessDeniedException
等 Spring 安全核心異常是運行時異常。由於這些異常是由DispatcherServlet
後面的身份驗證過濾器引發的,並且在調用控制器方法之前, @ControllerAdvice
ControllerAdvice 將無法捕獲這些異常。
Spring 安全異常可以通過添加自定義過濾器和構造響應體來直接處理。要通過@ExceptionHandler
和@ControllerAdvice,
我們需要AuthenticationEntryPoint
的自定義實現。 AuthenticationEntryPoint
用於發送從客戶端請求憑據的 HTTP 響應。儘管安全入口點有多種內置實現,但我們需要編寫自定義實現來發送自定義響應消息。
首先,讓我們看看在不使用@ExceptionHandler
的情況下全局處理安全異常。
3.沒有@ExceptionHandler
Spring 安全異常從AuthenticationEntryPoint
開始。讓我們為AuthenticationEntryPoint
編寫一個實現來攔截安全異常。
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 extends WebSecurityConfigurerAdapter {
@Autowired
@Qualifier("customAuthenticationEntryPoint")
AuthenticationEntryPoint authEntryPoint;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.requestMatchers()
.antMatchers("/login")
.and()
.authorizeRequests()
.anyRequest()
.hasRole("ADMIN")
.and()
.httpBasic()
.and()
.exceptionHandling()
.authenticationEntryPoint(authEntryPoint);
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("admin")
.password("password")
.roles("ADMIN");
}
}
3.3.配置休息控制器
現在,讓我們編寫一個監聽這個端點'/login'的休息控制器:
@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 extends WebSecurityConfigurerAdapter {
@Autowired
@Qualifier("delegatedAuthenticationEntryPoint")
AuthenticationEntryPoint authEntryPoint;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.requestMatchers()
.antMatchers("/login-handler")
.and()
.authorizeRequests()
.anyRequest()
.hasRole("ADMIN")
.and()
.httpBasic()
.and()
.exceptionHandling()
.authenticationEntryPoint(authEntryPoint);
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("admin")
.password("password")
.roles("ADMIN");
}
}
對於“ /login-handler
”端點,我們已經使用上面實現的DelegatedAuthenticationEntryPoint
配置了異常處理程序。
4.4.配置休息控制器
讓我們為 ' /login-handler
' 端點配置剩餘控制器:
@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. 結論
在本文中,我們學習瞭如何**使用@ExceptionHandler
全局處理 Spring Security 異常**。此外,我們創建了一個功能齊全的示例,幫助我們理解所解釋的概念。
本文的完整源代碼可在 GitHub 上獲得。