SSF0SSF0
首页
前端
  • Node
  • Go
  • C#
  • MySql
  • Bash
  • Git
  • Docker
  • VuePress
  • CI/CD
  • 服务器
  • 网站
  • 学习资料
  • 软件
Timeline
Github
标签
分类
首页
前端
  • Node
  • Go
  • C#
  • MySql
  • Bash
  • Git
  • Docker
  • VuePress
  • CI/CD
  • 服务器
  • 网站
  • 学习资料
  • 软件
Timeline
Github
标签
分类
  • Node.js

    • 在 Docker 容器中运行 Node.js Koa 应用的网络配置与常见问题
    • 在 Node.js 后端实现邮箱验证码功能
    • Redis 与 JWT 结合的 Token 验证方案
    • Node.js 后端三层架构中的错误处理最佳实践
    • 在 Koa 中批量设置路由 JWT 校验的最佳实践
  • Go

    • Go1
    • Go2
  • MySql

    • 数据库连接

Redis 与 JWT 结合的 Token 验证方案

背景

在现代 Web 应用程序中,身份验证是一个核心功能。JWT(JSON Web Token)因其无状态特性成为了流行的身份验证方案。然而,纯 JWT 方案存在一些限制,特别是在会话管理方面。本文将探讨如何结合 Redis 和 JWT 创建更灵活、安全的身份验证系统。

传统 JWT 方案的局限

JWT 作为一种自包含令牌,具有以下优点:

  1. 无需服务器存储状态

  2. 减少数据库查询

  3. 跨服务验证简单

  4. 包含必要的用户信息

但也面临这些挑战:

  1. 无法在过期前撤销令牌

  2. 令牌过期时间一旦设置无法修改

  3. 无法实现会话延期(滑动过期)

  4. 难以实现集中式会话管理

Redis+JWT 混合方案

通过结合 Redis 和 JWT,我们可以兼顾两者优点:

方案设计

  1. JWT 生成:

    • 创建包含用户 ID 和唯一令牌 ID(jti)的 JWT

    • 设置较长的过期时间(如 7 天)作为安全边界

  2. Redis 存储:

    • 以令牌 ID 为键存储在 Redis 中

    • 存储内容可以是用户 ID 或更丰富的会话信息

    • 设置较短的 TTL(如 2 小时)作为实际会话时长

  3. 双重验证:

    • 首先验证 JWT 的签名和标准过期时间

    • 然后检查 Redis 中是否存在对应的令牌记录

    • 两者都验证通过才视为有效

代码实现

// 用户令牌存储库
export class UserTokenRepository {
  private static readonly TOKEN_PREFIX = "user:token:";

  // 存储令牌,使用tokenId作为键
  public static async saveToken(
    tokenId: string,
    userId: string,
    expiresInDays: number
  ): Promise<void> {
    await redisClient.set(
      this.TOKEN_PREFIX + tokenId,
      userId,
      "EX",
      expiresInDays * 60 * 60 * 24
    );
  }

  // 验证令牌是否存在
  public static async verifyToken(tokenId: string): Promise<string | null> {
    return await redisClient.get(this.TOKEN_PREFIX + tokenId);
  }

  // 删除特定令牌
  public static async revokeToken(tokenId: string): Promise<void> {
    await redisClient.del(this.TOKEN_PREFIX + tokenId);
  }
}

// 令牌服务
class JwtTokenUtils {
  // 生成令牌
  async generateToken(user) {
    const tokenId = crypto.randomUUID();

    // 生成JWT
    const jwt = jsonwebtoken.sign(
      { userId: user.id, jti: tokenId },
      JWT_SECRET,
      { expiresIn: "7d" }
    );

    // 存储到Redis
    await UserTokenRepository.saveToken(tokenId, user.id, 1); // 1天有效期

    return jwt;
  }

  // 验证令牌
  async verifyToken(token) {
    // 1. 验证JWT
    const decoded = jsonwebtoken.verify(token, JWT_SECRET);

    // 2. 验证Redis中是否存在
    const userId = await UserTokenRepository.verifyToken(decoded.jti);
    if (!userId) {
      throw new Error("令牌已过期或已撤销");
    }

    return decoded;
  }

  // 撤销令牌
  async revokeToken(token) {
    const decoded = jsonwebtoken.decode(token);
    if (decoded && decoded.jti) {
      await UserTokenRepository.revokeToken(decoded.jti);
    }
  }
}

