前端登录态与 Token 管理:Cookie、BFF、刷新和退出
本文目录
登录成功只代表服务端建立了某种身份凭据,不代表把返回字符串存进浏览器后就完成认证。Token、Session ID 和 Refresh Token 都是持有者凭据:拿到它的人可能就能代表用户发请求。先选清楚浏览器与服务端如何维持会话,再设计保存、刷新、撤销和异常恢复。
先选择会话架构
| 方案 | 适合情况 | 主要边界 |
|---|---|---|
| 服务端 Session + HttpOnly Cookie | 同站 Web 应用、需要即时撤销和集中会话管理 | 服务端保存会话;写请求仍要做 CSRF 防护 |
| BFF 代持 OAuth Token | 前端不应接触上游访问令牌的 Web 应用 | 浏览器只持有 BFF 会话 Cookie;BFF 负责服务端 Token 生命周期 |
| 纯浏览器 OAuth 公共客户端 | 必须由浏览器直接访问资源服务器的 SPA | 使用 Authorization Code + PKCE;严格缩短凭据暴露窗口并控制授权范围 |
优先评估服务端 Session 或 BFF。不要为了“前后端分离”就默认把访问令牌和刷新令牌交给 JavaScript。若必须采用浏览器 OAuth 客户端,应按当前 OAuth 安全最佳实践实现 PKCE、精确 redirect URI、受限 scope、短时 access token 和刷新令牌轮换;不能把客户端密钥放进前端包。
JWT 只是声明格式与签名机制,不等于加密,也不天然等于安全会话。Base64URL 解码只是读取内容,不能证明签名、受众、签发方、有效期或权限可信。服务端每次都应验证签名算法白名单、iss、aud、exp、nbf 及所需授权,并考虑撤销、密钥轮换和权限变化。
浏览器凭据存储
不要把认证凭据放进 localStorage、sessionStorage、IndexedDB、URL、页面 DOM 或日志。任何同源 JavaScript 都可能读取 Web Storage;一次 XSS 就足以窃取持久凭据。URL 还可能进入浏览历史、Referer、代理日志和截图。
同站会话 Cookie 通常设置 Secure; HttpOnly; SameSite=Lax,并尽量使用 host-only 范围和窄 Path。HttpOnly 降低脚本直接读出 Cookie 的风险,但不能阻止已经执行的恶意脚本以用户身份发请求;SameSite 是纵深防御,不自动替代 CSRF token、Origin/Referer 校验和安全的请求方法设计。若业务必须跨站发送 Cookie,需要重新评估 CSRF、第三方 Cookie 限制,并在需要时配合 SameSite=None; Secure。
Cookie 中存放会话标识并不意味着可以不做安全设计:限制生命周期与权限、服务端轮换和撤销会话 ID、设置空闲/绝对超时、对敏感操作重新认证。响应中含敏感会话信息时正确使用 Cache-Control: no-store,同时避免 CDN 缓存个性化响应。
登录、刷新与请求错误
一个可维护的登录链路通常是:
- 登录接口验证账号密码或一次性邮箱验证码,服务端限速并返回通用错误,避免暴露账号是否存在。
- 成功后创建新的会话,防止登录前后的 Session Fixation;Cookie 属性由服务端设置,前端不读取凭据值。
- 请求层默认携带同源凭据;敏感写操作附带 CSRF 防护。授权判断始终在服务端完成。
401表示当前请求没有可接受的身份凭据,可尝试一次会话恢复;403表示身份已识别但不允许执行,不应无限刷新或自动重试。- 并发请求遇到过期时只进行一次共享刷新;刷新成功后只重试符合幂等条件的请求。不要在无限循环里重复提交支付、上传或其他非幂等操作。
- 刷新失败、会话撤销或账号切换时,清理应用内用户数据和未完成请求,返回登录入口并保留安全的站内目标地址。
若旧系统仍使用 Bearer Token:优先让 BFF 在服务端保存长期凭据。不得不由浏览器持有时,减少可读凭据的持久时间,避免长期 Refresh Token 暴露给脚本,限制 scope 和 audience;Refresh Token 应轮换并能检测重用。内存存储只能降低跨页面持久暴露,不会阻止正在执行的 XSS 代替用户发请求。
多标签页与退出
跨标签页只广播 logout、session-changed 这类非敏感事件,不广播 Token、Cookie、邮箱验证码或用户正文。可用 BroadcastChannel,旧环境以不含凭据的 storage 事件作降级。每个标签收到退出事件后清理本地 query/cache、停止私有任务并跳转;服务端仍须真正撤销会话。只删除浏览器 Cookie 而不让服务端使会话失效,不能撤销已被复制的凭据。
退出还应处理 Service Worker/Cache API、IndexedDB、内存缓存、WebSocket 和待上传队列中属于当前用户的数据。切换账号时清理旧主体缓存,确保页面返回、预取请求与其他标签不能重新展示前一用户的私有内容。
前端路由不是授权边界
路由守卫和菜单只负责让合法体验更清楚。隐藏按钮、客户端角色字段和难猜的 URL 都不能保护 API。后端对每个读取、下载、编辑、发布、导出和版本历史请求验证主体、动作与目标对象;默认拒绝,并避免在未授权列表、搜索和附件接口中泄露隐藏资源。
回归检查
- 无 Cookie、过期会话、撤销会话、畸形 Cookie、权限变化分别得到可区分且不泄露信息的处理。
- 恶意跨站表单不能触发状态变更;所有改变状态的接口不使用 GET。
- 并发过期请求只刷新一次,刷新失败后停止重试,非幂等请求不会被重复执行。
- 多标签登出、切换账号、前进/返回、刷新和离线恢复均不会显示旧账号数据。
- 检查浏览器存储、URL、日志、错误上报和分析平台,没有 Token、验证码、Cookie 或敏感正文。
- 用匿名、普通用户、只读和管理员直连 API 验证对象级权限,而不只检查页面按钮。
参考:OWASP Session Management Cheat Sheet、OWASP HTML5 Security Cheat Sheet、OWASP CSRF Prevention Cheat Sheet、RFC 6750 Bearer Token Usage、RFC 9700 OAuth 2.0 Security Best Current Practice。
Live Notes
留言提交后会先进入审核,审核通过后对其他读者公开。