基于NodeJs+MongoDB,如何安全存储Google Play订阅令牌防盗用?
安全处理JWT与高级用户权限验证的方案
1. 不存储明文JWT,改用令牌哈希存储
- 生成JWT后,不要直接存储明文,而是对JWT做加盐哈希(推荐用
bcrypt或argon2),将哈希值存入MongoDB的用户集合。 - 用户携带JWT请求高级路由时的验证流程:
- 先用密钥验证JWT签名有效性,确保令牌未被篡改。
- 从JWT payload中解析出用户ID,查询该用户对应的哈希令牌。
- 将请求中的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
相关产品推荐
相关产品推荐

