跳至主要內容
Skip to content

Access Token 與 Refresh Token ​

Access Token 和 Refresh Token 的分工,是現代 token-based authentication 最核心的設計之一。Access Token 負責短時間呼叫 API,Refresh Token 負責在使用者不重新登入的情況下取得新的 Access Token。真正難的地方不是「過期就刷新」,而是如何讓 refresh token 被盜時能被偵測、撤銷與控制損害。

一句話回答:Access Token 應該短效、權限範圍有限,用來呼叫 API;Refresh Token 較長效,用來向授權伺服器換取新的 Access Token,應存 server-side 記錄、可撤銷,並使用 refresh token rotation 與 reuse detection:每次刷新都簽發新的 refresh token 並使舊 token 失效,若舊 token 被再次使用,代表可能外洩,應撤銷整個 token family 或相關 session。


一、 必考觀念 ​

1. 為什麼要分兩種 token? ​

如果只有一個長效 access token:

text
access token 有效 30 天

一旦被偷,攻擊者就可以長時間呼叫 API。

如果只有一個很短效 access token:

text
access token 有效 10 分鐘

使用者每 10 分鐘就要重新登入,體驗很差。

所以常見設計是:

text
短效 Access Token + 長效且可控的 Refresh Token

2. Access Token 和 Refresh Token 的分工 ​

Token用途壽命儲存撤銷
Access Token呼叫 API短client / memory / cookie通常靠短效,必要時 denylist
Refresh Token換新 access token較長更嚴格保護必須可撤銷

Access Token 是給 resource server 看。

Refresh Token 是給 authorization server / auth API 看。

不要拿 refresh token 直接呼叫業務 API。

3. Refresh Token 是高價值憑證 ​

Refresh Token 的危險性在於:

text
只要它還有效,就可以不斷換新的 access token。

所以它要比 access token 更嚴格保護:

  • 存 server-side hash。
  • 支援撤銷。
  • 支援 rotation。
  • 支援 reuse detection。
  • 綁定 client / device / session。
  • 有絕對過期時間。
  • 高風險時要求重新登入。

RFC 9700(OAuth 2.0 Security Best Current Practice)也建議 public clients 的 refresh token 應 sender-constrained 或使用 refresh token rotation。


二、 基本流程 ​

1. 登入後簽發 token ​

回應可能是:

json
{
  "accessToken": "eyJ...",
  "expiresIn": 900,
  "refreshToken": "opaque-refresh-token"
}

2. Access token 過期後刷新 ​

刷新不是延長原本 access token,而是簽發新的 access token。

3. Refresh token 不一定是 JWT ​

很多系統會讓 refresh token 是 opaque token:

text
rt_8a9f...random...

然後資料庫只存 hash:

typescript
type RefreshToken = {
  id: string
  userId: string
  sessionId: string
  tokenHash: string
  familyId: string
  expiresAt: string
  consumedAt?: string
  revokedAt?: string
  replacedByTokenId?: string
}

這樣 server 可以精準撤銷與追蹤。


三、 Access Token 設計 ​

1. Access token 要短效 ​

常見壽命:

text
5 分鐘
10 分鐘
15 分鐘
30 分鐘

高風險系統更短,低風險內部系統可能稍長。

重點:

  • 不要長到像 session。
  • 不要長到權限變更後很久仍有效。
  • 不要長到外洩後損害難以控制。

2. Access token 要限制權限範圍 ​

Claims 可以包含:

json
{
  "sub": "user_123",
  "iss": "https://auth.example.com",
  "aud": "api.example.com",
  "scope": "profile:read orders:read",
  "session_id": "sess_123",
  "jti": "at_123",
  "exp": 1716533600
}

注意:

  • scope / permissions 不要過大。
  • audience 要對應 resource server。
  • 高風險 API 可即時查權限或 session 狀態。
  • 不要把敏感個資放進 token。

延伸讀:JWT 是什麼。

3. Access token 被盜怎麼辦? ​

短效是第一防線。

其他策略:

  • jti denylist。
  • session revoke。
  • passwordChangedAt 檢查。
  • permission version 檢查。
  • 高風險 API 查 server-side session。
  • sender-constrained token,例如 DPoP 或 mTLS。

不是每個系統都需要 denylist,但高風險系統要知道它的成本與取捨。


四、 Refresh Token 設計 ​

1. Refresh token 要可撤銷 ​

登出時:

text
revoke refresh token

修改密碼時:

text
revoke all refresh tokens for user

