使用 @ExceptionHandler 處理 Spring Security 異常

Spring Security
Remote
0
07:04 AM · Nov 30 ,2025

1. 概述

在本教程中,我們將學習如何使用 @ExceptionHandler@ControllerAdvice 全局處理 Spring Security 異常。ControllerAdvice 是一個攔截器,允許我們在應用程序中共享相同的異常處理。

2. Spring Security 異常

Spring Security 的核心異常,如 AuthenticationExceptionAccessDeniedException,是運行時異常。由於這些 異常由位於 DispatcherServlet 之後且在調用控制器方法之前執行的身份驗證過濾器拋出,因此 @ControllerAdvice 無法捕獲這些異常。

Spring Security 異常可以直接通過添加自定義過濾器和構建響應主體來處理。為了通過 @ExceptionHandler@ControllerAdvice 在全局級別處理這些異常,我們需要自定義 AuthenticationEntryPoint 的實現。 AuthenticationEntryPoint 用於發送一個 HTTP 響應,要求客户端提供憑據。 儘管存在多個內置實現用於安全入口點,但我們需要編寫自定義實現來發送自定義響應消息。

首先,讓我們在不使用 @ExceptionHandler 的情況下處理安全異常。

3. Without

Spring security 異常在 啓動。 讓我們編寫一個攔截 security 異常的實現

3.1. 配置

讓我們實現 並覆蓋 方法:

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

這裏,我們使用了 作為響應體的消息轉換器。

3.2. 配置

接下來,讓我們配置 以攔截用於身份驗證的路徑。 我們將配置 ‘/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. 測試

最後,讓我們使用 mock 測試測試此端點。

首先,讓我們編寫一個成功的身份驗證的測試用例:

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

現在,讓我們看看如何使用 達到相同的效果。

4. With @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. Tests

現在讓我們測試此端點:

@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. 結論

在本文中,我們學習瞭如何全局處理 Spring Security 異常,使用 @ExceptionHandler 機制。 此外,我們創建了一個完整的示例,幫助我們理解所解釋的概念。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.