共计 2441 个字符,预计需要花费 7 分钟才能阅读完成。
痛点分析:为什么传统方案在分布式系统中不够用
在开发 Bili Sync 这类多端同步应用时,前端认证 Token 的管理一直是让人头疼的问题。传统的 localStorage 和 sessionStorage 方案在单机环境下还能勉强应付,但在分布式系统中就显得力不从心了。让我们先看看这些传统方案的局限性:

- XSS 安全风险 :localStorage 完全暴露在 JavaScript 环境中,一旦发生 XSS 攻击,攻击者可以轻易获取用户的认证 Token。
- 多标签页同步问题 :当用户在多个浏览器标签页中操作时,一个标签页更新的 Token 无法实时同步到其他标签页,导致用户需要频繁重新登录。
- 移动端兼容性问题 :iOS WebView 对 localStorage 的大小有限制(通常只有 5MB 左右),而且可能被系统自动清除。
技术选型:JWT + Redis 组合方案
经过对各种方案的对比评估,我们最终选择了 JWT + Redis 的组合方案。这里分享一下我们的思考过程:
- 为什么选择 JWT
- 自包含特性:JWT 本身就包含了用户信息和过期时间,减少了后端查询数据库的次数
- 无状态:服务端不需要维护 session 状态,适合分布式架构
-
灵活性强:可以自定义 claims 来携带业务需要的额外信息
-
为什么选择 Redis
- 高性能:Redis 的读写速度极快,适合 Token 这种高频访问的数据
- 发布 / 订阅机制:天然支持多端实时同步
-
自动过期:原生支持设置 key 的过期时间,与 Token 过期机制完美契合
-
对比 Cookie HttpOnly 方案
- 优点:更安全,防止 XSS 攻击获取 Cookie
- 缺点:
- 跨域限制严格
- 无法在前端灵活控制
- 同步多个子域名时配置复杂
核心实现:从加密存储到实时同步
前端加密存储实现
我们使用 crypto-js 对敏感信息进行加密后再存入 localStorage:
import CryptoJS from 'crypto-js';
const SECRET_KEY = 'your-32-byte-secret-key';
// 加密函数
export function encryptData(data: string): string {return CryptoJS.AES.encrypt(data, SECRET_KEY).toString();}
// 解密函数
export function decryptData(ciphertext: string): string | null {
try {const bytes = CryptoJS.AES.decrypt(ciphertext, SECRET_KEY);
return bytes.toString(CryptoJS.enc.Utf8);
} catch (e) {console.error('解密失败', e);
return null;
}
}
// React 示例:安全存储 Token
const saveToken = (token: string) => {const encrypted = encryptData(token);
localStorage.setItem('auth_token', encrypted);
};
Redis 发布 / 订阅架构
我们使用 Redis 的发布 / 订阅机制来实现多端实时同步,架构如下图所示:
graph TD
A[客户端 A] -->| 发布 Token 更新 | B[Redis Channel]
B -->| 订阅更新 | C[客户端 B]
B -->| 订阅更新 | D[客户端 C]
E[认证服务] -->| 签发 Token| B
具体实现流程:
- 用户登录后,认证服务生成 JWT Token 并存入 Redis(设置过期时间)
- 服务端通过 Redis 频道发布 Token 更新事件
- 所有客户端订阅该频道,收到更新后同步本地存储
安全加固:从防御到优化
Token 过期与刷新机制
我们采用双 Token 机制来平衡安全性和用户体验:
- Access Token:短有效期(15 分钟),用于 API 鉴权
- Refresh Token:长有效期(7 天),存储在 HttpOnly Cookie 中,用于获取新的 Access Token
刷新流程:
- 前端检测到 Access Token 过期
- 静默发送 Refresh Token 到 /refresh 端点
- 服务端验证后返回新的 Access Token
- 通过 Redis 发布新 Token 到所有客户端
CSRF 防护措施
虽然 JWT 本身不容易受到 CSRF 攻击,但我们还是采取了额外防护:
- 关键操作(如修改密码)需要二次验证
- 重要 API 请求必须包含自定义 Header(如 X-Requested-With)
- 对敏感操作使用 POST/PUT/DELETE 方法而非 GET
避坑指南:实战经验分享
iOS WebView 的特殊处理
在 iOS WebView 中,我们遇到了 localStorage 被意外清除的问题。解决方案是:
- 优先尝试使用 sessionStorage
- 实现 fallback 机制,当存储失败时降级到内存存储
- 定期检查存储状态,必要时重新从服务端获取
避免 Redis 缓存雪崩
为防止大量 Token 同时过期导致雪崩,我们采用:
- 随机化 Token 过期时间(基础时间 ± 随机偏移量)
- 提前续期机制:在 Token 过期前 5 分钟自动续期
- 多级缓存:本地内存缓存 + Redis + 数据库
性能优化:让同步更快更稳
压力测试结果
经过优化,我们在以下环境下达到了 <100ms 的同步延迟:
- 测试环境:1000 并发用户
- Redis 集群:3 主 3 从
- 网络延迟:<50ms
关键优化点:
- 使用 Redis Pipeline 批量处理订阅消息
- 前端实现消息去重(基于消息 ID)
- 压缩 Token 体积(移除不必要的信息)
内存占用优化
对于大用户量场景,我们采用了:
- Token 分片存储:按用户 ID 哈希分配到不同 Redis 实例
- Lazy 清理:只在访问时检查 Token 是否过期
- 定期扫描:每天凌晨清理过期 Token
总结与思考
这套方案在实际运行中表现良好,成功解决了我们遇到的 Token 管理和同步问题。当然,安全是一个持续的过程,我们仍在不断改进。
留给读者的思考题:如何实现 Token 的主动失效能力?比如当用户修改密码后,如何立即让所有设备的旧 Token 失效?
