1. Overview
1.概述
In this tutorial, we will learn how to globally handle Spring security exceptions with @ExceptionHandler and @ControllerAdvice. The controller advice is an interceptor that allows us to use the same exception handling across the application.
在本教程中,我们将学习如何使用@ExceptionHandler和@ControllerAdvice全局处理Spring安全异常。controller advice是一个拦截器,它允许我们在整个应用程序中使用相同的异常处理。
2. Spring Security Exceptions
2.Spring的安全例外情况
Spring security core exceptions such as AuthenticationException and AccessDeniedException are runtime exceptions. Since these exceptions are thrown by the authentication filters behind the DispatcherServlet and before invoking the controller methods, @ControllerAdvice won’t be able to catch these exceptions.
Spring安全核心异常,如AuthenticationException和AccessDeniedException是运行时异常。由于这些异常是由DispatcherServlet后面的认证过滤器在调用控制器方法之前抛出的,@ControllerAdvice将无法捕获这些异常。
Spring security exceptions can be directly handled by adding custom filters and constructing the response body. To handle these exceptions at a global level via @ExceptionHandler and @ControllerAdvice, we need a custom implementation of AuthenticationEntryPoint. AuthenticationEntryPoint is used to send an HTTP response that requests credentials from a client. Although there are multiple built-in implementations for the security entry point, we need to write a custom implementation for sending a custom response message.
Spring安全异常可以通过添加自定义过滤器和构建响应体来直接处理。为了通过@ExceptionHandler和@ControllerAdvice>在全局层面处理这些异常,我们需要自定义实现AuthenticationEntryPoint。AuthenticationEntryPoint用于发送请求客户端凭据的HTTP响应。虽然安全入口点有多个内置的实现,但我们需要编写一个自定义的实现来发送自定义的响应消息。
First, let’s look at handling security exceptions globally without using @ExceptionHandler.
首先,让我们看看在不使用@ExceptionHandler的情况下全局处理安全异常。
3. Without @ExceptionHandler
3.没有@ExceptionHandler
Spring security exceptions are commenced at the AuthenticationEntryPoint. Let’s write an implementation for AuthenticationEntryPoint which intercepts the security exceptions.
Spring的安全异常是在AuthenticationEntryPoint开始的。让我们为AuthenticationEntryPoint写一个实现,拦截安全异常。
3.1. Configuring AuthenticationEntryPoint
3.1.配置AuthenticationEntryPoint
Let’s implement the AuthenticationEntryPoint and override commence() method:
让我们实现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();
}
}
Here, we’ve used ObjectMapper as a message converter for the response body.
这里,我们使用ObjectMapper作为响应体的消息转换器。
3.2. Configuring SecurityConfig
3.2.配置SecurityConfig
Next, let’s configure SecurityConfig to intercept paths for authentication. Here we’ll configure ‘/login‘ as the path for the above implementation. Also, we’ll configure the ‘admin’ user with the ‘ADMIN’ role:
接下来,让我们配置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.requestMatchers()
.antMatchers("/login")
.and()
.authorizeRequests()
.anyRequest()
.hasRole("ADMIN")
.and()
.httpBasic()
.and()
.exceptionHandling()
.authenticationEntryPoint(authEntryPoint);
return http.build();
}
}
3.3. Configure the Rest Controller
3.3.配置休息控制器
Now, let’s write a rest controller listening to this endpoint ‘/login’:
现在,让我们写一个监听这个端点”/login “的休息控制器。
@PostMapping(value = "/login", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<RestResponse> login() {
return ResponseEntity.ok(new RestResponse("Success"));
}
3.4. Testing
3.4.测试
Finally, let’s test this endpoint with mock tests.
最后,让我们用模拟测试来测试这个端点。
First, let’s write a test case for a successful authentication:
首先,让我们写一个成功认证的测试案例。
@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());
}
Next, let’s look at a scenario with failed authentication:
接下来,让我们看一下认证失败的情况。
@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())));
}
Now, let’s see how we can achieve the same with @ControllerAdvice and @ExceptionHandler.
现在,让我们看看如何用@ControllerAdvice和@ExceptionHandler实现同样的效果。
4. With @ExceptionHandler
4.使用@ExceptionHandler
This approach allows us to use exactly the same exception handling techniques but in a cleaner and much better way in the controller advice with methods annotated with @ExceptionHandler.
这种方法允许我们使用完全相同的异常处理技术,但在控制器建议中以更干净、更好的方式使用带有@ExceptionHandler注释的方法。
4.1. Configuring AuthenticationEntryPoint
4.1.配置AuthenticationEntryPoint
Similar to the above approach, we’ll implement AuthenticationEntryPoint and then delegate the exception handler to HandlerExceptionResolver:
与上述方法类似,我们将实现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);
}
}
Here we’ve injected the DefaultHandlerExceptionResolver and delegated the handler to this resolver. This security exception can now be handled with controller advice with an exception handler method.
这里我们注入了DefaultHandlerExceptionResolver,并将处理程序委托给这个解析器。这个安全异常现在可以用控制器建议的异常处理方法来处理。
4.2. Configuring ExceptionHandler
4.2.配置ExceptionHandler
Now, for the main configuration for the exception handler, we’ll extend the ResponseEntityExceptionHandler and annotate this class with @ControllerAdvice:
现在,对于异常处理程序的主要配置,我们将扩展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. Configuring SecurityConfig
4.3.配置SecurityConfig
Now, let’s write a security configuration for this delegated authentication entry point:
现在,让我们为这个委托认证的入口点写一个安全配置。
@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);
}
}
For the ‘/login-handler‘ endpoint, we’ve configured the exception handler with the above-implemented DelegatedAuthenticationEntryPoint.
对于’/login-handler‘端点,我们已经用上述实现的DelegatedAuthenticationEntryPoint配置了异常处理程序。
4.4. Configure the Rest Controller
4.4.配置休息控制器
Let’s configure the rest controller for the ‘/login-handler‘ endpoint:
让我们为’/login-handler‘端点配置休息控制器。
@PostMapping(value = "/login-handler", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<RestResponse> loginWithExceptionHandler() {
return ResponseEntity.ok(new RestResponse("Success"));
}
4.5. Tests
4.5.测试
Now let’s test this endpoint:
现在我们来测试一下这个端点。
@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())));
}
In the success test, we’ve tested the endpoint with a pre-configured username and password. In the failure test, we’ve validated the response for the status code and error message in the response body.
在成功测试中,我们用预先配置的用户名和密码测试了端点。在失败测试中,我们验证了响应的状态代码和响应体中的错误信息。
5. Conclusion
5.总结
In this article, we’ve learned how to globally handle Spring Security exceptions with @ExceptionHandler. In addition, we’ve created a fully functional example that helps us understand the concepts explained.
在这篇文章中,我们学习了如何用@ExceptionHandler全局处理Spring Security异常。此外,我们还创建了一个功能齐全的示例,帮助我们理解所解释的概念。
The complete source code of the article is available over on GitHub.
该文章的完整源代码可在GitHub上获得。