在分布式、微服务架构盛行的今天,API 是系统间通信的基石。暴露在公网的接口面临四大安全威胁:
流量过载:恶意或意外的巨大流量打垮你的服务。
数据窃取:请求/响应在传输过程中被窃听或篡改。
身份伪造:非法用户冒充合法身份调用接口。
重复攻击:合法请求被捕获后重复发送,造成业务异常(如重复下单、重复转账)。
“三板斧”正是为应对这些威胁而设计:
限流 (Rate Limiting):保护系统不被流量冲垮,保证服务稳定。
防重放 (Replay Attack Prevention):确保请求唯一性,防止重复攻击。
签名验证 (Signature Verification):验证请求者身份及数据完整性,防止篡改和伪造。
限制单位时间内系统能处理的请求数量,一旦超过阈值,对后续请求拒绝、排队或降级处理。
核心目标是保护系统稳定性,有效缓解 DDoS 或 CC 攻击。
算法 | 原理 | 优点 | 缺点 |
固定窗口计数器 | 时间窗口内计数,请求超过阈值则拒绝 | 简单高效 | 临界问题,突发流量可能压垮系统 |
滑动窗口日志 | 保存每个请求时间戳,统计窗口内请求数 | 精准 | 内存消耗大,高并发性能压力 |
滑动窗口计数器 | 大窗口拆成小窗口,结合权重统计 | 较精准,节省内存 | 实现复杂 |
令牌桶 | 按固定速率生成令牌,请求需拿到令牌 | 支持突发流量 | 实现复杂 |
漏桶 | 请求按恒定速率处理,多余丢弃 | 输出均匀 | 突发流量可能被丢弃 |
@Component@RequiredArgsConstructorpublic class DistributedRateLimiter { private final RedisTemplate<String, String> redisTemplate; public boolean isAllowed(String key, int windowInSeconds, int maxRequests) { long now = System.currentTimeMillis(); long windowStart = now - (windowInSeconds * 1000L); // 移除窗口外的旧数据
redisTemplate.opsForZSet().removeRangeByScore(key, 0, windowStart); // 获取当前窗口内请求数量
Long count = redisTemplate.opsForZSet().zCard(key); if (count != null && count >= maxRequests) return false; // 添加当前请求
redisTemplate.opsForZSet().add(key, String.valueOf(now), now);
redisTemplate.expire(key, windowInSeconds + 1, TimeUnit.SECONDS); return true;
}
}使用示例:
String key = "rate_limit:submit_order:" + userId;if (!rateLimiter.isAllowed(key, 60, 30)) { return ResponseEntity.status(429).body("Too Many Requests");
}网关+应用双层限流:网关粗粒度(IP维度),应用细粒度(用户/接口维度)。
Lua 原子操作:保证 Redis 多操作原子性。
动态阈值:结合 Prometheus 监控,根据流量动态调整限流策略。
确保每个请求只能被处理一次,核心是请求的 唯一性和时效性。
Nonce(一次性随机数)
Timestamp + Nonce(推荐):请求携带时间戳和随机数,服务端只需保存时间窗口内的 Nonce,即可验证唯一性。
@Component@RequiredArgsConstructorpublic class ReplayAttackValidator { private final RedisTemplate<String, String> redisTemplate; private static final long TIME_TOLERANCE_MS = 5 * 60 * 1000; // 5分钟 public boolean validate(String timestamp, String nonce) {
long clientTime; try { clientTime = Long.parseLong(timestamp); }
catch (NumberFormatException e) { return false; }
long serverTime = System.currentTimeMillis(); if (Math.abs(serverTime - clientTime) > TIME_TOLERANCE_MS) return false; String key = "nonce:" + timestamp + ":" + nonce; Boolean isAbsent = redisTemplate.opsForValue().setIfAbsent(key, "1", TIME_TOLERANCE_MS, TimeUnit.MILLISECONDS); return Boolean.TRUE.equals(isAbsent);
}
}秒/毫秒级窗口,避免时间漂移误判。
布隆过滤器+Redis,高并发场景快速判重。
高价值接口二次校验:结合 IP、User-Agent。
确保请求来自合法客户端,且数据在传输过程中未被篡改。
分配密钥:每个客户端唯一 AppId + SecretKey(不在网络中传输)。
客户端生成签名:
筛选参数(URL、Query、Body、公共参数如 AppId、Timestamp、Nonce)。
按参数名 ASCII 排序。
拼接成 StringToSign。
用 HMAC-SHA256 + SecretKey 计算签名。
将签名和公共参数放入 Header。
服务端验证签名:
先做防重放验证。
根据 AppId 查询 SecretKey。
重建 StringToSign 并计算服务端签名。
比对签名,一致放行,否者拒绝。
import hashlib, hmac, time, uuid
app_id = "your_app_id"secret_key = b"your_secret_key"timestamp = str(int(time.time()*1000))
nonce = str(uuid.uuid4())
body = '{"data":"test"}'
params = {"appId": app_id, "timestamp": timestamp, "nonce": nonce, "body": body}
sorted_params = sorted(params.items())
string_to_sign = '&'.join(f"{k}={v}" for k,v in sorted_params)
signature = hmac.new(secret_key, string_to_sign.encode(), hashlib.sha256).hexdigest()
headers = {"X-App-ID": app_id, "X-Timestamp": timestamp, "X-Nonce": nonce, "X-Signature": signature}@Component@Order(1) // 高优先级过滤器@RequiredArgsConstructorpublic class ApiAuthFilter extends OncePerRequestFilter { private final ReplayAttackValidator replayValidator; private final SecretKeyService secretKeyService; // 自定义服务,用于查询SecretKey @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // 1. 获取公共参数 String appId = request.getHeader("X-App-ID"); String timestamp = request.getHeader("X-Timestamp"); String nonce = request.getHeader("X-Nonce"); String clientSignature = request.getHeader("X-Signature"); // 2. 基础检查 if (StringUtils.isEmpty(appId) || ...) {
response.sendError(401, "Missing Auth Headers"); return;
} // 3. 防重放验证 if (!replayValidator.validate(timestamp, nonce)) {
response.sendError(401, "Replay Attack Detected"); return;
} // 4. 根据AppId查询SecretKey String secretKey = secretKeyService.getSecretKeyByAppId(appId); if (secretKey == null) {
response.sendError(401, "Invalid AppId"); return;
} // 5. 读取RequestBody(需要包装Request,由于流只能读一次)
CachedBodyHttpServletRequest cachedRequest = new CachedBodyHttpServletRequest(request); String requestBody = IOUtils.toString(cachedRequest.getInputStream(), StandardCharsets.UTF_8); // 6. 组装服务端的待签名字符串(规则必须与客户端完全一致!)
Map<String, String> params = new TreeMap<>(); // TreeMap自动按Key排序
params.put("appId", appId);
params.put("timestamp", timestamp);
params.put("nonce", nonce);
params.put("body", requestBody); // 将请求体作为签名参数 String stringToSign = params.entrySet().stream()
.map(entry -> entry.getKey() + "=" + entry.getValue())
.collect(Collectors.joining("&")); // 7. 计算服务端签名 String serverSignature = HmacUtils.hmacSha256Hex(secretKey, stringToSign); // 8. 比较签名 if (!serverSignature.equalsIgnoreCase(clientSignature)) {
response.sendError(401, "Invalid Signature"); return;
} // 9. 验证通过,放行请求
filterChain.doFilter(cachedRequest, response);
}
}支持多种签名算法:HMAC-SHA1/SHA256、RSA-SHA256。
设置签名版本号(X-Sign-Version)保证兼容。
Body 内容大时只对摘要签名。
幂等接口额外要求幂等 ID。

组合使用三板斧,构建多层安全防护:
限流 → 防重放 → 签名验证。
密钥管理安全:使用 Vault/KMS,定期轮换密钥。
灵活策略:不同 API、用户、IP 维度可设置不同限流。
监控告警:限流、签名失败、重放请求均需日志和告警。
客户端安全:App 做混淆加固,Web 前端使用 OAuth2 或 Token 授权。
生产级增强:Lua 原子操作、布隆过滤器、高价值接口二次校验、签名版本控制。
通过落地这套“三板斧”,你的接口系统将具备 稳定、高效、抗攻击、可信赖 的安全保障。