提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
前言
在 NestJS 架构中,实现 防重放攻击(Replay Attack) 是保护接口安全、提升风控能力的关键环节,尤其是在支付、订单、登录、敏感操作等场景中。
✅ 一、重放攻击的本质
攻击者截获合法请求并 重复发送(Replay),从而实现例如:
- 重复下单
- 重复提现
- 重复授权
✅ 二、大厂标准防重放方案核心要点
防护要素 | 实现方式 |
---|---|
请求唯一标识 | 每次请求携带唯一 nonce (UUID + 时间戳) |
时间戳限制 | 请求时间不得超过系统容忍的时间窗口(如 60 秒) |
服务端验签 | 所有请求参数和 timestamp + nonce 一起签名 |
签名校验 | 校验 signature 合法性,防止参数被篡改 |
nonce 缓存 | 将 nonce 缓存在 Redis,一旦使用即失效(幂等性) |
✅ 三、NestJS 实现步骤(示例)
1️⃣ 客户端请求结构(示例)
{
"data": { "amount": 100 },
"timestamp": "1713772601",
"nonce": "a1b2c3d4e5f6",
"sign": "6f1a0e5c84..."
}
2️⃣ 服务端实现防重放拦截器
// src/common/interceptors/replay.interceptor.ts
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
BadRequestException,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import * as crypto from 'crypto';
import { Redis } from 'ioredis';
@Injectable()
export class ReplayInterceptor implements NestInterceptor {
constructor(private readonly redis: Redis, private readonly secret: string) {}
async intercept(context: ExecutionContext, next: CallHandler): Promise<Observable<any>> {
const request = context.switchToHttp().getRequest();
const { timestamp, nonce, sign, data } = request.body;
// 1. 时间戳合法性
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - Number(timestamp)) > 60) {
throw new BadRequestException('Request expired');
}
// 2. nonce 是否重复
const nonceKey = `replay:nonce:${nonce}`;
const exists = await this.redis.get(nonceKey);
if (exists) throw new BadRequestException('Replay detected');
await this.redis.set(nonceKey, '1', 'EX', 120); // 防止短时间重复请求
// 3. 签名校验
const payload = JSON.stringify(data);
const expectedSign = crypto
.createHmac('sha256', this.secret)
.update(`${timestamp}.${nonce}.${payload}`)
.digest('hex');
if (sign !== expectedSign) throw new BadRequestException('Invalid signature');
return next.handle();
}
}
3️⃣ 应用到指定接口或全局
// 全局或局部注册
app.useGlobalInterceptors(new ReplayInterceptor(redis, process.env.SIGN_SECRET));
或在控制器中使用 @UseInterceptors(ReplayInterceptor)
。
4️⃣ 中央配置 + 自动绑定(可选)
封装为通用模块:
@Module({
providers: [
{
provide: APP_INTERCEPTOR,
useFactory: () => new ReplayInterceptor(redisInstance, process.env.SIGN_SECRET),
},
],
})
export class SecurityModule {}
✅ 四、补充建议(实践)
内容 | 建议 |
---|---|
Redis Key | 设计合理前缀,如 replay:nonce:<nonce> ,TTL 建议60-120秒 |
密钥管理 | secret 建议从配置中心或 KMS 动态注入 |
API 限流 | 搭配 @nestjs/throttler ,进一步提升防护 |
安全日志 | 签名失败、重放拦截需日志审计 |
SDK封装 | 客户端签名逻辑建议封装 SDK,统一集成 |
✅ 五、签名逻辑建议(客户端 / 服务端一致)
签名规则建议格式为:
SHA256_HMAC(secret, `${timestamp}.${nonce}.${JSON(data)}`)
也可自定义签名规则并文档化,配合 Swagger 提供示例。
那为什么选用Redis
在防重放方案中普遍选择 Redis 作为存储介质,原因主要体现在以下几点:
🚀 1. 高性能、低延迟
- Redis 的存取速度极快(毫秒级别甚至亚毫秒)。
- 请求防重放校验一般属于高频请求,每次请求都需要校验nonce是否被使用,高性能的 Redis 可最大程度减少额外延迟。
🔗 2. 分布式场景支持
- Redis 天然支持分布式部署。
- 在微服务架构、多实例部署时,使用本地内存存储无法保证一致性,而 Redis 可以天然地支持共享 nonce 数据。
- 多个 NestJS 实例可以共用 Redis 实现全局统一的防重放机制。
⏳ 3. 原生支持 TTL(过期时间)
- Redis 提供原生的 key 过期机制(TTL)。
- 对 nonce 自动过期管理非常友好,不需要额外代码实现。
- nonce 一般只需要短暂保存(如 60~120秒),Redis 的 TTL 特性极大降低了开发复杂度。
示例:
await redis.set(nonceKey, '1', 'EX', 120);
⚡️ 4. 内存存储,高并发场景优势明显
- Redis 基于内存存储,高并发场景吞吐能力卓越,适合大量并发请求。
- 防重放是典型高并发验证场景,MySQL 或其他磁盘型数据库性能明显不足。
📈 5. 横向扩展与高可用性
- Redis Cluster 或 Redis Sentinel 可以轻松实现高可用性和扩展。
- 即使 Redis 节点故障,也不会影响整体服务的稳定性。
🔐 6. 成熟且稳定的技术栈
- Redis 是主流企业广泛使用的缓存与中间件服务,可靠性、稳定性高。
- 企业级开源社区和商业版支持成熟完善。
🔍 7. 易于监控和观测
- Redis 提供了丰富的监控指标,可以轻松整合到监控体系(如 Prometheus + Grafana)。
- 可快速定位防重放机制的热点问题或异常。
🛠️ Redis 对比其他方案(如本地缓存或数据库):
对比维度 | Redis | 本地内存缓存 | MySQL等磁盘数据库 |
---|---|---|---|
速度与延迟 | ✅ 极快 | ✅ 快 | ❌ 较慢 |
分布式支持 | ✅ 支持 | ❌ 不支持(实例独立) | ✅ 支持但复杂性高 |
数据一致性 | ✅ 优秀 | ❌ 较差(实例隔离) | ✅ 优秀 |
TTL过期特性 | ✅ 原生 | ✅ 需要额外实现 | ❌ 需要额外实现 |
高并发处理能力 | ✅ 极高 | ✅ 高(但不分布式) | ❌ 一般 |
监控能力 | ✅ 丰富 | ❌ 一般 | ✅ 丰富但成本较高 |
💡 总结:为什么 Redis 最适合企业级别的防重放?
企业级别普遍场景:
- 高并发
- 分布式架构
- 严格性能需求
这些场景中,Redis 的高效能、分布式特性、TTL 特性完美满足需求,几乎是大厂实施防重放机制的最佳实践标准。
这也是为什么防重放方案普遍选用 Redis 的原因。