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&timestamp=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 Boot3.2.0基础框架
Spring AOP-切面编程
Redis7.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: 可以:

  1. 对文件内容计算摘要(如 MD5)
  2. 将摘要参与签名计算
  3. 分块上传时,每块单独签名

Q2: 如何实现签名密钥的动态更新?

A: 可以:

  1. 使用配置中心管理密钥
  2. 支持多版本密钥并存
  3. 平滑切换,兼容新旧密钥

Q3: 如何处理时间不同步问题?

A: 可以:

  1. 使用 NTP 同步服务器时间
  2. 放宽时间窗口限制
  3. 记录时间偏差,动态调整

Q4: 如何防止签名泄露?

A: 可以:

  1. 使用 HTTPS 加密传输
  2. 签名包含时间戳,限制有效期
  3. 定期更换密钥
  4. 监控异常签名请求

Q5: 如何实现多端签名?

A: 可以:

  1. 每个端使用不同的 appKey
  2. 服务端根据 appKey 查找对应密钥
  3. 支持不同端的不同权限

八、扩展功能

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
公众号:服务端技术精选
    评论
    0 评论
avatar

取消