知識庫 / Spring / Spring Security RSS 訂閱

使用 @ExceptionHandler 處理 Spring Security 異常

Spring Security
HongKong
9
12:00 PM · Dec 06 ,2025

1. 概述

本教程將學習如何使用 <em @ExceptionHandler</em><em @ControllerAdvice</em> 全局處理 Spring Security 異常。<strong>控制器建議(Controller Advice)是一種攔截器,它允許我們在應用程序中共享相同的異常處理邏輯。</strong>

2. Spring Security 異常

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

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

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

3. 不使用 @ExceptionHandler

Spring Security 異常在 AuthenticationEntryPoint 中啓動。 讓我們編寫一個實現,該實現攔截 Security 異常,該實現將攔截 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 作為響應體的消息轉換器。

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

現在,讓我們看看如何使用 @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 控制器

以下配置 Rest 控制器,用於 ‘login-handler</em/>’ 端點:

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

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

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

發佈 評論

Some HTML is okay.