# Refresh Token 輪替

本頁說明 Access Token 過期後 `VerifyJWT()` 如何在同一個請求內續期，以及 Refresh ID 何時換發。

## 觸發條件

續期只在下列兩種情況發生，Access Token 仍有效時不碰 Redis 的 Refresh 資料：

| 情境 | 取得 Refresh ID 的方式 |
|---|---|
| Access Token 已過期（`jwt expired`） | 解碼過期 Token 內的 `refresh_id` |
| 沒有 Access Token，但有 Refresh ID | `X-Refresh-ID` header 或 Refresh cookie |

## 續期流程

```mermaid
sequenceDiagram
    participant C as 客戶端
    participant A as VerifyJWT
    participant R as Redis
    participant U as checkUserExists
    C->>A: 過期 Access Token 或僅 Refresh ID
    A->>R: GET refresh:id，比對指紋
    A->>R: TTL refresh:id，version + 1
    opt version > 5 或剩餘 TTL < refreshTokenExpires / 2
        A->>R: 舊鍵 SETEX 5 秒
        A->>A: 產生新 Refresh ID，version 歸零
    end
    A->>R: SETEX refresh:id（refreshTokenExpires）
    A->>U: 使用者是否存在
    A-->>C: 新 Access Token + Refresh ID（Set-Cookie 與 X-New-*）
```

## 換發規則

| 條件 | 行為 |
|---|---|
| 每次續期 | `version + 1`、`expires_at` 延長，Redis TTL 重設為完整 `refreshTokenExpires` |
| `version > 5` | 換發新 Refresh ID |
| 剩餘 TTL 小於 `refreshTokenExpires / 2` | 換發新 Refresh ID |
| 換發時 | 舊鍵保留 5 秒，讓同時送出的請求仍能用舊 ID 續期 |

`CreateJWT()` 建立的 Refresh 資料 `version` 為 1：前 4 次續期沿用同一個 ID，第 5 次續期時 `version` 變成 6 而換發。換發後 `version` 歸零，之後第 6 次續期再換發一次。

每次續期都會把 TTL 重設為完整壽命，所以 Refresh ID 的有效期是**滑動**的：持續使用的工作階段不會過期；超過 `refreshTokenExpires` 未使用才會失效。

## 續期時寫回的內容

| 位置 | 內容 |
|---|---|
| `X-New-Access-Token` header | 新的 Access Token |
| `X-New-Refresh-ID` header | 目前的 Refresh ID（換發時為新值） |
| Access cookie | 新 Access Token，壽命 `accessTokenExpires` |
| Refresh cookie | Refresh ID，壽命 `refreshTokenExpires` |

新 Access Token 的 payload 為登入時存入 Refresh 資料的使用者快照，加上 `fp`、`version`、`refresh_id`。

相關：[驗證結果](/zh/verify-results)、[Redis 鍵](/zh/redis-keys)
