概述
在这个教程中,我们将学习如何使用@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
,并重写commence()
方法:
@Component
public class CustomAuthenticationEntryPoint implements 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控制器
现在,编写一个监听此端点/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())));
}
现在,我们将了解如何使用@ExceptionHandler
和@ControllerAdvice
实现相同的功能。
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控制器
为/login-handler
端点配置REST控制器:
@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上找到:点击此处。