登陆与鉴权安全设计:如何实现单次使用的JWT以增强接口的安全性

352 阅读3分钟

什么是JSON Web Tokens(JWT)?

JWT因其简单和无状态的特性成为了身份认证中的热门选择。然而,当JWT被恶意重复使用(例如重放攻击)时,会带来安全风险。为避免这种情况,可以采用一种确保每个令牌只能使用一次的方法。本文将介绍如何通过在数据库中存储登录ID,实现单次使用的JWT。


为什么要使用单次使用的JWT?

  1. 防止重放攻击:每个JWT只能使用一次,防止攻击者拦截后重复使用。
  2. 提升安全性:令牌被拦截后也无法被再次利用。
  3. 控制会话行为:可在令牌使用后立即失效,更好地控制会话生命周期。

实现思路:基于登录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 });
};

方法优点

  1. 轻量化:无需额外的令牌存储或Redis。
  2. 高效:用户登出后令牌立即失效。
  3. 简单验证:直接依赖现有数据库,无需额外的复杂逻辑。

方法局限性

  1. 必须登出:只有用户登出后令牌才会失效,这种方法不支持多设备或多会话场景。
  2. 不适合无状态API:对于完全无状态的API,这种方法的扩展性较差。
  3. 潜在的竞争条件:如果用户在多个设备上同时登录,可能会因登录ID被覆盖而产生同步问题。

对于更复杂的系统(如支持多设备登录或需要严格无状态的API),可以考虑使用令牌跟踪机制,以实现更细粒度的控制和更高的安全性。


总结

基于登录ID的单次使用JWT方法是一种简单高效的安全增强手段,特别适用于需要防止令牌重复使用的场景。尽管它足够轻量且易于实现,但对于复杂的应用程序,可能需要更具扩展性的解决方案。