Cookie、Session 与 Token

Cookie 是浏览器按域名和路径管理的小型键值数据。服务端通过 Set-Cookie 写入,符合条件时浏览器在后续请求中自动携带 Cookie 请求头。

常见属性:

  • Expires / Max-Age:有效期。
  • Domain / Path:发送范围。
  • Secure:仅通过 HTTPS 发送。
  • HttpOnly:禁止 JavaScript 读取,降低 Token 被 XSS 窃取的风险。
  • SameSite:限制跨站请求携带 Cookie。

SameSite=None 必须同时设置 Secure

Session

传统 Session 流程:

  1. 用户登录成功,服务端创建 Session。
  2. 浏览器保存只包含 Session ID 的 Cookie。
  3. 后续请求自动携带 Session ID。
  4. 服务端查询 Session 得到用户状态。

优点是服务端可以主动吊销;缺点是集群环境需要共享 Session、粘性会话或集中存储。

Session 与 Token 的优缺点对比

一句话区别:

  • Session:凭证本身通常只是 Session ID,用户状态主要存在服务端。
  • Token:凭证本身可以携带身份和权限信息,客户端每次请求主动带上,服务端验证 Token 判断身份。
对比项SessionToken
状态保存服务端保存用户状态,客户端只保存 Session ID可以把状态放在 Token 里,也可以只把 Token 当作查询 ID
服务端控制容易主动失效、踢下线、管理多端登录纯无状态 Token 难以及时吊销,需要黑名单、版本号或短过期时间
扩展性多机器部署需要共享 Session、粘性会话或集中存储业务服务只验签时扩展性好,适合跨服务、跨端调用
请求开销Cookie 里的 Session ID 通常较短JWT 往往更长,每次请求都会带来额外流量
安全风险Session ID 泄露后也会被冒用;Cookie 自动携带,需要考虑 CSRFBearer Token 泄露后可直接冒用;存储在 localStorage 时更怕 XSS
权限变更服务端状态实时可控,修改后容易立即生效如果权限写进 Token,旧 Token 可能在过期前仍保留旧权限
适用客户端浏览器 Web 应用非常常见Web、移动端、开放 API、微服务调用都常见
实现复杂度单体应用简单;分布式后需要 Session 存储方案简单使用不难;安全刷新、吊销、轮换会增加复杂度

Session 的优点:

  • 服务端掌控会话状态,退出登录、封禁账号、踢设备都比较直接。
  • 客户端只保存无业务含义的 Session ID,敏感信息不暴露给客户端。
  • 对传统浏览器应用友好,配合 HttpOnlySecureSameSite Cookie 可以减少凭证被脚本读取的风险。

Session 的缺点:

  • 服务端需要保存会话状态,请求量大时会增加存储和查询压力。
  • 多实例部署时要解决 Session 共享问题,例如 Redis、数据库、粘性会话。
  • Cookie 自动携带,请求有副作用时要额外做好 CSRF 防护。

Token 的优点:

  • 可以天然适配前后端分离、移动端和第三方 API,客户端显式放到请求头即可。
  • JWT 这类自包含 Token 可以减少服务端会话查询,业务服务只要拿到公钥就能验证签名。
  • 适合统一认证中心签发,多业务系统或微服务共同验证。

Token 的缺点:

  • Bearer Token 一旦泄露,在有效期内通常可以被直接冒用。
  • 纯无状态 Token 不容易立即失效,注销、封禁、权限回收需要额外设计。
  • JWT payload 默认不加密,不能放敏感信息。
  • Token 过长会增加请求体积;把大量权限信息写进 Token 也容易造成权限陈旧。

面试里可以这样总结:Session 更偏“服务端有状态会话”,控制力强,但分布式扩展要处理共享状态;Token 更偏“客户端携带凭证”,跨端和跨服务更灵活,但泄露、吊销、刷新和权限实时性要设计好。实际项目不是二选一,常见方案是 HttpOnly Cookie + 服务端会话,或者 Access Token + Refresh Token + 服务端吊销状态

Token 与 JWT

Token 是服务端签发给客户端的一段凭证。客户端后续请求携带它,服务端据此识别“请求者是谁、是否登录、能访问哪些资源”。和传统 Session ID 相比,Token 常见于前后端分离、移动端、开放 API、微服务调用等场景。

最常见的携带方式是放在请求头:

Authorization: Bearer <access_token>

Bearer 的意思是“持有者凭证”:谁持有这段 Token,谁就可以代表对应身份发起请求。因此 Token 一旦泄露,攻击者通常不需要知道密码,也能在有效期内冒充用户。

Token 本身可以有不同形态:

类型特点服务端如何验证
随机字符串 Token内容没有业务含义,只是一个高随机性的 ID到数据库、Redis 或认证服务查询状态
JWT自包含,内部带有用户、过期时间、签发者等声明,并带签名用密钥或公钥验证签名,再检查声明
自定义签名 Token业务自定义字段加签按约定解析字段并验证签名

JWT 是一种 Token 格式,不等于完整认证方案。一个 JWT 通常由三段组成:

header.payload.signature
  • header:声明算法和类型,例如 algtyp
  • payload:声明信息,例如用户 ID、过期时间、签发者、受众、权限范围。
  • signature:对前两段做签名,防止内容被篡改。

