SpringBoot + 网关请求签名验证 + 防篡改:关键接口参数签名,杜绝中间人攻击
前言
在当今互联网应用中,API 接口安全面临着严峻挑战:
- 中间人攻击:攻击者截获并篡改请求参数
- 重放攻击:攻击者重复发送已捕获的请求
- 参数篡改:恶意修改请求参数获取非法利益
- 非法调用:未授权的第三方调用敏感接口
这些安全威胁可能导致用户数据泄露、资金损失等严重后果。本文将详细介绍如何使用 Spring Boot 实现基于 HMAC-SHA256 的请求签名验证机制,有效防止中间人攻击和参数篡改。
一、API 安全威胁分析
1. 常见攻击类型
| 攻击类型 | 攻击原理 | 危害程度 | 防护难度 |
|---|---|---|---|
| 中间人攻击 | 截获并篡改通信内容 | 高 | 中 |
| 重放攻击 | 重复发送有效请求 | 中 | 低 |
| 参数篡改 | 修改请求参数 | 高 | 中 |
| 非法调用 | 未授权访问接口 | 中 | 低 |
| 数据泄露 | 截获敏感数据 | 高 | 高 |
2. 传统防护方案的局限性
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| HTTPS | 加密传输 | 无法防止参数篡改 | 基础安全 |
| Token 认证 | 身份验证 | 无法防止参数篡改 | 用户认证 |
| IP 白名单 | 简单直接 | 无法应对动态 IP | 内部服务 |
| 签名验证 | 防篡改、防重放 | 实现复杂 | 关键接口 |
3. 签名验证的优势
- 防篡改:任何参数修改都会导致签名验证失败
- 防重放:通过时间戳和随机数防止重复请求
- 身份验证:通过密钥确认调用方身份
- 完整性校验:确保请求内容完整无损
二、签名验证原理
1. HMAC-SHA256 算法
HMAC(Hash-based Message Authentication Code)是一种基于哈希的消息认证码算法:
HMAC-SHA256(key, message) = SHA256((key ⊕ opad) || SHA256((key ⊕ ipad) || message))
特点:
- 单向不可逆:无法从签名反推原始数据
- 密钥保护:签名需要密钥参与,攻击者无法伪造
- 雪崩效应:原始数据微小变化导致签名完全不同
- 高效计算:计算速度快,适合高并发场景
2. 签名生成流程
┌─────────────────────────────────────────────────────────────┐
│ 客户端签名生成流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 收集签名参数 │
│ ┌─────────────────────────────────────────────┐ │
│ │ appKey=your_app_key │ │
│ │ timestamp=1705276800000 │ │
│ │ nonce=a1b2c3d4e5f6 │ │
│ │ method=POST │ │
│ │ path=/api/order/create │ │
│ │ body={"amount":100,"userId":123} │ │
│ └─────────────────────────────────────────────┘ │
│ ↓ │
│ 2. 参数排序并拼接 │
│ ┌─────────────────────────────────────────────┐ │
│ │ appKey=your_app_key&body={"amount":100, │ │
│ │ "userId":123}&method=POST&nonce=a1b2c3d4e5f6│ │
│ │ &path=/api/order/create×tamp=1705276800│ │
│ └─────────────────────────────────────────────┘ │
│ ↓ │
│ 3. HMAC-SHA256 签名 │
│ ┌─────────────────────────────────────────────┐ │
│ │ signature = HMAC-SHA256(secretKey, message) │ │
│ └─────────────────────────────────────────────┘ │
│ ↓ │
│ 4. 发送请求 │
│ ┌─────────────────────────────────────────────┐ │
│ │ POST /api/order/create │ │
│ │ Headers: │ │
│ │ X-App-Key: your_app_key │ │
│ │ X-Timestamp: 1705276800000 │ │
│ │ X-Nonce: a1b2c3d4e5f6 │ │
│ │ X-Signature: abc123def456... │ │
│ │ Body: {"amount":100,"userId":123} │ │
│ └─────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
3. 签名验证流程
┌─────────────────────────────────────────────────────────────┐
│ 服务端签名验证流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 接收请求并提取签名参数 │
│ ┌─────────────────────────────────────────────┐ │
│ │ X-App-Key: your_app_key │ │
│ │ X-Timestamp: 1705276800000 │ │
│ │ X-Nonce: a1b2c3d4e5f6 │ │
│ │ X-Signature: abc123def456... │ │
│ └─────────────────────────────────────────────┘ │
│ ↓ │
│ 2. 验证时间戳有效性 │
│ ┌─────────────────────────────────────────────┐ │
│ │ |当前时间 - 请求时间| < 时间窗口(5分钟) │ │
│ └─────────────────────────────────────────────┘ │
│ ↓ │
│ 3. 验证随机数唯一性 │
│ ┌─────────────────────────────────────────────┐ │
│ │ 检查 nonce 是否已使用(防重放) │ │
│ └─────────────────────────────────────────────┘ │
│ ↓ │
│ 4. 重新计算签名 │
│ ┌─────────────────────────────────────────────┐ │
│ │ serverSignature = HMAC-SHA256(secretKey, │ │
│ │ buildSignMessage()) │ │
│ └─────────────────────────────────────────────┘ │
│ ↓ │
│ 5. 比对签名 │
│ ┌─────────────────────────────────────────────┐ │
│ │ serverSignature == clientSignature ? │ │
│ │ ✓ 验证通过 │ │
│ │ ✗ 验证失败 │ │
│ └─────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
三、技术选型
| 技术 | 版本 | 用途 |
|---|---|---|
| Spring Boot | 3.2.0 | 基础框架 |
| Spring AOP | - | 切面编程 |
| Redis | 7.0+ | 存储随机数防重放 |
| 自定义注解 | - | 标记需要签名验证的接口 |
| 拦截器 | - | 统一处理签名验证 |
四、代码实现
1. 项目结构
SpringBoot-ApiSignature-Demo/
├── src/
│ └── main/
│ ├── java/
│ │ └── com/
│ │ └── example/
│ │ └── signature/
│ │ ├── ApiSignatureApplication.java
│ │ ├── annotation/
│ │ │ └── RequireSignature.java
│ │ ├── config/
│ │ │ ├── SignatureConfig.java
│ │ │ └── WebConfig.java
│ │ ├── interceptor/
│ │ │ └── SignatureInterceptor.java
│ │ ├── service/
│ │ │ ├── SignatureService.java
│ │ │ └── NonceService.java
│ │ ├── controller/
│ │ │ ├── OrderController.java
│ │ │ └── SignatureTestController.java
│ │ ├── dto/
│ │ │ ├── ApiRequest.java
│ │ │ └── ApiResponse.java
│ │ └── exception/
│ │ └── SignatureException.java
│ └── resources/
│ └── application.yml
├── pom.xml
└── README.md
2. 核心代码实现
2.1 签名验证注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequireSignature {
/**
* 是否必填签名
*/
boolean required() default true;
/**
* 时间窗口(秒)
*/
long timeWindow() default 300;
/**
* 是否验证 nonce
*/
boolean checkNonce() default true;
}
2.2 签名验证拦截器
@Component
public class SignatureInterceptor implements HandlerInterceptor {
@Autowired
private SignatureService signatureService;
@Autowired
private NonceService nonceService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
RequireSignature annotation = handlerMethod.getMethodAnnotation(RequireSignature.class);
if (annotation == null || !annotation.required()) {
return true;
}
// 验证签名
try {
String appKey = request.getHeader("X-App-Key");
String timestamp = request.getHeader("X-Timestamp");
String nonce = request.getHeader("X-Nonce");
String signature = request.getHeader("X-Signature");
// 1. 参数校验
if (StringUtils.isAnyBlank(appKey, timestamp, nonce, signature)) {
throw new SignatureException("签名参数不完整");
}
// 2. 时间戳验证
long requestTime = Long.parseLong(timestamp);
long currentTime = System.currentTimeMillis();
if (Math.abs(currentTime - requestTime) > annotation.timeWindow() * 1000) {
throw new SignatureException("请求已过期");
}
// 3. Nonce 验证(防重放)
if (annotation.checkNonce() && nonceService.exists(nonce)) {
throw new SignatureException("重复的请求");
}
// 4. 签名验证
String requestBody = getRequestBody(request);
boolean valid = signatureService.verify(appKey, timestamp, nonce,
request.getMethod(),
request.getRequestURI(),
requestBody,
signature);
if (!valid) {
throw new SignatureException("签名验证失败");
}
// 5. 记录 nonce
if (annotation.checkNonce()) {
nonceService.save(nonce, annotation.timeWindow());
}
return true;
} catch (SignatureException e) {
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":401,\"message\":\"" + e.getMessage() + "\"}");
return false;
}
}
}
2.3 签名服务
@Service
public class SignatureService {
@Value("${signature.secret-key}")
private String secretKey;
private static final String HMAC_SHA256 = "HmacSHA256";
public String generate(String appKey, String timestamp, String nonce,
String method, String path, String body) {
String message = buildSignMessage(appKey, timestamp, nonce, method, path, body);
return hmacSha256(secretKey, message);
}
public boolean verify(String appKey, String timestamp, String nonce,
String method, String path, String body, String signature) {
String expectedSignature = generate(appKey, timestamp, nonce, method, path, body);
return expectedSignature.equals(signature);
}
private String buildSignMessage(String appKey, String timestamp, String nonce,
String method, String path, String body) {
// 参数排序并拼接
Map<String, String> params = new TreeMap<>();
params.put("appKey", appKey);
params.put("timestamp", timestamp);
params.put("nonce", nonce);
params.put("method", method);
params.put("path", path);
if (StringUtils.isNotBlank(body)) {
params.put("body", body);
}
return params.entrySet().stream()
.map(e -> e.getKey() + "=" + e.getValue())
.collect(Collectors.joining("&"));
}
private String hmacSha256(String key, String message) {
try {
Mac mac = Mac.getInstance(HMAC_SHA256);
SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), HMAC_SHA256);
mac.init(secretKeySpec);
byte[] hash = mac.doFinal(message.getBytes(StandardCharsets.UTF_8));
return Hex.encodeHexString(hash);
} catch (Exception e) {
throw new RuntimeException("签名生成失败", e);
}
}
}
2.4 Nonce 服务(防重放)
@Service
public class NonceService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private static final String NONCE_PREFIX = "signature:nonce:";
public boolean exists(String nonce) {
return redisTemplate.hasKey(NONCE_PREFIX + nonce);
}
public void save(String nonce, long ttl) {
redisTemplate.opsForValue().set(NONCE_PREFIX + nonce, "1", ttl, TimeUnit.SECONDS);
}
}
3. 控制器使用示例
@RestController
@RequestMapping("/api/order")
public class OrderController {
@RequireSignature(timeWindow = 300, checkNonce = true)
@PostMapping("/create")
public ApiResponse<Order> createOrder(@RequestBody OrderRequest request) {
// 业务逻辑
return ApiResponse.success(order);
}
@RequireSignature(timeWindow = 60, checkNonce = true)
@PostMapping("/pay")
public ApiResponse<Payment> payOrder(@RequestBody PaymentRequest request) {
// 支付逻辑
return ApiResponse.success(payment);
}
}
4. 客户端签名示例
public class ApiClient {
private String appKey;
private String secretKey;
private RestTemplate restTemplate;
public <T> T post(String path, Object body, Class<T> responseType) {
String timestamp = String.valueOf(System.currentTimeMillis());
String nonce = UUID.randomUUID().toString().replace("-", "");
String bodyJson = objectMapper.writeValueAsString(body);
// 生成签名
String signature = generateSignature(appKey, timestamp, nonce, "POST", path, bodyJson);
// 构建请求头
HttpHeaders headers = new HttpHeaders();
headers.set("X-App-Key", appKey);
headers.set("X-Timestamp", timestamp);
headers.set("X-Nonce", nonce);
headers.set("X-Signature", signature);
headers.setContentType(MediaType.APPLICATION_JSON);
// 发送请求
HttpEntity<String> entity = new HttpEntity<>(bodyJson, headers);
return restTemplate.postForObject(path, entity, responseType);
}
}
五、安全最佳实践
1. 密钥管理
| 策略 | 说明 | 重要性 |
|---|---|---|
| 密钥分离 | 不同应用使用不同密钥 | 高 |
| 定期轮换 | 定期更换密钥 | 高 |
| 安全存储 | 密钥加密存储,不硬编码 | 高 |
| 权限控制 | 限制密钥访问权限 | 中 |
| 审计日志 | 记录密钥使用情况 | 中 |
2. 时间窗口设置
| 场景 | 推荐时间窗口 | 说明 |
|---|---|---|
| 支付接口 | 60秒 | 高安全性要求 |
| 订单接口 | 300秒 | 平衡安全与体验 |
| 查询接口 | 600秒 | 较宽松的限制 |
| 内部服务 | 30秒 | 高安全性要求 |
3. 防重放策略
| 策略 | 实现方式 | 优缺点 |
|---|---|---|
| Nonce 缓存 | Redis 存储 nonce | 简单有效,依赖 Redis |
| 时间窗口 | 限制请求有效时间 | 无状态,但有时间窗口 |
| 序列号 | 递增序列号 | 严格有序,但复杂 |
| 组合策略 | Nonce + 时间窗口 | 最安全,推荐使用 |
4. 签名算法选择
| 算法 | 安全性 | 性能 | 适用场景 |
|---|---|---|---|
| HMAC-SHA256 | 高 | 高 | 推荐,通用场景 |
| HMAC-SHA512 | 更高 | 中 | 高安全要求 |
| RSA-SHA256 | 高 | 低 | 非对称场景 |
| SM3 | 高 | 高 | 国密要求 |
六、性能优化
1. 签名计算优化
- 缓存签名结果:相同参数缓存签名结果
- 并行计算:多线程计算签名
- 算法优化:选择高效的签名算法
2. Nonce 存储优化
- 布隆过滤器:快速判断 nonce 是否存在
- 本地缓存:结合本地缓存减少 Redis 访问
- 批量清理:定期清理过期 nonce
3. 拦截器优化
- 异步验证:非关键验证异步处理
- 白名单机制:信任的请求跳过验证
- 批量验证:批量请求统一验证
七、常见问题
Q1: 如何处理大文件上传的签名?
A: 可以:
- 对文件内容计算摘要(如 MD5)
- 将摘要参与签名计算
- 分块上传时,每块单独签名
Q2: 如何实现签名密钥的动态更新?
A: 可以:
- 使用配置中心管理密钥
- 支持多版本密钥并存
- 平滑切换,兼容新旧密钥
Q3: 如何处理时间不同步问题?
A: 可以:
- 使用 NTP 同步服务器时间
- 放宽时间窗口限制
- 记录时间偏差,动态调整
Q4: 如何防止签名泄露?
A: 可以:
- 使用 HTTPS 加密传输
- 签名包含时间戳,限制有效期
- 定期更换密钥
- 监控异常签名请求
Q5: 如何实现多端签名?
A: 可以:
- 每个端使用不同的 appKey
- 服务端根据 appKey 查找对应密钥
- 支持不同端的不同权限
八、扩展功能
1. 签名白名单
@Configuration
public class SignatureConfig {
@Bean
public SignatureWhiteList signatureWhiteList() {
return SignatureWhiteList.builder()
.addPath("/api/health")
.addPath("/api/public/**")
.build();
}
}
2. 签名日志记录
@Aspect
@Component
public class SignatureLogAspect {
@AfterReturning("@annotation(requireSignature)")
public void logSuccess(RequireSignature requireSignature) {
// 记录成功日志
}
@AfterThrowing(value = "@annotation(requireSignature)", throwing = "ex")
public void logFailure(RequireSignature requireSignature, Exception ex) {
// 记录失败日志,告警
}
}
3. 签名监控
- 签名成功率:监控签名验证的成功率
- 签名耗时:监控签名验证的耗时
- 异常签名:监控异常签名请求,及时告警
- 密钥使用:监控密钥使用情况
九、总结
通过 Spring Boot + HMAC-SHA256 实现的请求签名验证机制,可以有效防止:
- 中间人攻击:签名验证确保请求未被篡改
- 重放攻击:Nonce + 时间窗口防止重复请求
- 非法调用:密钥验证确认调用方身份
- 参数篡改:任何参数修改都会导致签名失败
这种方案具有以下优势:
- 安全性高:HMAC-SHA256 算法安全可靠
- 实现简单:基于注解和拦截器,无侵入性
- 性能优秀:签名计算高效,支持高并发
- 易于扩展:支持多种签名算法和策略
更多技术文章,欢迎关注公众号"服务端技术精选",及时获取最新动态。
标题:SpringBoot + 网关请求签名验证 + 防篡改:关键接口参数签名,杜绝中间人攻击
作者:jiangyi
地址:http://jiangyi.space/articles/2026/03/27/1774243876398.html
公众号:服务端技术精选
- 前言
- 一、API 安全威胁分析
- 1. 常见攻击类型
- 2. 传统防护方案的局限性
- 3. 签名验证的优势
- 二、签名验证原理
- 1. HMAC-SHA256 算法
- 2. 签名生成流程
- 3. 签名验证流程
- 三、技术选型
- 四、代码实现
- 1. 项目结构
- 2. 核心代码实现
- 2.1 签名验证注解
- 2.2 签名验证拦截器
- 2.3 签名服务
- 2.4 Nonce 服务(防重放)
- 3. 控制器使用示例
- 4. 客户端签名示例
- 五、安全最佳实践
- 1. 密钥管理
- 2. 时间窗口设置
- 3. 防重放策略
- 4. 签名算法选择
- 六、性能优化
- 1. 签名计算优化
- 2. Nonce 存储优化
- 3. 拦截器优化
- 七、常见问题
- Q1: 如何处理大文件上传的签名?
- Q2: 如何实现签名密钥的动态更新?
- Q3: 如何处理时间不同步问题?
- Q4: 如何防止签名泄露?
- Q5: 如何实现多端签名?
- 八、扩展功能
- 1. 签名白名单
- 2. 签名日志记录
- 3. 签名监控
- 九、总结
评论
0 评论