Redis 与 JWT 结合的 Token 验证方案
背景
在现代 Web 应用程序中,身份验证是一个核心功能。JWT(JSON Web Token)因其无状态特性成为了流行的身份验证方案。然而,纯 JWT 方案存在一些限制,特别是在会话管理方面。本文将探讨如何结合 Redis 和 JWT 创建更灵活、安全的身份验证系统。
传统 JWT 方案的局限
JWT 作为一种自包含令牌,具有以下优点:
无需服务器存储状态
减少数据库查询
跨服务验证简单
包含必要的用户信息
但也面临这些挑战:
无法在过期前撤销令牌
令牌过期时间一旦设置无法修改
无法实现会话延期(滑动过期)
难以实现集中式会话管理
Redis+JWT 混合方案
通过结合 Redis 和 JWT,我们可以兼顾两者优点:
方案设计
JWT 生成:
创建包含用户 ID 和唯一令牌 ID(jti)的 JWT
设置较长的过期时间(如 7 天)作为安全边界
Redis 存储:
以令牌 ID 为键存储在 Redis 中
存储内容可以是用户 ID 或更丰富的会话信息
设置较短的 TTL(如 2 小时)作为实际会话时长
双重验证:
首先验证 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),但在大多数要求严格会话管理的系统中,这种开销是可接受的。为提高性能:
使用 Redis 集群或读写分离
适当缓存验证结果(微秒级)
使用连接池减少连接开销
考虑在某些不敏感路径使用纯 JWT 验证
结论
Redis+JWT 的混合验证方案结合了两者的优点,为 Web 应用提供了更灵活、安全的身份验证机制。特别适用于:
需要精确控制用户会话的系统
对安全要求较高的应用
需要管理员控制用户会话的场景
要求实时权限变更的系统
在实现时,应根据具体业务需求和性能要求调整方案细节,找到安全性和用户体验的最佳平衡点。