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:
access token 有效 30 天一旦被偷,攻擊者就可以長時間呼叫 API。
如果只有一個很短效 access token:
access token 有效 10 分鐘使用者每 10 分鐘就要重新登入,體驗很差。
所以常見設計是:
短效 Access Token + 長效且可控的 Refresh Token2. 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 的危險性在於:
只要它還有效,就可以不斷換新的 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
回應可能是:
{
"accessToken": "eyJ...",
"expiresIn": 900,
"refreshToken": "opaque-refresh-token"
}2. Access token 過期後刷新
刷新不是延長原本 access token,而是簽發新的 access token。
3. Refresh token 不一定是 JWT
很多系統會讓 refresh token 是 opaque token:
rt_8a9f...random...然後資料庫只存 hash:
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 要短效
常見壽命:
5 分鐘
10 分鐘
15 分鐘
30 分鐘高風險系統更短,低風險內部系統可能稍長。
重點:
- 不要長到像 session。
- 不要長到權限變更後很久仍有效。
- 不要長到外洩後損害難以控制。
2. Access token 要限制權限範圍
Claims 可以包含:
{
"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 要可撤銷
登出時:
revoke refresh token修改密碼時:
revoke all refresh tokens for user可疑登入時:
revoke token family or session這就是為什麼 refresh token 通常要有 server-side record。
2. Refresh token 要存 hash
和 password reset token 類似,client 拿 raw token,server 存 hash。
建立:
const rawRefreshToken = createRandomToken()
const tokenHash = await hashToken(rawRefreshToken)
await refreshTokenRepository.create({
userId,
sessionId,
tokenHash,
familyId,
expiresAt,
})驗證:
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 失效。
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 來刷新:
RT1 used again這表示:
- 使用者或攻擊者重放舊 token。
- token 可能外洩。
- client 可能並發刷新處理錯誤。
安全做法通常是撤銷整個 token family:
revoke RT1、RT2、RT3...
要求重新登入3. Token family
Token family 是同一次登入生命週期中 refresh token 的鏈。
type RefreshToken = {
id: string
familyId: string
userId: string
sessionId: string
tokenHash: string
consumedAt?: string
replacedByTokenId?: string
revokedAt?: string
}當發現 reuse:
await refreshTokenRepository.revokeFamily({
familyId: token.familyId,
reason: 'reuse_detected',
})4. 並發刷新問題
真實前端可能同時發出多個 API,發現 access token 過期後同時刷新:
request A refresh RT1
request B refresh RT1如果沒有處理,第二個請求可能被判定為 reuse attack。
解法:
- 前端用 refresh mutex,只允許一個 refresh。
- 後端設很短的 grace window。
- 後端檢查是否為同一 client 在極短時間內重試。
- 仍要避免寬鬆到攻擊者可利用。
資深回答要能說出 rotation 和並發刷新之間的取捨。
六、 Refresh API 設計
1. 基本流程
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:
清除本地登入狀態
導回 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:
Browser 只拿 session cookie
BFF 保存 token 或代表前端呼叫 API這樣可以避免把 refresh token 暴露給瀏覽器 JavaScript。
代價是架構更複雜。
八、 登出與撤銷
1. 單裝置登出
revoke current refresh token family
or revoke current session如果每個裝置有一個 session / token family,就可以精準登出單裝置。
2. 全裝置登出
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:
RT1 -> AT1
RT1 -> AT2
RT1 -> AT3RT1 被偷後,攻擊者可以長期使用,而且難以偵測。
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 重放。
- 支援登出單裝置。
資料表:
type RefreshTokenRecord = {
id: string
userId: string
sessionId: string
familyId: string
tokenHash: string
expiresAt: string
consumedAt?: string
replacedByTokenId?: string
revokedAt?: string
revokedReason?: string
createdAt: string
}流程:
POST /token/refresh
-> hash raw refresh token
-> 找 token record
-> 不存在 / 過期 / revoked:拒絕
-> consumedAt 已存在:reuse detected,revoke family
-> 建立新 refresh token
-> 舊 token consumed
-> 簽發新 access token題目延伸:前端避免並發 refresh
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:
誰拿到 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 狀態。