JWT 黑名单机制和实现
在一个账户多端登录的场景下,如果修改密码需要让所有已登录设备的 Token 同时失效,可以通过 黑名单机制 来实现。
如果你使用的是 MongoDB,可以将多端登录的 Token 信息存储在 MongoDB 中,这样便于管理和查询。
1. 数据模型设计
为管理多端登录的 Token 信息,可以设计一个 UserToken
集合,用来存储用户的所有 Token 信息。
数据结构示例:
json
{
"_id": "64b123abc...", // MongoDB 自生成的唯一 ID
"userId": "1234567890", // 用户 ID
"jti": "uuid1", // Token 的唯一标识
"deviceId": "deviceA", // 设备 ID(前端提供)
"issuedAt": "2024-12-20T10:00:00Z", // Token 签发时间
"expiresAt": "2024-12-20T11:00:00Z", // Token 过期时间
"isInvalidated": false // 是否失效
}
2. 生成 Token 并存储
在生成 accessToken
和 refreshToken
后,将其唯一标识(jti
)和相关信息存储到 MongoDB。
示例代码:
typescript
import { v4 as uuidv4 } from "uuid";
import UserToken from "./models/UserToken"; // 假设你有一个 Mongoose 模型
// 生成 JWT
export const generateToken = async (
user: IUser,
deviceId: string
): Promise<object> => {
const jti = uuidv4(); // 生成唯一标识
const issuedAt = new Date();
const expiresAt = new Date(issuedAt.getTime() + 60 * 1000); // 1 分钟有效期
const accessToken = jwt.sign(
{ id: user._id, username: user.username, role: user.role, jti, deviceId },
JWT_SECRET,
{ expiresIn: "1m" }
);
const refreshToken = jwt.sign(
{ id: user._id, jti, deviceId },
REFRESH_TOKEN_SECRET,
{ expiresIn: "7d" }
);
// 存储 Token 信息到 MongoDB
await UserToken.create({
userId: user._id,
jti,
deviceId,
issuedAt,
expiresAt,
isInvalidated: false,
});
return { accessToken, refreshToken, jti };
};
3. 修改密码使所有 Token 失效
当用户修改密码时,需要将该用户的所有 Token 标记为失效。
示例代码:
typescript
const invalidateAllTokens = async (userId: string) => {
await UserToken.updateMany({ userId }, { $set: { isInvalidated: true } });
};
4. 手动注销某个设备
用户可以选择注销某个设备的登录状态。需要根据 deviceId
定位 Token 并将其标记为失效。
示例代码:
typescript
const logoutDevice = async (userId: string, deviceId: string) => {
await UserToken.updateMany(
{ userId, deviceId },
{ $set: { isInvalidated: true } }
);
};
5. 校验 Token 的有效性
每次验证 Token 时,除了检查其签名和有效期外,还需要查询 MongoDB,确认该 Token 是否已被标记为失效。
示例代码:
typescript
const verifyToken = async (token: string): Promise<boolean> => {
try {
const decoded = jwt.verify(token, JWT_SECRET) as any;
// 查询 MongoDB 中的 Token 信息
const tokenInfo = await UserToken.findOne({ jti: decoded.jti });
// 如果 Token 不存在或已失效,则拒绝访问
if (!tokenInfo || tokenInfo.isInvalidated) {
return false;
}
return true;
} catch (err) {
return false;
}
};
6. 定时清理过期的 Token
为了减少数据库的存储压力,可以定期清理已过期的 Token。
示例代码:
typescript
const cleanExpiredTokens = async () => {
const now = new Date();
await UserToken.deleteMany({ expiresAt: { $lt: now } });
};
// 示例:使用 Node.js 定时器每小时清理一次
setInterval(cleanExpiredTokens, 3600 * 1000);
总结
通过 MongoDB 实现多端登录管理的方案包括:
- 为每个 Token 引入唯一标识(JTI)和设备标识(deviceId)。
- 在 MongoDB 中存储 Token 信息,包括用户 ID、签发时间、过期时间等。
- 提供批量失效和单端注销功能,通过修改
isInvalidated
字段实现。 - 校验 Token 的有效性时查询数据库,确认 Token 状态。
- 定期清理过期的 Token,以优化存储资源。
这种方式能很好地支持多端登录,并方便地管理 Token 的生命周期和状态。