网关层如何防止接口重放攻击?如果攻击者抓取了请求包进行重放,你如何识别和拦截?
- 标签:后端、系统设计、场景题、API网关、安全
🏛 业务架构师
防重放攻击的核心方案是**"时间窗 + 一次性随机数 + 签名"三件套**,我先讲机制,再讲识别与拦截的层次,最后点出幂等是最后防线。
签名与防重放协议(服务端与调用方约定)
请求参数 + timestamp + nonce + app_secret
→ 按参数名排序拼接 → HMAC-SHA256 → sign服务端校验三步:
- 时间窗校验:
|now - timestamp| < 5 分钟——重放窗口外的请求直接拒绝(时间窗不能太长,太长给攻击者留空间;太短要求双方时钟同步,NTP 保证) - nonce 一次性:
SETNX nonce:{nonce} EX 300——窗口内的重放,同一 nonce 只接受第一次,Redis 记录已用的 nonce(TTL = 时间窗长度即可,过期后时间窗校验自然拦截) - 签名比对:防止攻击者伪造 timestamp/nonce——三者绑定在签名里,篡改任何一项签名校验失败
识别与拦截(纵深防御)
- 网关层:时间戳/nonce/sign 参数缺失直接拒绝;nonce 命中已用记录 → 标记攻击源(IP/设备进观察名单)
- 行为识别:同一 appid 高频重复 nonce 尝试 → 限流/封禁
- 业务幂等键:关键操作(支付、领券)带唯一请求号,服务端唯一索引兜底——就算前两层全被绕过(比如密钥泄漏),幂等保证攻击最多造成一次真实效果
密钥管理(安全的地基)
- secret 分发走安全通道、定期轮换(轮换期新旧双验平滑过渡)
- 签名算法版本化(sign-version),算法升级不停服
业务收尾:重放攻击的真实威胁是资金类接口——重复支付请求、重复领券。我的原则:签名防"外人",幂等防"一切"(包括合法用户的网络重试),两层都要有,幂等永远不能省。
🏗 系统架构师
防重放的核心是让每个请求"一次性、有时效、有身份",我按机制、实现、边界攻击讲。
1. 基础三要素:timestamp + nonce + 签名 客户端请求携带:
text
timestamp=1698000000 请求时间
nonce=8f3a... 随机串(请求唯一标识)
sign=HMAC(app_secret, method+path+timestamp+nonce+body)服务端校验顺序:
- 签名验证:用相同算法重算比对,防篡改(任何参数被改,签名对不上)
- 时效验证:
now - timestamp < 5分钟,超时拒绝——重放窗口被限定 - nonce 查重:nonce 存 Redis
SETNX,TTL = 时效窗口;已存在即为重放,拒绝
三要素缺一不可:没有 nonce,5 分钟内可原样重放;没有 timestamp,nonce 要永久存储;没有签名,攻击者可自己伪造合法 nonce。
2. 加固与对抗(讲深度)
- HMAC 而非裸 MD5/SHA:带密钥防彩虹台;app_secret 用更高等级保护(KMS、客户端白盒加密),密钥轮换机制
- nonce 的存储成本:高 QPS 下 Redis 写压力大,可用布隆过滤器做初筛(少量误杀可接受,回查精确集合确认)
- 幂等作为最后防线:重放即使穿透校验(TTL 边界、时钟偏移),业务层幂等键(订单号)保证不重复扣款——防重放是概率防御,幂等是确定性防御
- HTTPS 是前提:签名方案不防抓包后的重放,但防不了密钥窃取;敏感操作叠加生物/二次认证
- 证书绑定(certificate pinning)防中间人抓包,降低签名被截获分析的可能
3. 风控维度补充:同一签名特征高频重复请求(即使 nonce 都不同)→ 识别为脚本,触发验证码或限流。时间容忍窗口防时钟偏移(NTP 校时),建议 ±5 分钟内由 nonce 兜底。
一句话:签名防篡改、timestamp 限窗口、nonce 防重放、幂等兜底,四层递进。
💻 全栈开发专家
重放的本质是攻击者拿合法签名的历史请求再发一遍,防线是"请求新鲜度 + 一次性随机数 + 一次性业务票据"。
核心机制:timestamp + nonce + 签名
客户端签名时必须包含时间戳和随机数:
text
sign = HMAC-SHA256(appSecret, method + path + sortedParams + body + timestamp + nonce)网关校验三步:
- 验签:用同样算法重算比对(常量时间比较,防时序侧信道),防篡改。
- 时间窗:
|now - timestamp| > 300s直接拒绝——重放的老请求时间戳过期。 - nonce 一次性:nonce 存 Redis
SETNX+ 过期时间 = 时间窗长度:
java
Boolean first = redis.opsForValue().setIfAbsent("nonce:" + nonce, "1", 5, TimeUnit.MINUTES);
if (Boolean.FALSE.equals(first)) return reject("replay"); // 重复 nonce = 重放时间窗内相同的 nonce 出现第二次即判定重放。窗口设 5 分钟是平衡:太短客户端时钟偏移会误杀,太长 Redis 存储压力大。
纵深防御
- 传输层:全站 HTTPS/TLS 1.3,移动端证书锁定(certificate pinning)防中间人抓包。
- 票据一次性:敏感操作(支付、改密)在签名之外再发一次性 token(用后即焚),抓到历史请求也没用。
- 挑战应答:服务端先下发随机 challenge,签名包含 challenge,从根上杜绝"预生成签名重放"。
常见坑
- 只签名 query 不签 body,body 被替换签名仍合法——签名要覆盖完整请求。
- nonce 判重必须和时间窗校验同时开,只判 nonce 不限时间,攻击者可以提前囤积nonce。
- Redis 挂掉时的降级策略要提前定(默认拒绝还是放行只验时间窗),别到线上才拍脑袋。