可疑登入時:

text
revoke token family or session

這就是為什麼 refresh token 通常要有 server-side record。

2. Refresh token 要存 hash ​

和 password reset token 類似,client 拿 raw token,server 存 hash。

建立:

typescript
const rawRefreshToken = createRandomToken()
const tokenHash = await hashToken(rawRefreshToken)

await refreshTokenRepository.create({
  userId,
  sessionId,
  tokenHash,
  familyId,
  expiresAt,
})

驗證:

typescript
const tokenHash = await hashToken(input.refreshToken)
const token = await refreshTokenRepository.findByHash(tokenHash)

如果 refresh token table 外洩,攻擊者不能直接拿資料庫內容換 token。

3. Refresh token 也要過期 ​

常見:

  • idle timeout。
  • absolute timeout。
  • remember me 較長。
  • 管理員較短。
  • 高風險帳號較短。

不要讓 refresh token 永久有效。


五、 Refresh Token Rotation ​

1. Rotation 是什麼? ​

每次使用 refresh token 換新 access token 時,同時簽發新的 refresh token,並讓舊的 refresh token 失效。

text
RT1 -> 使用 -> 產生 AT2 + RT2 -> RT1 失效
RT2 -> 使用 -> 產生 AT3 + RT3 -> RT2 失效

這樣被偷的 refresh token 比較容易被偵測。

RFC 9700 延續並更新 OAuth 2.0 安全最佳實務,對 public clients 建議使用 refresh token rotation 或 sender-constrained refresh tokens 來防止 replay。

2. Reuse detection ​

如果 RT1 已經被使用並換成 RT2,之後又有人拿 RT1 來刷新:

text
RT1 used again

這表示:

  • 使用者或攻擊者重放舊 token。
  • token 可能外洩。
  • client 可能並發刷新處理錯誤。

安全做法通常是撤銷整個 token family:

text
revoke RT1、RT2、RT3...
要求重新登入

3. Token family ​

Token family 是同一次登入生命週期中 refresh token 的鏈。

typescript
type RefreshToken = {
  id: string
  familyId: string
  userId: string
  sessionId: string
  tokenHash: string
  consumedAt?: string
  replacedByTokenId?: string
  revokedAt?: string
}

當發現 reuse:

typescript
await refreshTokenRepository.revokeFamily({
  familyId: token.familyId,
  reason: 'reuse_detected',
})

4. 並發刷新問題 ​

真實前端可能同時發出多個 API,發現 access token 過期後同時刷新:

text
request A refresh RT1
request B refresh RT1

如果沒有處理,第二個請求可能被判定為 reuse attack。

解法:

  • 前端用 refresh mutex,只允許一個 refresh。
  • 後端設很短的 grace window。
  • 後端檢查是否為同一 client 在極短時間內重試。
  • 仍要避免寬鬆到攻擊者可利用。

資深回答要能說出 rotation 和並發刷新之間的取捨。


六、 Refresh API 設計 ​

1. 基本流程 ​

typescript
async function refresh(input: RefreshInput, request: Request) {
  const tokenHash = await hashToken(input.refreshToken)
  const token = await refreshTokenRepository.findByHash(tokenHash)

  if (!token || token.revokedAt || token.expiresAt < new Date()) {
    throw new InvalidRefreshTokenError()
  }

  if (token.consumedAt) {
    await refreshTokenRepository.revokeFamily({
      familyId: token.familyId,
      reason: 'reuse_detected',
    })

    throw new InvalidRefreshTokenError()
  }

  const newRefreshToken = createRandomToken()
  const newRefreshTokenHash = await hashToken(newRefreshToken)

  const accessToken = await accessTokenService.sign({
    sub: token.userId,
    sessionId: token.sessionId,
  })

  await transaction(async trx => {
    const replacement = await refreshTokenRepository.create(
      {
        userId: token.userId,
        sessionId: token.sessionId,
        familyId: token.familyId,
        tokenHash: newRefreshTokenHash,
        expiresAt: calculateRefreshExpiry(),
      },
      trx,
    )

    await refreshTokenRepository.consume(
      {
        tokenId: token.id,
        replacedByTokenId: replacement.id,
      },
      trx,
    )
  })

  return {
    accessToken,
    refreshToken: newRefreshToken,
  }
}

重點:

  • 驗證 token 是否存在、未過期、未撤銷。
  • consumed token 再次出現要視為高風險。
  • rotation 的 consume + create 要在 transaction。
  • 寫 audit log。

