技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTBR

前端鉴权的兄弟们:Cookie、Session、Token、JWT、单点登录

所属馆藏
Browser
文件格式
Markdown
原始路径
Browser/24-前端鉴权的兄弟们:cookie、session、token、jwt、单点登录
本文目录12 个章节
  1. 一、从 HTTP 无状态说起
  2. 二、Cookie:浏览器自动携带的凭证容器
  3. 三、服务端 Session:Cookie 只保存 ID
  4. 四、Token:凭证不等于 JWT
  5. 五、Token 编码、签名和加密
  6. 六、Access Token 和 Refresh Token
  7. 七、Session 和 Token 怎么选择
  8. 八、单点登录 SSO
  9. 九、密码、OAuth 和现代认证
  10. 十、常见错误
  11. 总结
  12. 参考资料

前端鉴权的兄弟们:Cookie、Session、Token、JWT、单点登录

Category(分类): Browser, Security, Authentication Status: 已更新

本文保留原文的主线:从 HTTP 无状态讲到 Cookie、服务端 Session、Token、JWT、Refresh Token 和 SSO。旧文中把“Token 等于客户端保存全部状态”、把 Cookie 属性和跨域行为说得过于绝对的部分,统一按现代浏览器和 OAuth/OIDC 实践修正。

认证(Authentication)回答“你是谁”;授权(Authorization)回答“你能做什么”。本文重点讨论认证凭证和会话,业务授权仍必须由服务端执行。

一、从 HTTP 无状态说起

HTTP 请求不会自动记住上一次请求对应的用户。无状态描述的是应用层的请求上下文,而不是说每次请求都要重新建立 TCP 连接:HTTP/1.1 Keep-Alive、HTTP/2 多路复用和 HTTP/3/QUIC 都可以复用连接。

登录系统需要在后续请求中携带一个凭证:

登录凭证 -> 浏览器保存/发送 -> 服务端验证 -> 识别主体 -> 执行授权

凭证可以是:

  • 服务端 Session ID;
  • 不透明的 Access Token;
  • 签名 JWT;
  • OAuth/OIDC 授权码换来的本地会话;
  • Passkey/WebAuthn 验证后创建的会话。

二、Cookie:浏览器自动携带的凭证容器

Cookie 的典型流程是:

  1. 登录接口通过 Set-Cookie 写入 Cookie;
  2. 浏览器根据域、路径、Secure、SameSite 和过期时间筛选;
  3. 后续匹配的 HTTP 请求自动带上 Cookie 请求头;
  4. 服务端解析 Cookie 并验证其中的会话标识。

Cookie 和服务端会话的基本流程

1. Domain 和 Path

Set-Cookie: session=abc; Domain=example.com; Path=/app
  • 不设置 Domain 时是 host-only Cookie,只发送给设置它的主机,不会自动覆盖所有子域;
  • 设置 Domain=example.com 后,符合规则的子域可能也会收到它;
  • Path 限制发送路径,但不是强安全边界,不能用它隔离高权限和低权限应用;
  • 不要为了“跨子域共享”而随意把会话 Cookie 设置到顶级主域,低信任子域被接管后可能影响整个主域。

2. Expires 和 Max-Age

Set-Cookie: session=abc; Max-Age=1800
  • Max-Age 表示从现在开始的秒数;
  • Expires 是绝对日期,兼容旧客户端;
  • 两者同时存在时通常 Max-Age 优先;
  • 两者都没有时,通常是会话 Cookie,但浏览器对“关闭浏览器”的处理和会话恢复会影响直觉;
  • 退出登录不能只等 Cookie 过期,服务端也要撤销或删除会话。

3. Secure、HttpOnly 和 SameSite

推荐的会话 Cookie 形式:

Set-Cookie: __Host-session=RANDOM_ID; Path=/; Max-Age=1800; Secure; HttpOnly; SameSite=Lax
  • Secure:只通过 HTTPS 发送;
  • HttpOnly:禁止 JavaScript 通过 document.cookie 读取;
  • SameSite=Lax/Strict/None:控制跨站请求携带规则。SameSite=None 必须同时使用 Secure
  • __Host- 前缀要求 SecurePath=/ 且不能设置 Domain,可降低错误的跨子域覆盖。

