使用 @ExceptionHandler 处理 Spring Security 异常

您所在的位置:网站首页 springsecurity拦截请求注解 使用 @ExceptionHandler 处理 Spring Security 异常

使用 @ExceptionHandler 处理 Spring Security 异常

2024-07-11 15:05| 来源: 网络整理| 查看: 265

1、概览

本文将会带你了解如何使用 @ExceptionHandler 和 @ControllerAdvice 全局处理 Spring Security 异常。

Controller Advice 是一种拦截器,常用于处理全局异常。

2、Spring Security 异常

Spring Security 核心异常(如 AuthenticationException 和 AccessDeniedException)属于运行时异常。由于这些异常是由 DispatcherServlet 后面的 Authentication Filter 在调用 Controller 方法之前抛出的,因此 @ControllerAdvice 无法捕获这些异常。

通过添加自定义 Filter 和构建响应体,可以直接处理 Spring Security 异常。要通过@ExceptionHandler 和 @ControllerAdvice 在全局级别处理这些异常,需要自定义 AuthenticationEntryPoint 的实现。AuthenticationEntryPoint 用于发送 HTTP 响应,要求客户端提供凭证。虽然已经有多个内置实现,但是我们仍然需要自己实现,以发送自定义响应。

首先,让我们看看如何在不使用 @ExceptionHandler 的情况下全局处理 Security 异常。

3、不使用 @ExceptionHandler

Spring Security 异常是从 AuthenticationEntryPoint 开始的。让我们编写一个 AuthenticationEntryPoint 的实现,用于拦截 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 作为响应体的 Message Converter。

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.requestMatchers() .antMatchers("/login") .and() .authorizeRequests() .anyRequest() .hasRole("ADMIN") .and() .httpBasic() .and() .exceptionHandling() .authenticationEntryPoint(authEntryPoint); return http.build(); } } 3.3、配置 Rest Controller

编写一个 Rest Controller,监听 /login 端点:

@PostMapping(value = "/login", produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity 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

这种方法允许我们使用完全相同的异常处理技术。

但在 Controller Advice 中使用 @ExceptionHandler 注解的方法时会更加简洁,效果也更好。

4.1、配置 AuthenticationEntryPoint

与上述方法类似,我们要实现 AuthenticationEntryPoint,然后将 Exception Handler 委托给 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,并将 Handler 委托给该 Resolver(解析器)。现在,可以使用 Exception Handler 方法通过 Controller Advice 来处理此 Security 异常。

4.2、配置 ExceptionHandler

现在,继承 ResponseEntityExceptionHandler 并使用 @ControllerAdvice 注解对该类进行注解。这是 Exception Handler 的主要配置。

@ControllerAdvice public class DefaultExceptionHandler extends ResponseEntityExceptionHandler { @ExceptionHandler({ AuthenticationException.class }) @ResponseBody public ResponseEntity 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

现在,为这个 delegatedAuthenticationEntryPoint(委托的身份认证入口) 编写一个 Security 配置:

@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); } }

使用上述实现的 DelegatedAuthenticationEntryPoint 为 /login-handler 端点配置了 Exception handler。

4.4、配置 Rest Controller

定义 /login-handler 端点的 Rest Controller。

@PostMapping(value = "/login-handler", produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity 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()))); }

在 houldSucceed 测试中,使用预先配置的用户名和密码测试了端点。

在 shouldFail 测试中,验证了响应的状态码和响应体中的错误消息。

5、总结

本文通过实际案例介绍了如何使用 @ExceptionHandler 全局处理 Spring Security 异常。

参考:https://www.baeldung.com/spring-security-exceptionhandler



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3