Bili Sync 前端认证 Token 的安全存储与高效同步方案

1次阅读
没有评论

共计 2441 个字符,预计需要花费 7 分钟才能阅读完成。

image.webp

痛点分析:为什么传统方案在分布式系统中不够用

在开发 Bili Sync 这类多端同步应用时,前端认证 Token 的管理一直是让人头疼的问题。传统的 localStorage 和 sessionStorage 方案在单机环境下还能勉强应付,但在分布式系统中就显得力不从心了。让我们先看看这些传统方案的局限性:

Bili Sync 前端认证 Token 的安全存储与高效同步方案

  • XSS 安全风险 :localStorage 完全暴露在 JavaScript 环境中,一旦发生 XSS 攻击,攻击者可以轻易获取用户的认证 Token。
  • 多标签页同步问题 :当用户在多个浏览器标签页中操作时,一个标签页更新的 Token 无法实时同步到其他标签页,导致用户需要频繁重新登录。
  • 移动端兼容性问题 :iOS WebView 对 localStorage 的大小有限制(通常只有 5MB 左右),而且可能被系统自动清除。

技术选型:JWT + Redis 组合方案

经过对各种方案的对比评估,我们最终选择了 JWT + Redis 的组合方案。这里分享一下我们的思考过程:

  1. 为什么选择 JWT
  2. 自包含特性:JWT 本身就包含了用户信息和过期时间,减少了后端查询数据库的次数
  3. 无状态:服务端不需要维护 session 状态,适合分布式架构
  4. 灵活性强:可以自定义 claims 来携带业务需要的额外信息

  5. 为什么选择 Redis

  6. 高性能:Redis 的读写速度极快,适合 Token 这种高频访问的数据
  7. 发布 / 订阅机制:天然支持多端实时同步
  8. 自动过期:原生支持设置 key 的过期时间,与 Token 过期机制完美契合

  9. 对比 Cookie HttpOnly 方案

  10. 优点:更安全,防止 XSS 攻击获取 Cookie
  11. 缺点:
    • 跨域限制严格
    • 无法在前端灵活控制
    • 同步多个子域名时配置复杂

核心实现:从加密存储到实时同步

前端加密存储实现

我们使用 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

具体实现流程:

  1. 用户登录后,认证服务生成 JWT Token 并存入 Redis(设置过期时间)
  2. 服务端通过 Redis 频道发布 Token 更新事件
  3. 所有客户端订阅该频道,收到更新后同步本地存储

安全加固:从防御到优化

Token 过期与刷新机制

我们采用双 Token 机制来平衡安全性和用户体验:

  • Access Token:短有效期(15 分钟),用于 API 鉴权
  • Refresh Token:长有效期(7 天),存储在 HttpOnly Cookie 中,用于获取新的 Access Token

刷新流程:

  1. 前端检测到 Access Token 过期
  2. 静默发送 Refresh Token 到 /refresh 端点
  3. 服务端验证后返回新的 Access Token
  4. 通过 Redis 发布新 Token 到所有客户端

CSRF 防护措施

虽然 JWT 本身不容易受到 CSRF 攻击,但我们还是采取了额外防护:

  1. 关键操作(如修改密码)需要二次验证
  2. 重要 API 请求必须包含自定义 Header(如 X-Requested-With)
  3. 对敏感操作使用 POST/PUT/DELETE 方法而非 GET

避坑指南:实战经验分享

iOS WebView 的特殊处理

在 iOS WebView 中,我们遇到了 localStorage 被意外清除的问题。解决方案是:

  1. 优先尝试使用 sessionStorage
  2. 实现 fallback 机制,当存储失败时降级到内存存储
  3. 定期检查存储状态,必要时重新从服务端获取

避免 Redis 缓存雪崩

为防止大量 Token 同时过期导致雪崩,我们采用:

  1. 随机化 Token 过期时间(基础时间 ± 随机偏移量)
  2. 提前续期机制:在 Token 过期前 5 分钟自动续期
  3. 多级缓存:本地内存缓存 + Redis + 数据库

性能优化:让同步更快更稳

压力测试结果

经过优化,我们在以下环境下达到了 <100ms 的同步延迟:

  • 测试环境:1000 并发用户
  • Redis 集群:3 主 3 从
  • 网络延迟:<50ms

关键优化点:

  1. 使用 Redis Pipeline 批量处理订阅消息
  2. 前端实现消息去重(基于消息 ID)
  3. 压缩 Token 体积(移除不必要的信息)

内存占用优化

对于大用户量场景,我们采用了:

  1. Token 分片存储:按用户 ID 哈希分配到不同 Redis 实例
  2. Lazy 清理:只在访问时检查 Token 是否过期
  3. 定期扫描:每天凌晨清理过期 Token

总结与思考

这套方案在实际运行中表现良好,成功解决了我们遇到的 Token 管理和同步问题。当然,安全是一个持续的过程,我们仍在不断改进。

留给读者的思考题:如何实现 Token 的主动失效能力?比如当用户修改密码后,如何立即让所有设备的旧 Token 失效?

正文完
 0
评论(没有评论)