HttpOnly 不能阻止 XSS 代码借助当前页面发起已认证请求,所以仍要做输出编码、CSP、依赖安全和 CSRF 防护。Cookie 自动携带也不意味着“必然无法防御 CSRF”,可以组合 SameSite、CSRF Token、Origin/Referer 校验和重新认证。

服务端可以一次响应多个 Cookie,每个 Cookie 使用单独的 Set-Cookie 头:

Set-Cookie: theme=dark; Path=/
Set-Cookie: session=abc; Path=/; HttpOnly; Secure; SameSite=Lax

请求头会把符合条件的非过期 Cookie 合并:

Cookie: theme=dark; session=abc

JavaScript 只能读取和设置非 HttpOnly Cookie,而且一次 document.cookie = ... 只设置一条:

document.cookie = 'theme=dark; Path=/; SameSite=Lax'
console.log(document.cookie)

前端不能通过 document.cookie 设置 HttpOnly。原文把 HttpOnly 写在前端赋值示例中是不正确的。

三、服务端 Session:Cookie 只保存 ID

Session 方案把状态保存在服务端,浏览器只保存一个不可预测的 Session ID:

  1. 用户提交账号密码;
  2. 服务端验证密码哈希、账号状态和风险策略;
  3. 服务端生成高熵 Session ID,并把用户、租户、过期时间、会话版本等写入 Session Store;
  4. 通过 HttpOnly Cookie 返回 Session ID;
  5. 后续请求自动带 Cookie;
  6. 服务端查找并验证 Session,再执行资源授权。

1. Session 存储

  • 进程内存:开发简单,重启即失效,不适合多实例;
  • Redis 等共享存储:常见的生产方案,适合 TTL、撤销和多实例;
  • 数据库:便于审计和持久化,但需要关注访问性能;
  • 加密/签名 Cookie Session:适合少量状态,但 Cookie 大小、密钥轮换、撤销和隐私仍需设计。

“Session 一定会把服务器压垮”并不成立。共享 Session Store、过期淘汰、水平扩展和合理 TTL 可以支撑大量会话。负载均衡的 IP Hash 只是旧式选择,会降低均衡效果,也不能解决机器宕机;通常优先使用共享存储。

2. Session 安全

  • 登录成功后轮换 ID,防止 Session Fixation;
  • ID 使用密码学安全随机数,不使用用户 ID、时间戳或自增数字;
  • 设置空闲超时和绝对过期时间;
  • 登出、改密、封禁、权限提升时撤销或轮换会话;
  • 重要操作可以要求重新认证或 MFA;
  • 服务端不要把密码明文写入 Session、日志或 Token;
  • 记录设备、会话创建时间、撤销原因和必要的审计信息。

Node/Express 中可使用成熟的 session 中间件,但生产环境不要使用默认的内存 Store:

app.use(session({
  name: '__Host-session',
  secret: process.env.SESSION_SECRET,
  store: redisStore,
  resave: false,
  saveUninitialized: false,
  cookie: {
    path: '/',
    httpOnly: true,
    secure: true,
    sameSite: 'lax',
    maxAge: 30 * 60 * 1000
  }
}))

示例中的 redisStore 需要选择与框架版本匹配的正式实现,并配置连接、故障处理和密钥管理。

四、Token:凭证不等于 JWT

Token 是凭证的泛称,可以是随机字符串,也可以是自包含的签名结构。它不一定通过 Cookie 传输,也不一定把所有用户信息放在客户端。

Token 登录流程

常见传输方式:

Authorization: Bearer ACCESS_TOKEN

或者:

Cookie: __Host-session=SESSION_ID

1. Opaque Token 和自包含 Token

Opaque Token 只是不可预测的随机值,服务端根据它查询状态,和 Session ID 很像;优点是容易撤销、内容不泄露,缺点是需要服务端存储或查询。

自包含 Token 中包含声明,服务端可以验证签名后读取,JWT 是最常见格式;它减少部分查询,但撤销、权限变更、Refresh Token、密钥轮换仍然需要服务端策略。

因此不要简单写成:

Session = 服务端有状态
Token   = 服务端完全无状态

真实系统经常是混合的:短期 JWT Access Token + 服务端存储的 Refresh Token,或者 OAuth Token + 本站 HttpOnly Session。

2. Token 保存位置