2. Refresh token 要綁定 session / client ​

可以記錄:

  • sessionId。
  • clientId。
  • device id。
  • userAgent hash。
  • IP history。
  • createdAt / lastUsedAt。

但不要過度依賴 IP 綁定,因為行動網路與公司網路可能變動。

3. Refresh 失敗時前端怎麼辦? ​

如果 refresh token invalid:

text
清除本地登入狀態
導回 login
保留 redirect
顯示登入已過期

不要陷入無限 refresh loop。


七、 儲存位置 ​

1. Web app 常見選擇 ​

選項一:Refresh token 放 HttpOnly Cookie。

優點:

  • JS 不能直接讀取。
  • 降低 XSS 直接偷 token。

缺點:

  • 要處理 CSRF。
  • Cookie 跨域設定複雜。

選項二:Refresh token 放 memory。

優點:

  • 重新整理後消失,降低長期偷取風險。

缺點:

  • 使用者刷新頁面可能要重新登入或靠 silent refresh。

選項三:Refresh token 放 localStorage。

優點:

  • 實作簡單。

缺點:

  • XSS 可直接讀取,風險高。

對一般 Web app,長效 refresh token 不建議裸放 localStorage。

2. Mobile app ​

Mobile app 應使用平台安全儲存:

  • iOS Keychain。
  • Android Keystore / EncryptedSharedPreferences。
  • Secure Enclave / hardware-backed key store,視需求。

不要把 refresh token 明文存在一般檔案或不安全 storage。

3. SPA 與 BFF ​

對高安全 Web app,可以考慮 Backend For Frontend:

text
Browser 只拿 session cookie
BFF 保存 token 或代表前端呼叫 API

這樣可以避免把 refresh token 暴露給瀏覽器 JavaScript。

代價是架構更複雜。


八、 登出與撤銷 ​

1. 單裝置登出 ​

text
revoke current refresh token family
or revoke current session

如果每個裝置有一個 session / token family,就可以精準登出單裝置。

2. 全裝置登出 ​

text
revoke all sessions / refresh token families for user

常見觸發:

  • 使用者手動全部登出。
  • 修改密碼。
  • 忘記密碼成功。
  • 管理員停用帳號。
  • reuse detection。
  • 帳號疑似被盜。

3. Access token 怎麼處理? ​

如果 access token 很短效,登出時可以只撤銷 refresh token。

但高風險系統可能要:

  • 將 access token jti 加入 denylist。
  • resource server 查 session 狀態。
  • 權限變更時提高 token version。

取決於你能接受 access token 在剩餘有效期內仍可用多久。


九、 常見錯誤 ​

1. Refresh token 不旋轉 ​

如果同一個 refresh token 可以用一個月,每次都換 access token:

text
RT1 -> AT1
RT1 -> AT2
RT1 -> AT3

RT1 被偷後,攻擊者可以長期使用,而且難以偵測。

2. Refresh token 不可撤銷 ​

如果 refresh token 是長效 JWT,且 server 不保存狀態,登出與可疑事件很難即時處理。

3. Refresh token 和 access token 用同一個 secret、同一套 claims ​

不同 token 用途不同,應該有:

  • 不同 lifetime。
  • 不同 audience。
  • 不同 storage。
  • 不同驗證邏輯。
  • 不同撤銷策略。

4. Refresh API 沒有 rate limit ​

Refresh endpoint 也可能被攻擊。

要限制:

  • 同一 token family。
  • 同一 user。
  • 同一 IP。
  • 同一 device。

5. 前端多請求同時 refresh ​

如果前端沒有 refresh mutex,可能造成:

  • rotation 衝突。
  • 使用者被踢出。
  • token family 被誤判 reuse。
  • 無限重試。

前端 token client 要集中管理 refresh 流程。


十、 追問題庫 ​

Q1:Access token 和 refresh token 差在哪? ​

Access token 用來呼叫 API,壽命短、權限範圍有限;refresh token 用來取得新的 access token,壽命較長、風險更高,必須可撤銷並更嚴格保護。

Q2:為什麼 access token 要短效? ​

因為 access token 一旦被偷,在有效期內通常可以直接呼叫 API。短效可以降低外洩後的損害窗口。

Q3:Refresh token rotation 是什麼? ​

每次 refresh 都簽發新的 refresh token,並讓舊 refresh token 失效。如果舊 token 再次被使用,代表可能外洩,應撤銷 token family 或要求重新登入。

