HTTP请求加盐签名防篡改、防重放完整通用方案(前后端全套代码)

发布时间:2026/7/1 18:29:36

HTTP请求加盐签名防篡改、防重放完整通用方案(前后端全套代码) 一、前言日常开发中很多接口仅依赖HTTPS Token做安全校验但存在明显漏洞HTTPS 只能防链路窃听无法防止请求参数被篡改、恶意重放请求单纯 Token 仅校验身份不校验请求参数完整性抓包工具可篡改 GET/POST 参数、伪造重复请求造成数据错乱、业务漏洞本文提供一套企业级通用 HTTP 加盐签名方案基于HMAC-SHA256算法结合时间戳、随机串、参数排序完美实现防篡改、防重放、防伪造适配GET/POST、Form/JSON所有请求方式附前端JS、后端Java完整可运行代码开箱即用。二、方案核心设计生产级标准2.1 核心校验四要素所有签名必须包含四个核心字段缺一不可兼顾安全性和唯一性salt密钥盐前后端约定固定密钥私密不对外泄露用于加密校验timestamp时间戳毫秒级时间控制请求有效期防重放攻击nonce随机串单次请求唯一随机值避免相同参数生成相同签名业务参数所有接口入参参与签名保证参数不可篡改2.2 防御能力说明防篡改任意参数修改签名即刻失效服务端验签不通过直接拦截防重放时间戳过期默认120s、随机串重复直接拒绝请求防伪造无正确salt密钥无法生成合法签名2.3 整体执行流程客户端流程获取当前毫秒时间戳、生成唯一随机串nonce收集全部业务请求参数按Key ASCII升序排序按固定规则拼接参数、时间戳、随机串使用HMAC-SHA256 约定salt生成签名sign将 sign、timestamp、nonce 放入请求Header发起请求服务端流程从Header获取签名、时间戳、随机串校验时间戳是否过期120s窗口期校验nonce是否重复Redis去重防重放获取请求全部参数按照客户端相同规则重新拼接、计算签名对比客户端签名与服务端签名一致放行不一致拦截三、签名拼接强制规范前后端必须严格对齐90%的签名不一致问题都是拼接规则不统一导致本文统一生产级规范参数全覆盖GET查询参数 POST JSON/Form参数全部参与签名参数排序所有参数Key按ASCII码升序排列禁止乱序拼接空值不丢弃null、空字符串参数正常参与拼接不忽略无多余分隔符keyvalue直接拼接不添加、等多余符号统一编码全部UTF-8编码避免中文乱码导致签名失效salt不参与前端拼接salt作为加密密钥仅用于HMAC加密不拼接到明文串四、前端JS完整实现Axios全局拦截自动签名基于Axios请求拦截器实现所有接口自动加盐签名无需每个接口单独处理适配Vue/React/原生项目。// 引入依赖crypto-js // npm install crypto-js --save import CryptoJS from crypto-js import axios from axios // 前后端约定密钥生产环境放环境变量禁止明文写死 const SIGN_SALT Api_Sign_Secret_2026#666 // 请求过期时间120秒 const EXPIRE_TIME 120 * 1000 // 生成随机nonce32位随机串 function generateNonce() { const chars ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 let nonce for (let i 0; i 32; i) { nonce chars.charAt(Math.floor(Math.random() * chars.length)) } return nonce } // 参数排序拼接 function sortParams(params) { if (!params || Object.keys(params).length 0) return // 按key ASCII升序排序 return Object.keys(params).sort().reduce((str, key) { return str key params[key] }, ) } // 生成HMAC-SHA256签名 function generateSign(params, timestamp, nonce) { const paramStr sortParams(params) // 固定拼接规则排序参数 时间戳 随机串 const sourceStr paramStr timestamp nonce // HMAC-SHA256加密转小写十六进制 return CryptoJS.HmacSHA256(sourceStr, SIGN_SALT).toString(CryptoJS.enc.Hex) } // Axios全局请求拦截 axios.interceptors.request.use(config { const timestamp Date.now().toString() const nonce generateNonce() // 合并GET/POST参数 let allParams {} if (config.params) allParams { ...allParams, ...config.params } if (config.data typeof config.data object) allParams { ...allParams, ...config.data } // 生成签名 const sign generateSign(allParams, timestamp, nonce) // 挂载请求头 config.headers[X-Timestamp] timestamp config.headers[X-Nonce] nonce config.headers[X-Sign] sign return config }) export default axios五、后端Java完整实现SpringBoot AOP全局验签采用AOP切面全局拦截无侵入业务代码统一校验所有接口支持Redis防重放适配SpringBoot所有版本。5.1 签名工具类核心加密、参数解析import cn.hutool.core.util.StrUtil; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets; import java.util.*; import java.util.stream.Collectors; /** * HTTP接口签名验签工具类 * 防篡改、防重放、防伪造 */ public class SignUtil { // 与前端统一的密钥 private static final String SIGN_SALT Api_Sign_Secret_2026#666; // 有效时长120秒 public static final long VALID_TIME 120 * 1000; // HMAC-SHA256算法 private static final String HMAC_SHA256 HmacSHA256; /** * 生成签名与前端逻辑完全对齐 */ public static String generateSign(MapString, Object params, String timestamp, String nonce) { // 参数按key ASCII升序排序拼接 String paramStr params.entrySet().stream() .sorted(Map.Entry.comparingByKey()) .map(entry - entry.getKey() StrUtil.toString(entry.getValue())) .collect(Collectors.joining()); // 拼接源串 String source paramStr timestamp nonce; // HMAC-SHA256加密 return hmacSha256(source, SIGN_SALT); } /** * HMAC-SHA256加密 */ private static String hmacSha256(String data, String key) { try { Mac mac Mac.getInstance(HMAC_SHA256); SecretKeySpec keySpec new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), HMAC_SHA256); mac.init(keySpec); byte[] bytes mac.doFinal(data.getBytes(StandardCharsets.UTF_8)); // 转十六进制小写 StringBuilder sb new StringBuilder(); for (byte b : bytes) { String hex Integer.toHexString(b 0xFF); if (hex.length() 1) { sb.append(0); } sb.append(hex); } return sb.toString(); } catch (Exception e) { throw new RuntimeException(签名生成失败, e); } } }5.2 自定义验签注解灵活控制接口import java.lang.annotation.*; /** * 接口验签注解 * 加此注解的接口需进行签名校验 */ Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) Documented public interface ApiSignCheck { }5.3 AOP切面全局验签逻辑import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import java.util.Enumeration; import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; Aspect Component public class ApiSignAspect { Resource private RedisTemplateString, String redisTemplate; // Redis随机串前缀 private static final String NONCE_PREFIX api:sign:nonce:; Around(annotation(apiSignCheck)) public Object around(ProceedingJoinPoint point, ApiSignCheck apiSignCheck) throws Throwable { HttpServletRequest request getRequest(); if (request null) { throw new RuntimeException(请求异常); } // 1. 获取请求头签名参数 String clientSign request.getHeader(X-Sign); String timestamp request.getHeader(X-Timestamp); String nonce request.getHeader(X-Nonce); // 非空校验 if (clientSign null || timestamp null || nonce null) { throw new RuntimeException(签名参数缺失请求非法); } // 2. 校验时间戳是否过期 long now System.currentTimeMillis(); if (now - Long.parseLong(timestamp) SignUtil.VALID_TIME) { throw new RuntimeException(请求已过期); } // 3. 校验nonce是否重复防重放 String nonceKey NONCE_PREFIX nonce; if (Boolean.TRUE.equals(redisTemplate.hasKey(nonceKey))) { throw new RuntimeException(禁止重复请求); } // 存入Redis120秒过期 redisTemplate.opsForValue().set(nonceKey, 1, SignUtil.VALID_TIME, TimeUnit.MILLISECONDS); // 4. 获取所有请求参数 MapString, Object allParams getAllRequestParams(request); // 5. 服务端生成签名对比 String serverSign SignUtil.generateSign(allParams, timestamp, nonce); if (!serverSign.equalsIgnoreCase(clientSign)) { throw new RuntimeException(参数篡改请求非法); } // 验签通过执行接口逻辑 return point.proceed(); } /** * 获取HttpServletRequest */ private HttpServletRequest getRequest() { RequestAttributes attributes RequestContextHolder.getRequestAttributes(); return ((ServletRequestAttributes) attributes).getRequest(); } /** * 获取全部请求参数适配GET/POST */ private MapString, Object getAllRequestParams(HttpServletRequest request) { MapString, Object paramMap new HashMap(); EnumerationString parameterNames request.getParameterNames(); while (parameterNames.hasMoreElements()) { String key parameterNames.nextElement(); paramMap.put(key, request.getParameter(key)); } return paramMap; } }5.4 接口使用方式只需在需要防护的接口上添加ApiSignCheck注解即可自动完成验签零侵入业务RestController RequestMapping(/api) public class TestController { // 开启签名校验 ApiSignCheck PostMapping(/test) public String test(RequestBody MapString,Object params){ return 请求合法业务执行成功; } }六、核心踩坑总结99%开发者会错的点6.1 签名不一致常见原因参数未按ASCII升序排序前后端拼接顺序不一致时间戳单位不统一前端毫秒、后端秒空参数、null值被前端舍弃、后端保留JSON序列化自带空格、换行导致源串不一致编码格式不统一GBK/UTF-86.2 安全优化要点禁止使用MD5优先HMAC-SHA256安全性远高于普通哈希salt密钥长度≥16位大小写数字特殊符号禁止硬编码明文暴露请求有效期严格控制在60-120秒缩短重放窗口期必须做nonce Redis去重仅靠时间戳无法彻底防重放线上必须启用HTTPS签名HTTPS双层防护七、方案优势总结通用性强适配所有HTTP接口、GET/POST、JSON/Form请求零侵入业务AOP切面全局拦截无需修改业务代码安全性高同时解决篡改、重放、伪造三大接口安全问题轻量高效加密速度快不影响接口响应性能易于维护统一规则、统一工具类后期迭代无需改动八、结语HTTPS只能解决传输安全请求加盐签名才是接口数据完整性的最终保障。本方案是互联网企业通用的生产级接口安全方案适配后台管理、APP、小程序、对外开放API等所有场景可直接复制部署到项目中快速提升接口安全等级。

相关新闻