You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于NodeJs+MongoDB,如何安全存储Google Play订阅令牌防盗用?

安全处理JWT与高级用户权限验证的方案

1. 不存储明文JWT,改用令牌哈希存储

  • 生成JWT后,不要直接存储明文,而是对JWT做加盐哈希(推荐用bcrypt或argon2),将哈希值存入MongoDB的用户集合。
  • 用户携带JWT请求高级路由时的验证流程:
    1. 先用密钥验证JWT签名有效性,确保令牌未被篡改。
    2. 从JWT payload中解析出用户ID,查询该用户对应的哈希令牌。
    3. 将请求中的JWT与存储的哈希值比对,匹配通过后再验证订阅状态。
  • 优势:即使数据库泄露,攻击者拿到的是不可逆的哈希值,无法直接冒充用户。

2. 缩短JWT有效期,搭配刷新令牌机制

  • 将访问JWT的有效期设为短周期(比如15分钟),同时生成一个刷新令牌(有效期可设为7天)。
  • 刷新令牌同样做加盐哈希后存入用户集合,与访问JWT关联。
  • 访问令牌过期后,用户用刷新令牌请求新的访问令牌,此时验证刷新令牌哈希和订阅状态,有效则发放新的访问令牌。
  • 优势:就算访问令牌被盗,可用时间窗口短,风险可控;若刷新令牌被盗,用户可主动注销(删除数据库中的刷新令牌哈希)。

3. 给JWT添加额外验证维度

  • 在JWT的payload中加入唯一标识,比如用户的设备ID哈希、IP地址哈希(不要存明文IP)或UA信息哈希。
  • 验证JWT时,除了签名,还要比对请求的设备/IP/UA哈希与payload中的值是否一致。
  • 优势:就算令牌被盗,攻击者用不同设备或IP访问时会被拦截。

4. 实现令牌注销机制

  • 在用户集合中维护无效令牌列表(或单独建集合),当用户主动注销、修改密码、取消订阅时,将对应JWT的哈希(或唯一标识)加入列表。
  • 每次验证JWT时,先检查令牌是否在无效列表中,存在则直接拒绝请求。
  • 配合短有效期JWT,定期清理过期的无效令牌,避免列表无限膨胀。

5. 订阅状态实时验证(可选)

  • 对于高敏感的高级路由,除了验证JWT和本地存储的订阅状态,每次请求时调用Google订阅API,实时确认用户订阅有效性。
  • 优势:避免本地订阅状态与实际Google订阅不同步的问题,也能防止攻击者使用已过期的令牌访问。

代码示例(哈希存储JWT)

用bcrypt处理哈希的核心逻辑:

const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');

// 生成并存储JWT哈希
async function generateAndStoreToken(userId) {
  const accessToken = jwt.sign({ userId }, process.env.JWT_SECRET, { expiresIn: '15m' });
  // 加盐哈希令牌
  const hashedToken = await bcrypt.hash(accessToken, 10);
  // 更新用户集合中的哈希令牌
  await User.findByIdAndUpdate(userId, { $set: { hashedAccessToken: hashedToken } });
  return accessToken;
}

// 路由验证中间件
async function validateToken(req, res, next) {
  const token = req.headers.authorization?.split(' ')[1];
  if (!token) return res.status(401).send('Unauthorized');

  // 验证JWT签名
  let decoded;
  try {
    decoded = jwt.verify(token, process.env.JWT_SECRET);
  } catch (err) {
    return res.status(401).send('Invalid token');
  }

  // 查询用户及哈希令牌
  const user = await User.findById(decoded.userId);
  if (!user) return res.status(401).send('User not found');

  // 比对令牌哈希
  const isMatch = await bcrypt.compare(token, user.hashedAccessToken);
  if (!isMatch) return res.status(401).send('Invalid token');

  // 验证订阅状态
  if (!user.isActiveSubscription) return res.status(403).send('Subscription expired');

  req.user = user;
  next();
}

内容的提问来源于stack exchange,提问作者Poseidon Broger

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.16 17:07:03