为什么API接口需要这三道防线
一次完整的API请求从客户端发出到服务端响应,中间会经过网络传输、网关转发等多个节点。攻击者可以在任何节点上窃听、截获甚至篡改数据包。如果不加防护,三个典型问题就会暴露:第一,攻击者把截获的有效请求原封不动再发一次(重放攻击),可能导致重复下单、重复扣款;第二,攻击者修改请求中的参数值(如金额、用户ID),试图绕过业务逻辑;第三,攻击者使用脚本以极高频率调用接口,耗尽服务器资源或触发计费漏洞。这三个问题对应三种防护策略:防重放攻击、签名校验、限频策略。它们各自解决不同维度的风险,但通常组合使用才能形成完整的API接口安全防护体系。
防重放攻击:让同一请求只能生效一次
重放攻击的本质
重放攻击不依赖破解密码或伪造身份,攻击者只需要在合法用户完成一次正常请求后,记录下完整的HTTP报文(包括URL、Header、Body甚至Cookie),然后在任意时间重新发送给服务器。如果服务器没有判断请求是否已被处理过,就会再次执行相同操作。常见于支付回调、订单提交、Token刷新等场景。
时间戳 + Nonce 方案
这是业界最成熟的防重放方案。客户端在每次请求中附带两个额外参数:timestamp(当前Unix时间戳,秒级或毫秒级)和 nonce(随机字符串,每次请求唯一)。服务端收到请求后做以下检查:
1. 计算当前时间与timestamp的差值,若超过预设窗口(如60秒),直接拒绝;
2. 从缓存(Redis)中查询该nonce是否已被使用,若存在则拒绝;
3. 将本次请求的nonce存入缓存,设置过期时间等于时间窗口,确保后续相同nonce的请求被拦截。
这里有几个关键的实现细节:时间窗口不宜过短(避免因客户端与服务端时钟偏差导致合法请求被拒),一般设为5~60秒;nonce必须保证全局唯一,常用UUID或雪花算法生成;缓存建议使用Redis的SET NX EX命令原子操作,防止并发重复写入。如果服务是集群部署,nonce的存储必须共享(如Redis),不能使用本地内存。
序列号方案
适用于有严格顺序要求的场景(如游戏对战指令)。客户端与服务端维护一个递增的序列号,每次请求携带当前序列号,服务端记录每个客户端最近一次成功使用的序列号。若新请求的序列号小于等于已记录值,则判定为重放。该方案不需要额外存储nonce,但要求客户端不能并发乱序发送,且需要处理序列号回绕问题。
签名校验:防止参数被中途篡改
签名的作用
即使有防重放保护,如果请求中的关键参数被攻击者修改(比如把“支付金额”从100改为1),服务端照常执行就会产生严重后果。签名校验(Signature Verification)利用服务端与客户端共享的密钥(Secret Key),对请求参数进行Hash运算生成一个签名,附加在请求中。服务端用相同规则重新计算签名,比对是否一致,不一致则拒绝。
标准签名生成流程
以下步骤在客户端完成:
1. 将所有参与签名的参数(通常包括业务参数、时间戳、nonce,不包括签名本身)按键名进行字典排序;
2. 拼接成字符串,格式如 key1=value1&key2=value2;
3. 在拼接字符串末尾(或开头)追加共享密钥 secret;
4. 对整个字符串做MD5或HMAC-SHA256运算,得到签名值。
服务端验证时,取出请求中的签名值,然后使用同样的排序拼接规则(参数值必须与客户端一致)和同样的密钥重新计算签名,比对二者是否相等。注意:时间戳和nonce也应参与签名,这样即使签名泄露,攻击者也无法伪造新时间戳下的签名,从而与防重放形成联动。
密钥管理与分发
签名校验的前提是密钥不被泄露。如果是服务端之间的API调用,密钥可以通过离线方式(如配置文件、环境变量)或动态分发服务(如AWS KMS)下发。如果是客户端(App、浏览器)调用,密钥不能硬编码在代码中,通常的做法是利用认证接口先获取临时access_token,再用该token作为签名密钥的一部分;或者使用非对称加密——客户端用私钥签名,服务端用公钥验签,但性能开销更大。
限频策略:抵挡高频调用与DDoS
为什么需要限频
签名校验和防重放只验证请求的合法性,但无法阻止合法用户(或持有合法密钥的攻击者)在短时间内发送海量请求。这种滥用行为会导致数据库连接池耗尽、CPU飙升、第三方API费用暴增,甚至触发连锁故障。限频策略是API接口安全防护的最后一层兜底。
常见限频算法
- 令牌桶(Token Bucket):以固定速率向桶中放入令牌,请求到来时消耗一个令牌,若桶空则拒绝。允许短时突发流量。实现简单,常用在本地限流。
- 漏桶(Leaky Bucket):请求以固定速率流出,超出的请求被丢弃或排队。保证处理速率绝对平滑,适合下游处理能力固定的场景。
- 滑动窗口(Sliding Window):记录最近时间窗口内的请求次数,如每10秒最多100次。基于Redis的Sorted Set或计数器实现,能更精确地防止边界冲击。
在实际API网关中,通常针对每个客户端IP、每个AccessKey或每个用户ID分别设置限流阈值。当请求超过阈值时,返回HTTP 429 Too Many Requests,并在响应头中告知重试时间(Retry-After)。
分布式限流的注意事项
微服务架构下,限流计数器必须中心化存储,否则每个节点的独立计数会导致整体限额被突破。Redis是首选方案:使用Lua脚本原子地检查并增加计数器,设置过期时间自动清理。性能方面,可以引入本地缓存降级:当Redis不可用时,退化为本地限流(阈值适当降低),避免击穿。
三者协同:构建纵深防御
单独使用其中一种策略都有漏洞:防重放无法防篡改,签名校验不防重放(若时间戳被伪造),限频不能验证身份。最佳做法是让三个策略工作在同一请求处理链中:
1. 网关层先做限频,快速拒绝超量请求,减少后续计算开销;
2. 然后校验签名,验证参数完整性和来源;
3. 最后检查时间戳和nonce,防重放。
时序安排上有一定讲究:如果先查nonce再校验签名,攻击者可以用合法签名但已过期的nonce尝试,服务端需要先查缓存,如果nonce不存在或已过期,可以直接返回“签名不匹配”,但这样会让攻击者获得枚举nonce的机会。更安全的方式是:先校验签名(保证参数来自可信客户端),再校验时间窗口,最后检查nonce。如果nonce重复,可以返回自定义错误码,便于客户端重试时重新生成nonce。
落地时的常见误区
- 时间戳精度不足:使用秒级时间戳,如果网络延迟较大,合法请求可能因超时被误杀。建议使用毫秒时间戳,并将允许偏差设置为几秒。
- nonce未设置过期时间:如果不设置TTL,nonce缓存会无限膨胀,最终撑爆内存。务必以时间窗口为依据设置过期。
- 签名参数排序不一致:客户端和服务端必须使用完全相同的排序规则(如按ASCII码升序),否则即使参数相同,签名也匹配不上。常见错误是忽略嵌套对象或数组的序列化方式。
- 限频粒度过粗:仅对IP做限流,无法区分不同用户。应当将API Key或用户ID纳入限流键,并为不同接口设置不同阈值(如登录接口需要更严格的限制)。
- 忽略回滚机制:如果防重放或签名校验出现bug,导致大量合法请求失败,必须有降级开关。建议在API网关层设置白名单或灰度百分比,紧急情况下可临时关闭校验,同时保留日志用于事后修复。
总结
API接口安全防护不是单一技术的堆砌,而是一套相互制约的策略组合。防重放攻击阻止请求被重复执行,签名校验确保参数在传输中不被篡改,限频策略控制资源消耗节奏。三者结合,能让接口在公开网络中具备基本的抗攻击能力。本文给出的时间戳+nonce、HMAC签名、令牌桶/滑动窗口方案已在大量生产环境中验证有效。实施时务必关注时钟同步、缓存原子性、密钥轮转等细节,再配合完善的监控和告警,才能长期守住API的安全底线。
延伸阅读
