接口安全设计
签名验签
签名流程
客户端:
1. 拼接参数(按字典序)+ Timestamp + Nonce
2. 使用 SecretKey 计算 HMAC-SHA256
3. 将签名放在 Header 中发送
服务端:
1. 校验 Timestamp 是否在有效期内(±5min)
2. 校验 Nonce 是否已使用(Redis Set)
3. 用相同方式计算签名,比对是否一致实现示例
java
public class SignUtil {
private static final long EXPIRE_SECONDS = 300;
public static String sign(Map<String, String> params, String secret) {
// 1. 参数排序
TreeMap<String, String> sorted = new TreeMap<>(params);
// 2. 拼接字符串
String content = sorted.entrySet().stream()
.map(e -> e.getKey() + "=" + e.getValue())
.collect(Collectors.joining("&"));
// 3. HMAC-SHA256
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec key = new SecretKeySpec(
secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
mac.init(key);
byte[] result = mac.doFinal(content.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(result);
}
public static boolean verify(Map<String, String> params,
String sign, String secret, String nonce, long ts) {
// 时间校验
if (Math.abs(System.currentTimeMillis() / 1000 - ts) > EXPIRE_SECONDS) {
return false;
}
// Nonce 防重放(需 Redis 去重)
// 计算签名比对
String calc = sign(params, secret);
return calc.equals(sign);
}
}加密传输
传输层(TLS)
生产环境必须使用 HTTPS,避免中间人攻击。详见 HTTPS 与 TLS 原理。
应用层加密
yaml
# 敏感字段额外加密(防脱库泄露)
请求体: RSA/国密 SM2 加密
响应体: AES-256-GCM 加密
密钥: 每次请求协商(ECDH)防重放
| 方案 | 实现 | 适用场景 |
|---|---|---|
| Nonce | 每次请求携带唯一随机数,Redis 5min 过期去重 | 通用 API |
| Timestamp | 请求时间戳 ±5min 有效 | 配合 Nonce 使用 |
| Sequence | 单调递增序号 | 支付/转账 |
| Token 绑定 | Token 绑定客户端 IP/设备指纹 | 敏感操作 |
Nonce 去重实现
java
// Redis 原子检查 + 设置
String key = "nonce:" + nonce;
Boolean success = redisTemplate.opsForValue()
.setIfAbsent(key, "1", Duration.ofMinutes(5));
if (Boolean.FALSE.equals(success)) {
throw new SecurityException("重复请求");
}限流
算法对比
| 算法 | 特点 | 适用场景 |
|---|---|---|
| 令牌桶 | 允许短时突发 | 通用 API |
| 漏桶 | 平滑流量 | 网关入口 |
| 滑动窗口 | 精度高 | 精细控制 |
| 计数器 | 实现简单 | 粗粒度限流 |
Spring Boot 限流示例
java
@Component
public class RateLimiterAspect {
private final Map<String, RateLimiter> limiters = new ConcurrentHashMap<>();
@Around("@annotation(rateLimit)")
public Object around(ProceedingJoinPoint pjp, RateLimit rateLimit) {
String key = rateLimit.key();
RateLimiter limiter = limiters.computeIfAbsent(key,
k -> RateLimiter.create(rateLimit.qps()));
if (!limiter.tryAcquire()) {
throw new TooManyRequestsException("请求过于频繁");
}
return pjp.proceed();
}
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
String key();
double qps() default 100;
}