使用 @ExceptionHandler 处理 Spring Security 异常
最后更新:2024年5月11日
1. 概述
在本教程中,我们将学习如何使用 @ExceptionHandler 和 @ControllerAdvice 全局处理 Spring security 异常。控制器建议是一种拦截器,允许我们在整个应用程序中使用相同的异常处理。
2. Spring Security 异常
Spring security 核心异常,例如 AuthenticationException 和 AccessDeniedException 都是运行时异常。由于这些异常由在DispatcherServlet之后、调用控制器方法之前身份验证过滤器抛出,因此@ControllerAdvice 无法捕获这些异常。
Spring security 异常可以通过添加自定义过滤器并构造响应主体来直接处理。要通过@ExceptionHandler 和 @ControllerAdvice 在全局级别处理这些异常,我们需要自定义实现 AuthenticationEntryPoint。AuthenticationEntryPoint 用于发送请求客户端提供凭据的 HTTP 响应。虽然存在多种内置的安全入口点实现,但我们需要编写自定义实现以发送自定义响应消息。
首先,让我们看看如何在不使用@ExceptionHandler的情况下全局处理安全异常。
3. 不使用 @ExceptionHandler
Spring security 异常在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 {
@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. 测试
最后,让我们使用模拟测试来测试此端点。
首先,让我们编写一个用于成功身份验证的测试用例
@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 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. 测试
现在让我们测试这个端点
@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 上获取。 一旦你以 Baeldung Pro 会员 身份登录,就开始学习并在项目上进行编码。