位置特点
内存XSS 持久性较低,刷新后丢失
localStorage方便但同源 XSS 可读取,不适合长期高价值令牌
HttpOnly CookieJavaScript 不能读取,但自动发送,需要 CSRF 防护
IndexedDB结构化持久化,但同样受同源脚本访问,不能抵御 XSS

若浏览器应用不需要直接访问第三方 API,常用做法是 BFF 或服务端 Session,让 Token 留在服务端。若必须由 SPA 持有 Token,应使用短期 Access Token、Refresh Token Rotation、严格 CSP 和撤销策略,不能通过 URL 传递。

五、Token 编码、签名和加密

1. Base64 不是加密

把 JSON 进行 Base64URL 编码只能改变表现形式:

const encoded = btoa(JSON.stringify({ userId: 'user-42' }))

任何拿到结果的人都可以解码,用户也能修改内容。Base64 不能证明内容可信,也不能隐藏秘密。

Base64 只编码不加密的示意

2. 签名防篡改

可以使用 HMAC 或非对称数字签名:

  • HS256/HMAC:签发和验证使用同一共享密钥,验证方也有生成合法 Token 的能力;
  • RS256/PS256/ES256:私钥签发、公钥验证,适合多个服务只需要验证的场景;
  • 签名用于检测篡改,不等于加密;
  • 服务端必须固定允许算法,不能盲信 Token header 的 alg
  • 验证 issaudexpnbfiatjti 和业务权限。

原文把 HMAC 说成“用私钥加密”是不准确的:HMAC 使用共享密钥;非对称签名才有私钥和公钥的角色。

import { jwtVerify } from 'jose'

const key = new TextEncoder().encode(process.env.JWT_SECRET)

const { payload } = await jwtVerify(token, key, {
  algorithms: ['HS256'],
  issuer: 'https://id.example.com',
  audience: 'api.example.com'
})

生产代码应使用维护良好的库、密钥轮换和 JWKS 缓存,不要手写完整 JWT 验证器。

签名 Token 防篡改示意

3. JWT 的结构

签名 JWT(JWS)通常是三段:

base64url(header).base64url(payload).base64url(signature)

示例 payload:

{
  "iss": "https://id.example.com",
  "sub": "user-42",
  "aud": "api.example.com",
  "exp": 1893456000,
  "iat": 1893452400,
  "scope": "article:read"
}

JWT 结构示意

JWT payload 默认可读,不要放密码、私钥或不应暴露的个人资料。若确实需要保密,应讨论 JWE 或更合适的服务端会话,而不是把 JWS 当加密。

六、Access Token 和 Refresh Token

Access Token 用于访问业务 API,通常应较短;Refresh Token 用于换取新的 Access Token,生命周期更长但访问频率更低。

Access Token  -> 调用 API,短期
Refresh Token -> Token Endpoint,换新 Access Token

Access Token 和 Refresh Token 刷新流程

推荐实践:

  • Refresh Token 只发给需要它的客户端类型;
  • 使用 Refresh Token Rotation,每次刷新都作废旧 Refresh Token;
  • 服务端检测到旧 Refresh Token 重用时,撤销整个令牌族;
  • 服务端存储 Refresh Token 的哈希、设备、客户端、过期时间和撤销状态;
  • 不把 Token 放在 URL、日志、Referer 和错误追踪数据中;
  • 访问令牌过期时只尝试一次刷新,多个标签页需要协调刷新锁,避免刷新风暴;
  • Refresh Token 失效后让用户重新认证。

七、Session 和 Token 怎么选择

维度服务端 SessionOpaque/JWT Token
浏览器保存通常是 HttpOnly CookieCookie、内存或 Authorization 头
服务端状态Session StoreOpaque 需要;JWT 验证本身可少查一次
立即撤销直接删除/撤销 SessionJWT 需要版本、黑名单或短过期;Refresh Token 可撤销
CSRFCookie 自动发送,需要防 CSRFAuthorization 头由脚本主动设置,可降低 CSRF,但 XSS 风险仍在
扩展需要共享 Session Store需要密钥、JWKS、撤销和刷新体系
适合同域 Web、BFF、管理后台移动端、第三方 API、服务间调用、OAuth/OIDC

没有“Token 一定比 Session 高级”的结论。优先根据浏览器、移动端、服务间调用、撤销要求、隐私和运维能力选择。