JWT 的 payload 默认只是 Base64URL 编码,不是加密,不能存放密码、身份证号、银行卡号等敏感信息。签名用于校验完整性和签发者,并不会隐藏内容。任何拿到 JWT 的人都可以解码看到 payload。

JWT 常见声明:

声明含义
subSubject,主体,通常是用户 ID
expExpiration Time,过期时间
iatIssued At,签发时间
nbfNot Before,早于该时间不可用
issIssuer,签发者
audAudience,接收方 / 使用方
jtiJWT ID,Token 唯一标识,常用于吊销或追踪
scope / roles权限范围或角色

服务端验证 Token 时不能只看“能不能解码”,至少要检查:

  1. 签名是否合法,算法是否符合预期。
  2. 是否过期,nbfiat 等时间声明是否合理。
  3. issaud 是否匹配当前系统。
  4. 用户是否仍然存在,账号是否被禁用。
  5. 权限范围是否满足当前接口要求。
  6. 如果支持吊销,还要检查黑名单、版本号或会话状态。

一个常见误区是“用了 JWT 就不需要查数据库”。JWT 可以减少每次请求查询 Session 的成本,但并不意味着永远不查服务端状态。用户禁用、密码修改、权限变更、主动退出、多设备管理、风控封禁等场景,都可能需要服务端状态参与校验。

签名算法也要注意边界:

  • 对称签名,例如 HS256,签发和验证使用同一个密钥,适合单体服务或少量可信服务。
  • 非对称签名,例如 RS256ES256,认证服务用私钥签发,业务服务用公钥验证,更适合多服务验证。

业务代码不应该盲目信任 Token 里声明的算法,应该在服务端固定允许的算法列表。否则历史上出现过把 alg=none 或算法混淆当成合法输入的安全问题。

  • JavaScript 无法读取,降低凭证被 XSS 直接窃取的风险。
  • 浏览器自动携带,需要正确设置 SameSite 并考虑 CSRF。

localStorage

  • 不会自动随请求发送,CSRF 风险模型不同。
  • 任意成功执行的同源 XSS 都能读取并外传 Token。

不存在脱离场景的绝对答案。高价值 Web 会话通常优先考虑 Secure + HttpOnly + SameSite Cookie,并同时做好 XSS、CSRF 和服务端鉴权。

Access Token 与 Refresh Token

  • Access Token:有效期短,用于业务接口。
  • Refresh Token:有效期更长,只用于换取新的 Access Token,权限和暴露面应更小。

典型流程:

  1. 用户登录成功,认证服务签发 Access Token 和 Refresh Token。
  2. 客户端访问业务接口时携带 Access Token。
  3. Access Token 过期后,客户端使用 Refresh Token 调用刷新接口。
  4. 刷新成功后,服务端返回新的 Access Token,必要时也轮换 Refresh Token。
  5. 刷新失败时,客户端清理本地身份并跳转登录。

Access Token 应该短有效期、少权限、只面向业务接口。这样即使泄露,攻击窗口也相对有限。Refresh Token 有效期更长,价值更高,最好只发送到专门的刷新接口,不用于普通业务请求。

Refresh Token 常见设计:

设计说明
固定 Refresh Token登录后长期不变,实现简单,但泄露后风险较高
轮换 Refresh Token每次刷新都返回新的 Refresh Token,旧的立即失效
复用检测旧 Refresh Token 被再次使用时,说明可能泄露,可以吊销整条会话
设备维度管理每台设备维护独立会话,便于单独退出或风控

多个请求同时发现 Token 过期时,应使用“单飞锁”:只允许一个刷新请求执行,其他请求等待同一个 Promise,避免刷新风暴。

刷新失败后要清理本地身份并跳转登录,不能无限重试。

Token 吊销有几种常见做法:

做法适用场景代价
短过期时间普通 Access Token无法立即失效,只能缩短风险窗口
黑名单 / denylist退出登录、封禁、泄露应急每次请求需要查状态或缓存
Token 版本号密码修改、权限整体刷新需要维护用户维度版本
服务端保存会话高价值系统、多设备管理更接近 Session,需要集中存储

所以“JWT 无法注销”并不准确。更准确的说法是:纯无状态 JWT 无法在过期前被服务端主动失效;如果引入黑名单、版本号或会话表,就可以注销,但也会重新引入服务端状态。

前端还需要处理几个细节:

  • 不要把长期有效的 Token 放进 URL,URL 容易进入浏览器历史、日志、Referer。
  • 不要只依赖前端路由守卫判断登录,所有敏感接口都必须在服务端鉴权。
  • 刷新接口应限制频率,并区分“过期”“无效”“被吊销”等错误。
  • 权限变更后,服务端不能只相信旧 Token 里的角色声明;高风险接口应实时校验权限。
  • 退出登录时,客户端清理本地凭证,服务端也应尽量吊销 Refresh Token 或当前会话。

高频追问

  • 删除 Cookie 为什么必须匹配 Domain 和 Path?
  • HttpOnly 能否防止 XSS 发起恶意请求?
  • JWT 如何注销?
  • JWT payload 能不能放用户隐私信息?
  • Session 和 Token 各自适合什么场景?
  • Token 相比 Session 一定更安全吗?
  • Access Token 为什么要短有效期?
  • Refresh Token 为什么建议轮换?
  • 跨子域登录如何设计?
  • 为什么前端路由守卫不能代替服务端鉴权?