接口没签名 → 参数随便改、请求随便重放、金额随便篡改。5 分钟看完即用,每个坑附错误写法 + 正确姿势。
// ❌ 只验 sign,不验 timestamp → 拿旧请求重放,照样过
String sign = request.getHeader("X-Sign");
String params = body + "&key=" + SECRET; // 没有时间戳
if (!DigestUtils.md5Hex(params).equals(sign)) {
throw new BizException("签名错误");
}// ✅ 签名串必须含 timestamp + nonce,过期拒收
String ts = request.getHeader("X-Timestamp");
String nonce = request.getHeader("X-Nonce");
if (Math.abs(System.currentTimeMillis() - Long.parseLong(ts))
> 5 * 60 * 1000) {
throw new BizException("请求已过期");
}
String sign = DigestUtils.md5Hex(
body + "&ts=" + ts + "&nonce=" + nonce + "&key=" + SECRET);铁律:签名必须绑定时间窗口(建议 5 分钟),不然抓包重放百发百中。
// ❌ 纯 MD5(参数+key) → 太弱,加个盐就破了
String sign = DigestUtils.md5Hex(params + SECRET);// ✅ HMAC-SHA256 + 随机 nonce,破解成本指数级
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(SECRET.getBytes(), "HmacSHA256"));
String sign = Base64.getEncoder()
.encodeToString(mac.doFinal(params.getBytes()));生产至少用 HMAC-SHA256,别再裸 MD5。
// ❌ Filter 里 request.getReader() 读完 → 流已消费,Controller 里 request body 为空
String body = IOUtils.toString(request.getReader());// ✅ 包装成 ContentCachingRequestWrapper,流可重复读
public class SignFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain chain) {
ContentCachingRequestWrapper wrapper =
new ContentCachingRequestWrapper(request);
chain.doFilter(wrapper, response);
byte[] body = wrapper.getContentAsByteArray(); // 随手拿
verifySign(new String(body, StandardCharsets.UTF_8));
}
}// ❌ 代码里写死 → 一次泄露,所有环境沦陷
private static final String SECRET = "prod-secret-key-2024";// ✅ yml 配置 + 配置中心加密,不同环境不同 key
@Value("${api.sign.secret}")
private String secret;
// application.yml
api:
sign:
secret: ${API_SIGN_SECRET} // 环境变量注入,不进 Git// ❌ GET 按 query string 拼,POST 按 body 拼 → 两套逻辑,必出 bug
if ("GET".equals(method)) {
signStr = sortAndJoin(request.getParameterMap());
} else {
signStr = IOUtils.toString(request.getReader());
}// ✅ 统一规则:字典序拼接所有参数 + timestamp + nonce + key
TreeMap sorted = new TreeMap<>();
sorted.put("timestamp", ts);
sorted.put("nonce", nonce);
sorted.putAll(extractParams(request)); // GET/POST 统一提取
String signStr = sorted.entrySet().stream()
.map(e -> e.getKey() + "=" + e.getValue())
.collect(Collectors.joining("&")) + "&key=" + SECRET; # | 坑点 | 快速修复 |
1 | 没 timestamp/nonce | 签名串绑定时间戳,5 分钟过期 |
2 | 只用 MD5 | 换 HMAC-SHA256 |
3 | Filter 消费 Body | 用 |
4 | key 硬编码 | 环境变量/配置中心,不进 Git |
5 | GET/POST 规则不一致 | 统一按字典序拼接参数 |
一句话总结:接口签名就四条铁律——加时间戳防重放、HMAC 替代 MD5、Body 用 Wrapper 重复读、key 不进代码。把这 5 个坑都封上,你的 API 才算穿上铠甲。
更新时间:2026-06-03
本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828
© CopyRight All Rights Reserved.
Powered By 61893.com 闽ICP备11008920号
闽公网安备35020302035593号