什么是JSON Web Tokens(JWT)?
JWT因其简单和无状态的特性成为了身份认证中的热门选择。然而,当JWT被恶意重复使用(例如重放攻击)时,会带来安全风险。为避免这种情况,可以采用一种确保每个令牌只能使用一次的方法。本文将介绍如何通过在数据库中存储登录ID,实现单次使用的JWT。
为什么要使用单次使用的JWT?
- 防止重放攻击:每个JWT只能使用一次,防止攻击者拦截后重复使用。
- 提升安全性:令牌被拦截后也无法被再次利用。
- 控制会话行为:可在令牌使用后立即失效,更好地控制会话生命周期。
实现思路:基于登录ID的单次使用JWT
核心思路是:当用户登录时,生成一个唯一的登录ID并将其存储到数据库中,同时将该登录ID写入JWT。在每次请求时,验证JWT中的登录ID是否与数据库中的记录一致。如果不匹配或登录ID为空,则令牌无效,从而防止重复使用。
实现步骤
1. 生成并存储唯一的登录ID
在用户登录时,生成一个随机的登录ID,存储到数据库中,并将其包含在JWT中。
代码示例:
const generateLoginId = () => Math.random().toString(36).substring(2, 15);
const userLogin = async (user) => {
const loginId = generateLoginId();
await db.updateUser(user.id, { loginId });
const token = jwt.sign({ userId: user.id, loginId }, 'your-secret-key', { expiresIn: '15m' });
return token;
};
2. 在每次请求时验证登录ID
用户发起请求时,解析JWT并验证其中的登录ID是否与数据库中的记录一致。
代码示例:
const validateToken = async (token) => {
const decoded = jwt.verify(token, 'your-secret-key');
const user = await db.getUser(decoded.userId);
if (user.loginId !== decoded.loginId) {
throw new Error('Invalid or expired token');
}
return user;
};
3. 用户登出时清空登录ID
用户登出时,将数据库中的登录ID设置为null,以确保令牌无法被重复使用。
代码示例:
const userLogout = async (userId) => {
await db.updateUser(userId, { loginId: null });
};
方法优点
- 轻量化:无需额外的令牌存储或Redis。
- 高效:用户登出后令牌立即失效。
- 简单验证:直接依赖现有数据库,无需额外的复杂逻辑。
方法局限性
- 必须登出:只有用户登出后令牌才会失效,这种方法不支持多设备或多会话场景。
- 不适合无状态API:对于完全无状态的API,这种方法的扩展性较差。
- 潜在的竞争条件:如果用户在多个设备上同时登录,可能会因登录ID被覆盖而产生同步问题。
对于更复杂的系统(如支持多设备登录或需要严格无状态的API),可以考虑使用令牌跟踪机制,以实现更细粒度的控制和更高的安全性。
总结
基于登录ID的单次使用JWT方法是一种简单高效的安全增强手段,特别适用于需要防止令牌重复使用的场景。尽管它足够轻量且易于实现,但对于复杂的应用程序,可能需要更具扩展性的解决方案。