Q4:Refresh token 要不要存資料庫? ​

通常要。至少要存 hash、userId、sessionId、familyId、expiresAt、revokedAt、consumedAt 等資訊,才能撤銷、rotation、reuse detection 和多裝置管理。

Q5:登出時要撤銷 access token 嗎? ​

看風險。若 access token 很短效,可以只撤銷 refresh token;若是高風險系統,可以把 access token 的 jti 加入 denylist,或讓 resource server 查 session 狀態。

Q6:Refresh token 被偷怎麼辦? ​

如果有 rotation,舊 token 被再次使用時可觸發 reuse detection,撤銷整個 token family、通知使用者、要求重新登入,必要時撤銷所有 session。


十一、 實作題 ​

題目:設計 Refresh Token Rotation ​

需求:

  • Access token 15 分鐘過期。
  • Refresh token 30 天過期。
  • 每次 refresh 都 rotation。
  • 偵測舊 refresh token 重放。
  • 支援登出單裝置。

資料表:

typescript
type RefreshTokenRecord = {
  id: string
  userId: string
  sessionId: string
  familyId: string
  tokenHash: string
  expiresAt: string
  consumedAt?: string
  replacedByTokenId?: string
  revokedAt?: string
  revokedReason?: string
  createdAt: string
}

流程:

text
POST /token/refresh
  -> hash raw refresh token
  -> 找 token record
  -> 不存在 / 過期 / revoked:拒絕
  -> consumedAt 已存在:reuse detected,revoke family
  -> 建立新 refresh token
  -> 舊 token consumed
  -> 簽發新 access token

題目延伸:前端避免並發 refresh ​

typescript
let refreshPromise: Promise<TokenPair> | null = null

async function refreshOnce() {
  if (!refreshPromise) {
    refreshPromise = authApi.refresh().finally(() => {
      refreshPromise = null
    })
  }

  return refreshPromise
}

多個 API 同時遇到 401 時,共用同一個 refresh promise,避免多次使用同一個 refresh token。


十二、 資深視角 ​

1. Refresh token 是 session 的另一種表達 ​

如果你的 refresh token:

  • 有 server-side record。
  • 有 sessionId。
  • 可撤銷。
  • 可列出裝置。
  • 可 rotation。

那它其實已經很接近 session 管理。

所以不要迷信「token-based 就一定 stateless」。

2. Rotation 提高安全,也提高複雜度 ​

Rotation 帶來:

  • Reuse detection。
  • 被盜後可偵測。
  • 更細緻的撤銷。

但也帶來:

  • 並發刷新問題。
  • 前後端同步問題。
  • token family 管理。
  • transaction 與一致性要求。

高風險系統值得;低風險系統也至少要理解取捨。

3. Sender-constrained token 是更高階方向 ​

Refresh token rotation 仍然是 bearer token:

text
誰拿到 token,誰就能用。

更高強度可以考慮 sender-constrained token,例如:

  • mTLS。
  • DPoP。
  • 裝置綁定。

這些會把 token 綁定到某個 client key,降低被偷後直接重放的風險。


總結 ​

問題回答重點
Access token短效,用來呼叫 API
Refresh token較長效,用來換新 access token
為什麼分開降低 access token 被偷後的損害,同時維持體驗
Rotation每次 refresh 簽發新 refresh token,舊 token 失效
Reuse detection舊 token 再次使用代表可能外洩,撤銷 token family
儲存Refresh token 應 server-side 記錄並只存 hash
登出撤銷 refresh token / session,必要時 denylist access token
風險並發 refresh、token 被盜、長效 token 不可撤銷

一句完整的面試回答可以是:

Access token 和 refresh token 的分工是:access token 短效,用來呼叫 API,通常包含 sub、aud、scope、exp、jti 等 claims;refresh token 較長效,用來向 auth server 換新的 access token,安全等級更高,應該可撤銷並嚴格保護。Access token 短效可以降低被偷後的損害窗口;refresh token 則用來維持使用者體驗。Refresh token 通常要存在 server-side record,資料庫只存 hash,並記錄 userId、sessionId、familyId、expiresAt、consumedAt、revokedAt。比較安全的做法是 refresh token rotation:每次 refresh 都簽發新的 refresh token 並使舊 token 失效;如果舊 token 再次被使用,視為 reuse detection,撤銷整個 token family 並要求重新登入。登出時至少撤銷 refresh token,高風險系統也可 denylist access token 或讓 resource server 查 session 狀態。

延伸閱讀 ​