JWT、Session 與 Laravel Cookie Session Driver 的比較
最近在重新整理身份驗證(Authentication)相關的知識,發現自己對「Session 到底存在哪裡」、「JWT 為什麼難以撤銷」、「Laravel 那個 Cookie Driver 到底跟 JWT 差在哪」這幾件事一直模模糊糊。趁這次把幾份規範與框架文件讀過一遍,我想把傳統 Server-side Session、JWT(JSON Web Token)與 Laravel Cookie Session Driver 三種方案做一個完整的比較筆記,順便釐清自己過去的一些誤解。
這篇筆記的重點不在於「哪一個最好」,而是希望能夠回答一個問題:在不同的場景下,我應該選擇哪一種方案?
為什麼需要 Session?
HTTP 本身是無狀態(Stateless)的協定,伺服器在處理完一個請求之後,並不會「記得」剛剛是誰來敲門。但是現代應用幾乎都需要「使用者登入後,下一個請求伺服器要知道是同一個人」的能力,於是衍生出 Session 的概念。
讓伺服器在每個請求之間維持身份的方式,大致可以分成兩種思路:
- Server-side State:狀態存在伺服器,客戶端只持有一個指向狀態的「鑰匙」(通常是 Session ID)。
- Client-side State:狀態直接放在客戶端持有的 Token 裡面,伺服器只負責驗證。
JWT 屬於第二種思路,Laravel 的 Cookie Session Driver 也屬於第二種,但兩者在「加密」與「使用方式」上有很大的差異。
傳統 Server-side Session
運作流程
傳統 Server-side Session 是最直觀的做法:
- 使用者輸入帳號密碼登入。
- 伺服器產生一個
session_id,並將對應的使用者資訊(user id、權限、購物車內容等)存在伺服器端的 Session Store(記憶體、Redis、資料庫、共用檔案系統等)。 - 透過
Set-Cookie把session_id發給瀏覽器。 - 之後每次請求瀏覽器都會自動帶上這個 Cookie。
- 伺服器拿
session_id去 Session Store 查詢使用者狀態。
Set-Cookie: PHPSESSID=abc123def456; HttpOnly; Secure; SameSite=Lax; Path=/
優點
- 主動撤銷容易:要踢人下線,刪除 Session Store 裡那筆紀錄即可,下次請求就會被當作未登入。
- 可稽核:能夠列出目前所有登入中的使用者、強制特定使用者登出、查看登入裝置。
- 網路成本低:Cookie 只帶 32 byte 左右的 ID,幾乎沒有頻寬負擔。
- 內容完全不外洩:Session 內容根本沒有離開伺服器,攻擊者就算拿到 Cookie 也讀不到內容(只能拿來重放)。
缺點
- 需要 Session Store:Redis、資料庫或共用檔案系統都是額外的依賴。
- 水平擴展需要設計:要嘛使用 Sticky Session 把同一使用者導到同一台機器,要嘛使用共用的 Session Store。
- 微服務架構不友善:每個服務都要查中心化的 Session Store,效能與耦合都成問題。
- 跨網域(Cross-Domain)困難:Cookie 有同源限制,做 Single Sign-On(SSO)特別痛苦。
- 對行動 App、M2M API 不友善:原本就不是為非瀏覽器設計的。
JWT 的出現
結構與規範
JWT 由 RFC 7519 定義,是一種自包含(Self-contained)的 Token 格式。它把狀態從伺服器搬到 Token 自己身上,因此伺服器可以做到真正的無狀態。
JWT 由三段以 . 分隔的 Base64URL 字串組成:Header.Payload.Signature。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0In0.signature-here
- Header:演算法(
alg,例如HS256、RS256)與型別(typ)。 - Payload:claims,例如
sub(subject)、exp(expiration)、iat(issued at)、iss(issuer)、aud(audience)、nbf(not before)、jti(JWT ID)。 - Signature:使用密鑰對
Header.Payload做簽章。
[!IMPORTANT] JWT 的 Payload 只是 Base64 編碼,不是加密。任何拿到 Token 的人都可以直接 Base64 解碼讀內容。簽章只保證「沒被竄改」,不保證「不能被看見」。這是我以前最常搞錯的觀念。
JWT 解決了什麼問題?
- 無狀態:把使用者狀態放進 Token 自己身上,伺服器不用查中心化的 Session Store。
- 跨服務驗證:任何持有驗證金鑰的服務都能獨立驗證 Token。
- 適合非對稱情境:使用 RS256 時,發 Token 的服務用私鑰簽章,其他服務只需要公鑰就能驗證,非常適合微服務。
- 跨網域、SSO 場景:因為驗證不依賴 Cookie 與同源限制,OAuth 2.0 / OIDC 大量採用 JWT 作為 Access Token / ID Token 的承載格式。
JWT 的問題
結構性問題:撤銷(Revocation)困難
JWT 最大的問題是「簽出去就收不回來」,Token 在 exp 之前都會被驗證為有效。常見的痛點:
- 使用者按了登出按鈕,但 Token 在到期前仍然有效。
- 發現 Token 被盜取,過期前無法阻止攻擊者使用。
- 使用者改了密碼,舊 Token 仍然可以使用。
業界常見的折衷做法(本質上都是偷偷把伺服器狀態加回來):
- 短壽命 Access Token(5–15 分鐘)+ Refresh Token:縮短洩漏的暴露窗口。
- Token Denylist(黑名單):把要撤銷的
jti丟到 Redis,驗證時順便查一下;黑名單只需保留到 Token 的exp為止。 - Token Version 比對:在使用者表加上
token_version欄位,要全面撤銷時把它 +1,驗證 Token 時比對token_versionclaim。
[!CAUTION] 一旦用了 Denylist 或 Token Version,就等於放棄了 JWT「真正無狀態」的優勢。這沒有對錯,但要清楚自己在做什麼取捨。
Payload 公開可讀
因為 Payload 只是 Base64 編碼,所以絕對不要在 Payload 放敏感資訊,例如密碼、信用卡號、個人識別資訊(PII)。如果真的有加密需求,可以使用 JWE(JSON Web Encryption),但複雜度會大幅上升。
簽章演算法陷阱
JWT 的歷史上有兩個經典漏洞,都跟「信任 Header 裡的 alg 欄位」有關:
alg: none漏洞:早期的 library 看到"alg": "none"就直接跳過簽章驗證。- HS256 / RS256 混淆漏洞:伺服器原本用 RS256 驗證,攻擊者把 Header 改成 HS256,並使用「公開的公鑰」當作 HMAC 的 secret 來偽造簽章;如果 library 完全信任 Header 的
alg,就會把公鑰當對稱金鑰用,攻擊成功。
[!IMPORTANT] 防禦方法很簡單也很重要:驗證時演算法絕對不能信 Header,要在程式碼裡寫死白名單(Allowlist)。
Token 體積
- Session ID 通常 32 byte 左右。
- JWT 動輒幾百 bytes 到 1KB 以上。
- 每個請求都要帶,對行動裝置、API Gateway 都是額外的負擔。
JWT 客戶端儲存方式比較
把 Token 放在哪裡,會直接決定攻擊面(Attack Surface)的範圍。
| 儲存位置 | XSS 風險 | CSRF 風險 | 重新整理保留 | 適用場景 |
|---|---|---|---|---|
localStorage | 🔴 高(JS 可讀) | 低 | ✅ | 不建議存 Token |
sessionStorage | 🔴 高(JS 可讀) | 低 | ❌(關 tab 消失) | 不建議存 Token |
| HttpOnly Cookie | 🟢 JS 讀不到 | 🟡 需防禦 | ✅ | Web 場景首選 |
| In-memory(JS 變數) | 🟡 同頁 JS 可讀 | 低 | ❌ | SPA 的 Access Token |
| iOS Keychain / Android Keystore | 由 OS 保護 | — | ✅ | 行動原生 App |
為什麼 localStorage 是高風險:localStorage 對所有同源 JS 開放讀取,第三方套件、CDN script、廣告 SDK 都能讀到,一行 localStorage.getItem('token') 就可能整個被偷走。
HttpOnly Cookie 的正確設定:
Set-Cookie: token=eyJ...; HttpOnly; Secure; SameSite=Strict; Path=/
HttpOnly:JavaScript 讀不到。Secure:只在 HTTPS 連線傳送。SameSite=Strict或Lax:擋掉跨站請求,緩解 CSRF。
SPA 推薦的混合模式:
Access Token → 存在 JS 記憶體(變數裡),重新整理就消失
Refresh Token → 存在 HttpOnly Cookie,只送到 /refresh
這樣 XSS 拿不到 Refresh Token(因為 HttpOnly),CSRF 也打不到 Access Token(因為它根本不在 Cookie 裡)。
提升 JWT 安全性的做法
- 短壽命 Access Token + Refresh Token:縮短洩漏窗口。
- Refresh Token Rotation + Reuse Detection:每次換新 Token 就讓舊的 Refresh Token 失效;如果偵測到同一個 Refresh Token 被使用兩次,整條 token family 全部撤銷。
- 嚴格的演算法白名單:呼叫 library 時傳入
algorithms參數寫死,不信任 Header 的alg。 - DPoP(Demonstrating Proof-of-Possession,RFC 9449):把 Token 綁定到客戶端的金鑰對,光偷 Token 沒用,攻擊者還必須拿到對應的私鑰。
- 細粒度的 Claims 驗證:
iss、aud、nbf、exp全部都要驗。 - Content Security Policy(CSP):限制可載入的腳本來源,降低 XSS 攻擊面(屬於補救性質)。
- 不要在 Payload 放敏感資料。
- 密鑰管理:HS256 至少 256 bit;優先選擇 RS256 / ES256;支援
kid(Key ID)以便多金鑰並存與輪換。
Laravel Cookie Session Driver
接下來看 Laravel 比較特別的 Cookie Session Driver。剛開始接觸的時候我以為它就是 JWT 的另一種寫法,後來才發現它跟 JWT 有一個關鍵差異:它真的用 AES 加密。
運作原理
Cookie Name = session 名稱(通常是 laravel_session)
Cookie Value = AES_Encrypt(整包 session 資料, APP_KEY)
- 使用
APP_KEY(預設 AES-256-CBC)對整包 Session 資料進行加密。 - 加密後的密文放在 HttpOnly Cookie 裡。
- 伺服器本身完全不儲存任何 Session 狀態。
- Laravel 預設的
config/session.php已經啟用http_only、secure、same_site=lax,預設就是相對安全的設定。
[!NOTE] 這是 Laravel Cookie Driver 和 JWT 的最大差異。JWT 只簽章不加密(Payload 公開可讀),Laravel Cookie Session 則是真的把整包資料用 AES 加密放進 Cookie,沒有
APP_KEY完全解不開。
與 JWT、傳統 Session 的對照
| 特性 | 傳統 Session(Redis/DB) | JWT | Laravel Cookie Session |
|---|---|---|---|
| 資料儲存位置 | 伺服器 | 客戶端 | 客戶端 |
| 資料是否加密 | N/A | ❌ 只簽章 | ✅ AES 加密 |
| 客戶端能讀內容 | 拿不到 | ✅ Base64 解碼 | ❌ 沒 KEY 讀不到 |
| 客戶端能改內容 | 拿不到 | ❌ 簽章會破 | ❌ 解不開就改不了 |
| 主動撤銷 | ✅ 容易 | ❌ 困難 | ❌ 困難 |
| 預設儲存方式 | 伺服器自管 | 開發者決定 | HttpOnly Cookie |
優點
- 真正的無狀態:不需要 Redis、資料庫或共用儲存;不需要 Sticky Session;非常適合 Serverless 部署(AWS Lambda、Cloud Run、Vercel)。
- 資料保密性比 JWT 強:因為是真加密,理論上可以放敏感資料(雖然還是不建議)。
- 預設安全:HttpOnly + Secure + SameSite 預設都開好。
- 與 Laravel 生態完全相容:CSRF Token、Flash messages、Auth guards、
old()、validation errors 全部正常運作,使用者完全感受不到底層是 Cookie Driver。
缺點
-
Cookie 4KB 大小限制
瀏覽器規範每個 Cookie 大約 4KB。整包資料經過 AES 加密後,因為 IV、padding、Base64 編碼會變得更大,實際可用空間大約只剩 3KB。一個 Laravel session 可能同時包含 Auth 資訊、CSRF Token、Flash messages、validation errors、
url.intended等等,膨脹很快。一旦超過上限,瀏覽器會直接把 Cookie 丟掉,使用者的體驗會是「突然像是沒登入」,而且 server log 看不到任何錯誤。 -
每次請求都扛完整 session
上行的 Request Header 帶完整 Cookie,下行的
Set-Cookie回完整內容。對行動裝置與慢速網路是有感的負擔,再加上每個請求都要做一次 AES 加解密,CPU 成本不是零。 -
撤銷與 JWT 一樣困難
無法主動把使用者踢下線、無法列出目前在線的使用者、改密碼後舊 Cookie 仍然有效。
-
APP_KEY輪換會讓所有人登出APP_KEY同時用於整個 Laravel 的加密(Cookie、Crypt::encrypt()、Queue payload 等)。換了 key 之後,舊 Cookie 全部解不開,所有人被迫重新登入。Laravel 11 起支援APP_PREVIOUS_KEYS環境變數,可以保留舊 key 做解密、新 key 做加密,讓輪換能平滑進行。 -
併發請求的競態問題(Race Condition)
多個 tab 或同時發出的 AJAX,每個 response 都會帶自己版本的
Set-Cookie,最後寫入的會覆蓋前面的。沒有任何 lock 機制。Session 內的計數器、購物車這類 read-modify-write 場景有資料遺失的風險。 -
APP_KEY洩漏是災難APP_KEY洩漏的影響範圍非常大:可以解密所有截獲的 Cookie;更恐怖的是可以偽造任意 session 假冒任何使用者。Server-side Session 即使 Redis 被攻破,也只是拿到目前活著的 Session,而 Cookie Driver 一旦APP_KEY外洩,攻擊者等於拿到「無限發證的權限」。
[!CAUTION]
APP_KEY對 Cookie Session Driver 而言不只是一把加密金鑰,它幾乎等於「整個系統的根憑證」。專案的部署、CI、備份檔,都要把它當機密等級的資料看待。
適合與不適合的場景
適合:
- Serverless 部署、沒有共享儲存可用。
- 中小型應用、Session 資料量穩定且不大。
- 不需要 Active Session Management(強制登出、查看在線使用者)。
不適合:
- 需要強制登出、Session 稽核、查看在線使用者。
- Session 資料量大、容易接近或超過 4KB。
- 高度併發的 SPA / API。
- 金融、醫療等需要快速撤銷的場景。
三種方案綜合比較
| 比較項目 | 傳統 Server-side Session | JWT | Laravel Cookie Session |
|---|---|---|---|
| 狀態儲存位置 | 伺服器 | 客戶端 | 客戶端 |
| 是否加密內容 | N/A(內容在伺服器) | ❌ 僅簽章 | ✅ AES 加密 |
| 主動撤銷 | 🟢 容易 | 🔴 困難 | 🔴 困難 |
| 水平擴展 | 🟡 需共用 Store 或 Sticky | 🟢 天生支援 | 🟢 天生支援 |
| 跨網域 / SSO | 🔴 困難 | 🟢 容易 | 🔴 困難(受 Cookie 限制) |
| Cookie / Token 大小 | 32 byte | 數百 byte ~ 1KB+ | 受 4KB Cookie 限制 |
| 是否需要外部依賴 | 🔴 需要 Session Store | 🟢 不需要 | 🟢 不需要 |
| 在線使用者稽核 | ✅ 可以 | ❌ 不行 | ❌ 不行 |
| 適合場景 | 單體 + Active Session Management | 微服務、跨網域 API、行動 App | Serverless、小型 Laravel 應用 |
決策建議:什麼時候用哪個?
整理一下我自己的判斷依據:
- 單體應用 + Redis 已經存在 → 直接用傳統 Server-side Session,最簡單、可撤銷、可稽核。
- 微服務 / SSO / 跨網域 API → JWT,搭配短壽命 Access Token + Refresh Token Rotation。
- Serverless 部署,不想多顧一個 Redis → Laravel Cookie Session Driver,但要謹慎管理
APP_KEY並避開大型 Session。 - 金融、醫療這類需要即時撤銷的場景 → 傳統 Session,或 JWT + Denylist(接受多一筆 Redis 查詢的成本)。
- 原生行動 App → JWT,存在 iOS Keychain 或 Android Keystore;Refresh Token 採用 Rotation + Reuse Detection。
- SPA Web 應用 → Access Token 放 in-memory,Refresh Token 放 HttpOnly Cookie 的混合模式。
[!NOTE] 沒有銀彈。當你準備在「無狀態」與「可撤銷」之間做取捨時,就是這篇筆記要派上用場的時候。