iT邦幫忙

2026 iThome 鐵人賽

DAY 9
0
Vibe Coding

做一個團購後端,順便搞懂那些事系列 第 9

Day 9|JWT 怎麼測?先分定義清楚誰該負責任!

  • 分享至 

  • xImage
  •  

JWT要怎麼測試?要寫那些測試?JWT 很容易寫出一堆測試:簽發、解析、過期、簽章錯誤、登出黑名單等等,但其中不少只是在重測 JWT 函式庫。

第一步不是列出所有要測試的情況,而是先定義:這件事是誰的責任?

測試對象 要確認什麼 測試方式
JwtUtils 是否照本專案的規則簽發、解析 整合測試,真實金鑰
TokenAuthenticator 拿到各種驗證結果後怎麼處理 單元測試 + mock

JwtUtils 測的正是編碼與簽章本身,mock 掉就沒有意義。TokenAuthenticator 則相反,它關心的是「過期了怎麼辦」、「被撤銷了怎麼辦」這種情況發生後怎麼處理。


JwtUtils測試:那就真的簽一次

JwtUtilsTest 是整合測試:啟動整個 Spring 應用,讀取真實的金鑰檔,簽出來的 token 跟正式環境的一模一樣。

@Test
@DisplayName("每次產出的 jti 都不同")
void jtiIsUnique() {
    String token1 = jwtUtils.generateAccessToken("alice");
    String token2 = jwtUtils.generateAccessToken("alice");

    String jti1 = jwtUtils.parseToken(token1).getId();
    String jti2 = jwtUtils.parseToken(token2).getId();

    assertThat(jti1).isNotEqualTo(jti2);
}

這個測試測的不是「UUID 會不會重複」,而是簽發流程有沒有真的每次產生新的 jti

登出時,本專案把 access token 的 jti 放進黑名單。這時如果同一個使用者的兩支 token 拿到相同的 jti,登出一台裝置,另一台也會跟著被踢出去。


TokenAuthenticator:測試 token 過期後的處理

總不會為了測試token過期的情境,用 Thread.sleep(15 * 60 * 1000) 來測,真的讓測試等15分鐘吧? 常見做法有三種:

做法 適合情境
注入 Clock 程式本身需要可控的時間
直接簽一顆已過期的 token 想驗證函式庫判斷過期的實際行為
mock 解析結果 只想驗證拿到過期結果後怎麼處理

這次採用mock 解析結果的作法:

@Test
@DisplayName("過期 token → empty")
void expiredToken_empty() {
    when(jwtUtils.parseToken("expired"))
            .thenThrow(new ExpiredJwtException(null, null, "expired"));

    assertThat(authenticator.tryAuthenticate("expired")).isEmpty();
}

如果Redis 掛掉時怎麼驗證身分?

JWT 驗證順序是:解析 JWT(同時驗簽章與效期)→ 取出 jti → 查 Redis 黑名單。已經登出的 token 在黑名單上,拿它呼叫 API 會被擋下。

但 Redis 掛掉時,黑名單查不到,系統事先又不知道誰登出過,只能對所有請求二選一:

選擇 沒登出的人 已登出的人
fail-open:查不到就放行 照常使用 舊 token 還能用
fail-close:查不到就擋下 全部被踢出去 擋下

本專案選擇 fail-open。代價有上限:簽章與效期在查 Redis 之前就驗完了,偽造或過期的 token 照樣擋下;漏掉的只有「已經登出、但還沒過期」的 token,最多再用 15 分鐘。

@Test
@DisplayName("黑名單檢查 Redis 炸掉 → fail-open,仍回 Authentication")
void redisDown_failOpen() {
    when(jwtUtils.parseToken("good")).thenReturn(claims("alice", "jti-3"));
    when(tokenRedisService.isAccessTokenBlacklisted("jti-3"))
            .thenThrow(new RedisConnectionFailureException("redis down"));

    Optional<Authentication> result = authenticator.tryAuthenticate("good");

    assertThat(result).isPresent();
    assertThat(result.get().getName()).isEqualTo("alice");
}

降級路徑平常不會被執行,因此如果沒有這條測試,某次重構把例外改成「驗證失敗」也不會有人察覺,要等 Redis 真的故障那天才發現。


上一篇
Day 8|登出只刪 refresh,那張還活著的 access 怎麼作廢?
系列文
做一個團購後端,順便搞懂那些事9
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言