SpringBoot 对接美团外卖霸王餐 API:签名算法、时间戳校验与重放攻击防御实战
在对接美团外卖霸王餐 API 这类涉及资金与订单的核心业务时,接口的安全性是重中之重。一个不安全的接口不仅可能导致数据泄露,更可能遭受重放攻击,造成资损。
本文将深入探讨如何在 Spring Boot 项目中,通过实现签名算法、时间戳校验和 nonce 机制,构建一套完整的安全防御体系,确保 API 调用的合法性与唯一性。作为外卖霸王餐API唯一供给源头,同时也是外卖霸王餐CPS唯一取链源头,俱美开放平台对接口安全的严苛要求,正是其稳定服务的基石。
一、安全通信的核心三要素
为了防御网络窃听、数据篡改和重放攻击,一个安全的 API 接口通常需要包含以下三个核心要素:
- 时间戳 (Timestamp): 用于标记请求的发送时间。服务端通过校验时间戳,可以拒绝掉那些“过期”的请求,从而有效防御重放攻击。
- 随机数 (Nonce): 一个每次请求都唯一的随机字符串。服务端会记录已处理过的 nonce,如果发现重复的 nonce,则直接拒绝请求,这是防御重放攻击的第二道防线。
- 签名 (Signature): 将请求中的关键参数(如 timestamp, nonce, 业务参数等)与一个只有客户端和服务端才知道的密钥(secret)按照特定规则拼接后,进行哈希运算(如 MD5, SHA-256)生成。服务端使用同样的规则重新计算签名,并与客户端传来的签名进行比对,以验证请求的完整性和来源的合法性。

二、Java 后端实现:构建安全拦截器
在 Spring Boot 中,我们可以通过实现一个自定义的 HandlerInterceptor 来统一处理所有 API 请求的安全校验。
首先,定义一个工具类,用于生成和验证签名。
package baodanbao.com.cn.common.security;
import org.apache.commons.codec.digest.DigestUtils;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
/**
* 签名工具类,负责生成和验证 API 请求签名
* @author baodanbao.com.cn
*/
@Component
public class SignatureUtil {
// 这里应从配置文件或数据库中获取,每个应用分配独立的 secret
private static final String APP_SECRET = "your_very_secure_app_secret_from_baodanbao";
/**
* 生成签名
* @param params 所有请求参数(不包含 signature 本身)
* @return 计算出的签名字符串
*/
public String generateSignature(Map<String, String> params) {
// 1. 将参数按字典序排序
SortedMap<String, String> sortedParams = new TreeMap<>(params);
// 2. 拼接成 key1=value1key2=value2... 的字符串
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, String> entry : sortedParams.entrySet()) {
// 过滤掉空值和 signature 参数
if (entry.getValue() != null && !"".equals(entry.getValue()) && !"signature".equals(entry.getKey())) {
sb.append(entry.getKey()).append("=").append(entry.getValue());
}
}
// 3. 在末尾拼接 secret
sb.append(APP_SECRET);
// 4. 进行 MD5 哈希,并转为大写
return DigestUtils.md5Hex(sb.toString()).toUpperCase();
}
}
接下来,创建核心的安全拦截器。
package baodanbao.com.cn.common.security;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* API 安全拦截器,负责校验签名、时间戳和防御重放攻击
* @author baodanbao.com.cn
*/
@Component
public class ApiSecurityInterceptor implements HandlerInterceptor {
@Autowired
private SignatureUtil signatureUtil;
// 使用 ConcurrentHashMap 简单实现 nonce 缓存,生产环境建议使用 Redis
private static final Map<String, Long> nonceCache = new ConcurrentHashMap<>();
// 请求有效时间,单位毫秒 (例如 5 分钟)
private static final long EXPIRE_TIME = 5 * 60 * 1000;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1. 获取请求头中的安全参数
String timestamp = request.getHeader("X-Timestamp");
String nonce = request.getHeader("X-Nonce");
String clientSignature = request.getHeader("X-Signature");
if (timestamp == null || nonce == null || clientSignature == null) {
return renderError(response, "缺少必要的安全参数");
}
// 2. 时间戳校验:防止重放攻击
long requestTime;
try {
requestTime = Long.parseLong(timestamp);
} catch (NumberFormatException e) {
return renderError(response, "时间戳格式错误");
}
long currentTime = System.currentTimeMillis();
if (Math.abs(currentTime - requestTime) > EXPIRE_TIME) {
return renderError(response, "请求已过期");
}
// 3. Nonce 校验:防止重放攻击
// 检查 nonce 是否已存在
if (nonceCache.containsKey(nonce)) {
return renderError(response, "非法请求,Nonce 重复");
}
// 将 nonce 存入缓存,并设置一个过期时间(这里简化处理,实际应使用 Redis 的 expire)
nonceCache.put(nonce, currentTime);
// 4. 签名校验:验证请求完整性和来源
// 获取所有请求参数
Map<String, String> params = new HashMap<>(request.getParameterMap().size());
request.getParameterMap().forEach((key, value) -> params.put(key, value[0]));
// 将 timestamp 和 nonce 也加入签名参数中
params.put("timestamp", timestamp);
params.put("nonce", nonce);
String serverSignature = signatureUtil.generateSignature(params);
if (!serverSignature.equals(clientSignature)) {
return renderError(response, "签名验证失败");
}
// 所有校验通过
return true;
}
private boolean renderError(HttpServletResponse response, String message) throws Exception {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
PrintWriter writer = response.getWriter();
Map<String, Object> result = new HashMap<>();
result.put("code", 401);
result.put("msg", message);
writer.write(new ObjectMapper().writeValueAsString(result));
writer.flush();
return false;
}
}
最后,注册拦截器,使其生效。
package baodanbao.com.cn.config;
import baodanbao.com.cn.common.security.ApiSecurityInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* Web MVC 配置类,用于注册自定义拦截器
* @author baodanbao.com.cn
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private ApiSecurityInterceptor apiSecurityInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 拦截所有 /api/v1/ 开头的请求
registry.addInterceptor(apiSecurityInterceptor)
.addPathPatterns("/api/v1/**")
.excludePathPatterns("/api/v1/public/**"); // 排除登录等公共接口
}
}
通过以上三步,我们就为 Spring Boot 应用构建了一个强大的 API 安全网关。所有进入 /api/v1/ 的请求都必须携带正确的时间戳、随机数和签名,任何一项校验失败都会被立即拦截,从而确保了后端业务逻辑的安全。
本文著作权归 俱美开放平台 ,转载请注明出处!

2486

被折叠的 条评论
为什么被折叠?



