iT邦幫忙

2025 iThome 鐵人賽

DAY 13
0

為什麼需要 Refresh Token?

  • Access Token 建議設定很短(例如 2–10 分鐘),降低外洩風險。
  • 使用者不可能一直重新登入,所以我們發一張 Refresh Token(長效),在 Access Token 到期時,用它換一張新的 Access Token。
  • 這樣就能同時兼顧「安全」與「體驗」。

今天要做的:

  1. 補齊 /login-with-refresh/refresh 的完整流程
  2. 補強 安全實務撤銷/黑名單類型檢查Cookie 送法CSRF 重點。
  3. 測試腳本與常見陷阱排查。

功能流程圖

  1. POST /login-with-refresh → 回 { accessToken, refreshToken }
  2. Access 到期 → POST /refresh(帶 refreshToken
  3. 伺服器驗證 Refresh Token → 發新 Access Token(可選:同時旋轉一張新的 Refresh Token)
  4. { accessToken, refreshToken }

先看現有的 Controller

我們已經把兩個端點寫好了

// Login:同時回 accessToken 與 refreshToken
@PostMapping("/login-with-refresh")
public ResponseEntity<?> loginWithRefresh(@RequestBody Map<String, String> request) {
    String username = request.get("username");
    String password = request.get("password");

    if ("admin".equals(username) && "password".equals(password)) {
        String accessToken = JwtUtil.generateAccessToken(username, "ROLE_ADMIN");
        String refreshToken = JwtUtil.generateRefreshToken(username);

        return ResponseEntity.ok(Map.of(
                "accessToken", accessToken,
                "refreshToken", refreshToken
        ));
    }
    return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
            .body(Map.of("error", "帳號或密碼錯誤"));
}

// 用 refreshToken 換新的 accessToken
@PostMapping("/refresh")
public ResponseEntity<?> refresh(@RequestBody Map<String, String> request) {
    String refreshToken = request.get("refreshToken");
    try {
        Jws<Claims> claims = JwtUtil.validateToken(refreshToken);
        String username = claims.getBody().getSubject();

        String newAccessToken = JwtUtil.generateAccessToken(username, "ROLE_ADMIN");

        return ResponseEntity.ok(Map.of(
                "accessToken", newAccessToken,
                "refreshToken", refreshToken // 目前重用舊的 refresh(簡化版)
        ));
    } catch (ExpiredJwtException e) {
        return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
                .body(Map.of("error", "Refresh Token 過期,請重新登入"));
    } catch (Exception e) {
        return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
                .body(Map.of("error", "Refresh Token 無效"));
    }
}

目前我們的 SecurityConfig 目前只 permitAll("/login", "/login-with-refresh")記得讓 /refresh 也能匿名呼叫

.authorizeHttpRequests(auth -> auth
    .requestMatchers("/login", "/login-with-refresh", "/refresh").permitAll()
    .anyRequest().authenticated()
)

Cookie 還是 Header 傳 Refresh?

  • Header(JSON body):後端 API 最簡單;前端儲存於 localStorage / indexedDB但風險是 XSS 可竊取 Token。
  • HttpOnly Cookie(建議):後端把 Refresh Token 放在 HttpOnly + Secure + SameSite 的 Cookie,JS 取不到,降低 XSS 風險。
    • 此時 /refresh 可以走 Cookie,不需 body 帶 token。
    • 需注意 CSRF:建議 SameSite=Lax/Strict 或搭配 CSRF Token。

測試步驟(Postman/curl)

1) 登入拿票(含 refresh)

curl -X POST http://localhost:8080/login-with-refresh \
  -H "Content-Type: application/json" \
  -d '{"username":"admin","password":"password"}'

回應:

{
  "accessToken":"<短效>",
  "refreshToken":"<長效>"
}

2) 帶 Access 打受保護 API

curl http://localhost:8080/hello \
  -H "Authorization: Bearer <accessToken>"

3) Access 過期後,用 Refresh 換新 Access

curl -X POST http://localhost:8080/refresh \
  -H "Content-Type: application/json" \
  -d '{"refreshToken":"<refreshToken>"}'

成功會回新的 accessToken(若有旋轉,也會回新的 refreshToken)。

常見陷阱與排查

  1. 呼叫 /refresh 回 401:Token 類型錯誤
    • 忘了檢查 type="refresh",或前端誤把 Access Token 傳來刷新。
  2. 一直 401:Invalid Token
    • SECRET 不一致(簽發與驗證要同一把 key)。
    • Token 遭竄改或格式錯。
  3. /hello 取不到使用者
    • 確認 JwtAuthenticationFilteraddFilterBefore(..., UsernamePasswordAuthenticationFilter.class)
    • 確認請求 Header 有 Authorization: Bearer <token>
    • Token 是否過期。
  4. CSRF 誤傷(若改用 Cookie 並開啟 CSRF 防護)
    • 確認 SameSite 設定與 CSRF Token 配置。純 API 通常先 csrf.disable(),或採 REST + Cookie 流程時搭配 CSRF Token。

今天的收穫

  • 了解 Refresh Token 的核心意義:讓短效 Access 變得可用。
  • /login-with-refresh/refresh 串好、可實測。
  • 實務上會有一些需要實作的,例如CSRF,或者是嘗試更換,但練習的話我認為只要能夠先把Refresh token的API寫出來,確定能夠更換即可。

加上了Refresh token,整個都不一樣了,雖然只是增加一個Refresh,實務上就會變得靈活很多,但同事也會有很多的邏輯需要調整,這次雖然沒有給大家專案,但有了之前的案例,我相信大家應該是有機會獨立寫出來,大家可以試著跟AI溝通,讓延伸前幾天的程式碼,新增Refresh token。

相信大家也會需要一點時間消化,大家可以重複看看,然後再休息一下,我們明天見!


上一篇
Day 12 JWT 整合 Spring Security
系列文
「站住 口令 誰」關於資安權限與授權的觀念教學,以Spring boot Security框架實作13
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言