本文介绍基于服务器生成 Nonce 来防止重放攻击的标准模式。
我们详细地分解一下后端需要做的事情:
1. 提供获取 Nonce 的接口 (Nonce Generation Endpoint)
- 需要一个专门的 API 端点,例如
GET /api/get-nonce
。 - 当前端(在准备发起需要保护的请求之前)调用这个接口时:
-
生成 Nonce: 服务器使用密码学安全 的随机数生成器创建一个足够长、难以预测的唯一字符串。
typescriptimport crypto from 'crypto'; function generateNonce(length = 16): string { // 生成 16 字节的安全随机数据,转为十六进制字符串 (长度变为 32) return crypto.randomBytes(length).toString('hex'); } app.get('/api/get-nonce', (req, res) => { // 这里可能需要验证用户身份,防止匿名用户耗尽 Nonce 资源 if (!req.session.user) { // 假设使用了 session return res.status(401).send('Unauthorized'); } const nonce = generateNonce(); const ttlSeconds = 300; // Nonce 有效期,例如 5 分钟 // 将 Nonce 存储起来,并设置有效期 // 推荐使用 Redis 或 Memcached 等内存数据库,性能好且自带 TTL 功能 redisClient.set(`nonce:${nonce}`, 'valid', 'EX', ttlSeconds, (err, reply) => { if (err) { console.error('Error storing nonce:', err); return res.status(500).send('Internal Server Error'); } console.log(`Generated and stored nonce: ${nonce} for user ${req.session.user.id}`); // 将生成的 Nonce 返回给前端 res.json({ nonce: nonce }); }); });
-
存储 Nonce: 将生成的 Nonce 存储在服务器端的一个临时存储中(强烈推荐 Redis 或 Memcached )。关键是要同时设置一个过期时间 (TTL - Time To Live) ,例如 5 分钟。这能防止存储无限增长,并自动清理过期的、未使用的 Nonce。存储时,至少要记录 Nonce 本身,可以简单地将 Nonce 作为 Key,Value 设为
true
或valid
。有时也会存储关联的用户 ID 或 Session ID。 -
返回 Nonce: 将生成的 Nonce 通过 API 响应返回给前端。
-
2. 处理带有 Nonce 的主业务请求 (Main Request Handling)
-
当前端提交业务数据(例如表单),并在请求中包含了
data
,nonce
,hash
时,服务器端的处理流程如下:typescriptapp.post('/api/submit-data', async (req, res) => { const { data, nonce, hash: receivedHash } = req.body; // 或从请求头获取 if (!data || !nonce || !receivedHash) { return res.status(400).send('Missing required parameters.'); } // 1. 验证 Nonce 的有效性 (检查是否存在且未使用) // 尝试从 Redis 中获取并立即删除它 (原子操作) // 使用 DEL 命令尝试删除,如果返回 1 表示删除成功 (存在且被删了),返回 0 表示不存在 (或已被删) redisClient.del(`nonce:${nonce}`, (err, reply) => { if (err) { console.error('Error validating nonce:', err); return res.status(500).send('Internal Server Error'); } if (reply === 1) { // Nonce 存在且已被成功删除,表示它是有效的且是第一次使用 console.log(`Nonce ${nonce} validated and consumed.`); // 2. 重新计算哈希值 const dataToHash = JSON.stringify(data) + "|" + nonce; // 必须和前端用完全一样的方式拼接 const calculatedHash = sha256(dataToHash); // 使用相同的哈希算法 // 3. 比较哈希值 if (calculatedHash === receivedHash) { // 哈希匹配,请求有效!处理业务逻辑 console.log('Hash matches. Processing business logic for data:', data); // ... 执行你的业务操作 ... res.send('Data processed successfully.'); } else { // 哈希不匹配,数据可能被篡改 console.warn(`Hash mismatch for nonce ${nonce}. Expected ${calculatedHash}, got ${receivedHash}`); res.status(400).send('Invalid request signature.'); } } else { // Nonce 不存在 (reply === 0) // 可能原因:Nonce 错误、已过期被 Redis 自动删除、或已被之前的请求使用并删除 console.warn(`Invalid or expired/used nonce received: ${nonce}`); res.status(400).send('Invalid or expired token.'); } }); });
- 验证 Nonce:
- 从请求中获取
nonce
。 - 尝试从存储中查找并删除 这个
nonce
。使用 Redis 的DEL
命令是一个很好的原子操作方式:如果DEL nonce:abc
返回 1,说明这个 key 存在并且被成功删除了;如果返回 0,说明这个 key 不存在(可能已过期或已被使用)。 - 如果删除成功 (返回 1),说明 Nonce 有效且是首次使用。继续下一步。
- 如果删除失败 (返回 0),说明 Nonce 无效(过期、错误或已使用)。立即拒绝请求。
- 从请求中获取
- 重新计算哈希: 使用与前端完全相同 的方法(相同的哈希算法、相同的数据序列化方式、相同的拼接顺序)来拼接接收到的
data
和nonce
,然后计算哈希值。 - 比较哈希: 将服务器端计算出的哈希值与请求中带来的
receivedHash
进行比较。- 如果一致,说明数据未被篡改,并且请求是基于有效的 Nonce 发起的。处理业务逻辑。
- 如果不一致,说明数据在中途可能被篡改(或者前后端计算逻辑不一致)。拒绝请求。
- 验证 Nonce:
关键点:
- 原子性: 检查 Nonce 是否存在和将其标记为"已使用"(通过删除)应该是一个原子操作,以防止并发请求使用同一个 Nonce 的竞争条件。Redis 的
DEL
或GETDEL
(如果需要获取值再删除) 提供了这种保证。 - 有效期 (TTL): 给 Nonce 设置有效期非常重要,可以防止存储无限膨胀,并处理那些生成了但从未被使用的 Nonce。
- 安全性: Nonce 生成必须使用安全的随机数源。
- 存储选择: Redis 或类似内存数据库是存储 Nonce 的理想选择,因为它们速度快且易于实现 TTL。避免使用关系数据库(除非有特殊原因并做了优化),也避免存储在应用服务器的内存中(无法扩展且重启会丢失)。
这种服务器生成、验证并立即销毁 Nonce 的方式是实现一次性令牌以防止重放攻击的可靠方法。