iT邦幫忙

2025 iThome 鐵人賽

DAY 11
0
佛心分享-SideProject30

吃出一個SideProject!系列 第 11

Day11:Auth Service -Global Exception

  • 分享至 

  • xImage
  •  

昨天,我們延伸了前天遇到的問題,複習了 Exception 的概念,並分析我們遇到的錯誤,最後提出了幾個 Exception 常見的處理方式。

今天想延伸昨天介紹的內容,實際應用在專案上解決我們遇到的問題。

但在此之前,我想先驗證前天的說法,確認我們的推論沒問題後,再進一步進行改寫。

想法驗證

前天提到,我們的 API 回傳收到了預期外的 403 Forbidden。對這個問題,我們的推論是:

  1. 參數驗證失敗後噴出 MethodArgumentNotValidException
  2. Spring 框架負責捕捉這個 Exception ,最後將請求轉發到 /error
  3. 由於我們在 SecutiryFilterChain 的設定,這個路徑被擋下了(權限不足)。

為了驗證這個說法,我們可以透過在 SecurityConfig 開放 /error 的權限,並重發一次請求,來查看結果會怎麼樣,改寫內容如下:

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
            .csrf(csrf -> csrf.disable())
            .authorizeHttpRequests(auth -> auth
                    .requestMatchers(HttpMethod.POST,"/users/**").permitAll()
                    **.requestMatchers("/error").permitAll()**
                    .anyRequest().authenticated()
            );
     return http.build();
}

加上 requestMatchers("/error").permitAll() 後,回到 API Tester 重發一次請求:

https://ithelp.ithome.com.tw/upload/images/20250925/20178099p8NUZSyO2j.png

終於,如我們預期的,這次收到了 400 Bad Request 的回應。不過雖然如預期收到了回應,但我們可以看到錯誤訊息中並沒有提及觸發 Bad Request 的是哪一個參數。針對這個問題,開放權限也不是一個根本的解決方法,因此我們還是要針對 Exception 的問題進行處理。

問題解決:添加 GlobalException

建立 GlobalException

在此服務下新增一個 exception 的 package,並建立 GlobalEexception 類別。
在類別上加上 @RRestControllerAdvice 的註解,並新增 handleValidationExceptions 方法,並在方法上加上 @ExceptionHandler ,傳入我們要處理的 Exception 類別。詳細內容如下:

@RestControllerAdvice 
public class GlobalExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST) 
    public ResponseEntity<Map<String, String>> handleValidationExceptions(
            MethodArgumentNotValidException ex) {

        Map<String, String> errors = new HashMap<>();
        
        // 走訪所有驗證錯誤,將欄位名稱和錯誤訊息放入 Map 中
        ex.getBindingResult().getAllErrors().forEach((error) -> {
            String fieldName = ((FieldError) error).getField();
            String errorMessage = error.getDefaultMessage();
            errors.put(fieldName, errorMessage);
        });
        
        return ResponseEntity.badRequest().body(errors);
    }
...
}

該方法回傳一個 ResponseEntity 的物件,我們迴圈取得錯誤訊息包含的欄位以及對應訊息後,放入 Map,最後可以指定 HttpStatusCode,一起回傳給調用的函式。

  • RestControllerAdvice: 加上這個註解後的類別會監聽所有 @RestController 中拋出的例外,並尋找對應的處理方法。因為內建了 @ResponseBody,所以方法的回傳值會被自動序列化為 JSON。

  • @ExceptionHandler({Exception.class}): 標註在方法上,宣告這個方法是特定類型 Exception 的處理器。當被監聽的 Controller 拋出指定的例外時,這個方法就會被觸發。

我們在 Service 檢查有無重複註冊的信箱時,有拋出 IllegalStateException,Controller 捕捉了這個RuntimeException,我們可以用相同的方式處理這個錯誤,詳細內容如下:

    ...
    @ExceptionHandler(IllegalStateException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ResponseEntity<Map<String, String>> handleIllegalStateException(IllegalStateException ex) {
        Map<String, String> errorResponse = new HashMap<>();
        errorResponse.put("error", ex.getMessage());
        return ResponseEntity.badRequest().body(errorResponse);
    }
    ...

移除 try-catch 區塊

到 Controller 中,將 try-catch 移除,不僅是在整理程式碼,移除 try-catch 後若 ExceptionHandler 未正常生效,一樣會遇到 403 Frobidden 的問題,如此一來可以讓我們更好驗證是否成功處理 Exception。

...
@PostMapping
public ResponseEntity<?> registerUser(@Valid @RequestBody RegistrationRequest registrationRequest) {
        UserEntity user = authService.registerUser(registrationRequest);
        return ResponseEntity.status(HttpStatus.CREATED).build();
}
...

設定好後,記得移除 /error 權限,以驗證我們發生的 Exception 不再被轉發至 /error 。

測試結果

參數檢核: 再次以不符合長度規則的密碼進行測試,可以看到下圖的結果正確返回了400 Bad Request 以及 Response Body 中出現密碼不符合長度規格的訊息。因為請求還沒到 Controller 就被攔截,因此先出現參數檢核的錯誤而非信箱重覆註冊的錯誤也符合預期。

https://ithelp.ithome.com.tw/upload/images/20250925/20178099sAGGxeJ24b.png

重複註冊信箱: 這次測試以重複郵件地址進行註冊,可以看到下圖結果正確返回了400 Bad Request 以及 Response Body 中出現了我們在Exception中自訂的錯誤訊息。

https://ithelp.ithome.com.tw/upload/images/20250925/20178099oXUhqV3Kg6.png

兩種測試結果都表明,我們的 GlobalExceptionHandler 成功地捕捉了來自框架的 Checked Exception 和我們自己拋出的 RuntimeException。


今天,我們成功驗證了 403 錯誤的成因。在驗證結果後,建立了GlobalException 來統一管理 Exception,最後也確認了功能如我們預期一般地回應。

明天,我想我們可以開始進行登入功能的實作了!(終於XD)


上一篇
Day10:Exception
下一篇
Day 12:Auth Service-登入流程 與 JWT Token
系列文
吃出一個SideProject!13
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言