八、单点登录 SSO

1. 同一主域并不等于好的 SSO

过去常见做法是把 Cookie 的 Domain 设置为主域,让 a.example.comb.example.com 共享:

Set-Cookie: session=...; Domain=example.com

这不是跨不同顶级域的通用 SSO,而且会扩大 Cookie 的信任范围:任意低信任子域都可能影响主域 Cookie。现代系统更常让每个业务应用拥有自己的本地会话,由统一 IdP 通过 OIDC 提供登录体验。

2. 不同域名的 SSO

不同域名之间的 SSO 授权流程

推荐的 Authorization Code + PKCE/OIDC 流程:

  1. 用户访问 A 系统,没有 A 的本地会话;
  2. A 将浏览器重定向到 IdP,携带 client_id、精确 redirect_uristatenonce 和 PKCE code_challenge
  3. IdP 没有自己的会话时要求用户登录;
  4. 登录成功后重定向回 A 的 callback,并只携带一次性授权码;
  5. A 的后端用授权码和 code_verifier 与 IdP 换取 Token;
  6. A 验证 issuer、audience、签名、nonce、PKCE 和 code,再创建 A 自己的 HttpOnly Session;
  7. 用户访问 B 时,B 重复相同过程;因为 IdP 已经有自己的会话,用户通常不需要再次输入密码;
  8. A 和 B 各自执行自己的业务授权,不能因为 SSO 登录就成为管理员。

授权码可以短暂出现在回调 URL 中,但应使用 HTTPS、一次性、短期、精确回调地址,并在换取后清理 URL,不能直接把长期 Token 放在 URL。

3. 登出

单点退出需要分别考虑:

  • 清除业务系统本地 Session;
  • 撤销 Refresh Token;
  • 调用 OIDC RP-Initiated Logout 或提供商撤销端点;
  • 如果 IdP 支持,使用 Front-Channel/Back-Channel Logout;
  • 处理其他设备和并发会话。

只删除浏览器本地 Cookie,不一定会让服务器会话、IdP 会话和其他设备退出。

九、密码、OAuth 和现代认证

  • 密码服务端使用 Argon2id、bcrypt 或 scrypt 哈希,不能明文保存;
  • 登录、重置密码和 MFA 接口需要限速、审计和风险控制;
  • OAuth 2.0 是授权委托,登录身份应使用 OIDC 并验证 ID Token;
  • 新的浏览器应用优先 Authorization Code + PKCE,不使用旧式 Implicit Flow;
  • client_secret 不能放在纯前端应用中;
  • Passkey/WebAuthn 使用公钥凭证,私钥留在验证器,能提高抗钓鱼能力。

十、常见错误

  1. 把 Cookie 认为只能防 Session,不能防 CSRF:Cookie 自动发送会产生风险,但 SameSite、CSRF Token 和 Origin 校验可以组合防护;
  2. 把 localStorage 认为比 Cookie 安全:它减少自动携带,但 XSS 可以读取;
  3. 把 Base64 当加密:编码没有保密性和完整性;
  4. 把 JWT 当成永远不需要服务端状态的 Session:撤销、刷新、权限版本和审计仍需要服务端;
  5. 把 Domain Cookie 当作跨域 SSO:不同源不能直接共享 Cookie,扩大 Domain 还有子域风险;
  6. 收到 JWT 就信任 role=admin:必须验证签名和服务端授权策略;
  7. 把第三方 Access Token 直接当本站登录态:应绑定第三方主体,再创建本站会话;
  8. 把刷新 Token 无限重试:会造成刷新风暴,应该处理并发和失败状态。

总结

  • Cookie 是浏览器自动携带的凭证容器,不等于完整的会话方案;
  • Session 通常让 Cookie 保存 ID、服务端保存状态,生产环境用共享 Store 解决集群问题;
  • Token 是泛称,可以是 opaque token 或 JWT;
  • Base64 不是加密,HMAC 共享密钥和非对称签名的密钥模型不同;
  • Access Token 短期使用,Refresh Token 要轮换、撤销和检测重用;
  • SSO 应优先采用 OAuth 2.0 Authorization Code + PKCE 与 OIDC;
  • 每个业务系统仍需独立维护会话、授权、撤销和审计。

参考资料

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS