实现一次性令牌以防止重放攻击的可靠方法

本文介绍基于服务器生成 Nonce 来防止重放攻击的标准模式。

我们详细地分解一下后端需要做的事情:

1. 提供获取 Nonce 的接口 (Nonce Generation Endpoint)

  • 需要一个专门的 API 端点,例如 GET /api/get-nonce
  • 当前端(在准备发起需要保护的请求之前)调用这个接口时:
    • 生成 Nonce: 服务器使用密码学安全 的随机数生成器创建一个足够长、难以预测的唯一字符串。

      typescript 复制代码
      import 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 设为 truevalid。有时也会存储关联的用户 ID 或 Session ID。

    • 返回 Nonce: 将生成的 Nonce 通过 API 响应返回给前端。

2. 处理带有 Nonce 的主业务请求 (Main Request Handling)

  • 当前端提交业务数据(例如表单),并在请求中包含了 data, nonce, hash 时,服务器端的处理流程如下:

    typescript 复制代码
    app.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 无效(过期、错误或已使用)。立即拒绝请求
    • 重新计算哈希: 使用与前端完全相同 的方法(相同的哈希算法、相同的数据序列化方式、相同的拼接顺序)来拼接接收到的 datanonce,然后计算哈希值。
    • 比较哈希: 将服务器端计算出的哈希值与请求中带来的 receivedHash 进行比较。
      • 如果一致,说明数据未被篡改,并且请求是基于有效的 Nonce 发起的。处理业务逻辑
      • 如果不一致,说明数据在中途可能被篡改(或者前后端计算逻辑不一致)。拒绝请求

关键点:

  • 原子性: 检查 Nonce 是否存在和将其标记为"已使用"(通过删除)应该是一个原子操作,以防止并发请求使用同一个 Nonce 的竞争条件。Redis 的 DELGETDEL (如果需要获取值再删除) 提供了这种保证。
  • 有效期 (TTL): 给 Nonce 设置有效期非常重要,可以防止存储无限膨胀,并处理那些生成了但从未被使用的 Nonce。
  • 安全性: Nonce 生成必须使用安全的随机数源。
  • 存储选择: Redis 或类似内存数据库是存储 Nonce 的理想选择,因为它们速度快且易于实现 TTL。避免使用关系数据库(除非有特殊原因并做了优化),也避免存储在应用服务器的内存中(无法扩展且重启会丢失)。

这种服务器生成、验证并立即销毁 Nonce 的方式是实现一次性令牌以防止重放攻击的可靠方法。

相关推荐
夕颜1112 分钟前
记录一下关于 Cursor 设置的问题
后端
凉白开3382 分钟前
Scala基础知识
开发语言·后端·scala
2401_824256865 分钟前
Scala的函数式编程
开发语言·后端·scala
小杨4041 小时前
springboot框架项目实践应用十四(扩展sentinel错误提示)
spring boot·后端·spring cloud
陈大爷(有低保)1 小时前
Spring中都用到了哪些设计模式
java·后端·spring
程序员 小柴1 小时前
SpringCloud概述
后端·spring·spring cloud
喝醉的小喵1 小时前
分布式环境下的主从数据同步
分布式·后端·mysql·etcd·共识算法·主从复制
雷渊2 小时前
深入分析mybatis中#{}和${}的区别
java·后端·面试
我是福福大王2 小时前
前后端SM2加密交互问题解析与解决方案
前端·后端
老友@2 小时前
Kafka 全面解析
服务器·分布式·后端·kafka