实际解决的问题

这种双重验证方案解决了以下实际问题:

1. 实现即时登出/令牌撤销

在银行应用、企业管理系统等安全要求高的场景,用户登出或管理员封禁账户时,需要立即使令牌失效。通过 Redis 验证层,可以删除 Redis 中的记录,使 JWT 立即失效。

// 用户登出
async function logout(req, res) {
  await JwtTokenUtils.revokeToken(req.headers.authorization);
  res.status(200).json({ message: "登出成功" });
}

2. 灵活控制会话时长

可以设置较短的活跃会话时间(如 2 小时),同时保持较长的 JWT 安全边界(如 7 天)。这样既保证了安全性,又提高了用户体验。

3. 实现会话续期(滑动过期)

每次用户访问 API,可以自动刷新 Redis 中令牌的 TTL,活跃用户无需频繁登录。

// 验证令牌并续期
async function verifyAndRefresh(token) {
  const decoded = jsonwebtoken.verify(token, JWT_SECRET);

  // 验证Redis
  const userId = await UserTokenRepository.verifyToken(decoded.jti);
  if (!userId) {
    throw new Error("令牌已过期或已撤销");
  }

  // 续期Redis TTL
  await redisClient.expire(
    UserTokenRepository.TOKEN_PREFIX + decoded.jti,
    2 * 60 * 60 // 2小时
  );

  return decoded;
}

4. 实时权限控制

当用户权限变更时,可以立即撤销现有令牌,强制用户获取包含新权限信息的令牌。适用于权限敏感的系统,如 SaaS 平台或企业内部系统。

5. 集中式会话管理

可以实现会话监控和管理功能:

// 获取所有活跃会话
async function getActiveSessions() {
  const keys = await redisClient.keys(UserTokenRepository.TOKEN_PREFIX + "*");
  return Promise.all(
    keys.map(async (key) => {
      const userId = await redisClient.get(key);
      const ttl = await redisClient.ttl(key);
      return {
        tokenId: key.replace(UserTokenRepository.TOKEN_PREFIX, ""),
        userId,
        expiresIn: ttl,
      };
    })
  );
}

// 强制所有用户登出
async function logoutAllUsers() {
  const keys = await redisClient.keys(UserTokenRepository.TOKEN_PREFIX + "*");
  if (keys.length > 0) {
    await redisClient.del(...keys);
  }
}

6. 多设备登录管理

可以为每个设备创建单独的令牌,并进行单独管理:

// 获取用户所有设备的会话
async function getUserSessions(userId) {
  // 需要额外的用户-令牌映射索引
  const keys = await redisClient.keys(UserTokenRepository.TOKEN_PREFIX + "*");

  const sessions = [];
  for (const key of keys) {
    const value = await redisClient.get(key);
    if (value === userId) {
      sessions.push(key.replace(UserTokenRepository.TOKEN_PREFIX, ""));
    }
  }

  return sessions;
}

// 只登出特定设备
async function logoutDevice(userId, deviceId) {
  const tokenId = `${userId}-${deviceId}`;
  await UserTokenRepository.revokeToken(tokenId);
}

7. 安全增强

提供双重验证机制,即使 JWT 密钥泄露,攻击者还需要通过 Redis 验证,增加了一层安全保障。适用于金融、医疗等高安全要求的应用。

性能考虑

虽然这种方案增加了一些性能开销(每次验证都需要查询 Redis),但在大多数要求严格会话管理的系统中,这种开销是可接受的。为提高性能:

  1. 使用 Redis 集群或读写分离

  2. 适当缓存验证结果(微秒级)

  3. 使用连接池减少连接开销

  4. 考虑在某些不敏感路径使用纯 JWT 验证

结论

Redis+JWT 的混合验证方案结合了两者的优点,为 Web 应用提供了更灵活、安全的身份验证机制。特别适用于:

  • 需要精确控制用户会话的系统

  • 对安全要求较高的应用

  • 需要管理员控制用户会话的场景

  • 要求实时权限变更的系统

在实现时,应根据具体业务需求和性能要求调整方案细节,找到安全性和用户体验的最佳平衡点。

最后更新时间:
贡献者: 何风顺
上一页
在 Node.js 后端实现邮箱验证码功能
下一页
Node.js 后端三层架构中的错误处理最佳实践