本文目录导读:

在 Java 开发中,统一参数签名流程(通常用于API接口鉴权、防止参数篡改、确保请求唯一性)的核心在于:制定一套标准的、可逆的、包含时序信息的签名算法规则,并通过统一过滤器或拦截器进行校验。
以下是实现一个标准、统一的 Java 参数签名流程的详细步骤和代码范例。
统一的签名流程标准
通常包含以下 5 个核心步骤:
- 组合参数:将所有请求参数(包括
AppID,Timestamp,Nonce随机数)按字典序排序并拼接成字符串。 - 拼接密钥:在拼接字符串后,加上双方约定的
SecretKey(API密钥)。 - 摘要计算:使用
HMAC-SHA256或MD5进行哈希计算。 - 生成签名字符串:将哈希结果转换为小写十六进制字符串,作为
sign。 - 校验与防御:服务端拿到参数后,用相同的算法计算签名,比对是否一致,并检查
Timestamp(时间戳防重放,默认允许5-15分钟误差)和Nonce(随机数防重放,需存入Redis缓存去重)。
核心代码模块设计
签名工具类(核心)
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Arrays;
import java.util.Map;
import java.util.TreeMap;
import java.util.UUID;
public class SignUtil {
/**
* 生成 API 签名(HMAC-SHA256 算法)
*
* @param params 请求参数(不包括sign本身)
* @param secret API私钥
* @return 小写的签名字符串
*/
public static String generateSign(Map<String, String> params, String secret) {
// 1. 参数排序(使用TreeMap保证字典序)
Map<String, String> sortedParams = new TreeMap<>(params);
// 2. 拼接参数:key=value&key=value (排除空值)
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, String> entry : sortedParams.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
if (value != null && !value.isEmpty()) {
sb.append(key).append("=").append(value).append("&");
}
}
// 删除最后一个&
if (sb.length() > 0) {
sb.deleteCharAt(sb.length() - 1);
}
// 3. 拼接密钥
sb.append(secret);
String stringToSign = sb.toString();
// 4. HMAC-SHA256 加密
try {
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKeySpec = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
mac.init(secretKeySpec);
byte[] digest = mac.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8));
return bytesToHex(digest);
} catch (Exception e) {
throw new RuntimeException("签名生成失败", e);
}
}
/**
* 生成 MD5 签名(另一种常见方式,速度更快)
*/
public static String generateMD5Sign(Map<String, String> params, String secret) {
// 逻辑同上,直到 stringToSign
// ...
// 最后使用 MessageDigest.getInstance("MD5")
return "";
}
/**
* 生成 Nonce 随机数(UUID去横线)
*/
public static String generateNonce() {
return UUID.randomUUID().toString().replaceAll("-", "");
}
/**
* 字节数组转十六进制字符串
*/
public static String bytesToHex(byte[] bytes) {
StringBuilder hexString = new StringBuilder();
for (byte b : bytes) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) hexString.append('0');
hexString.append(hex);
}
return hexString.toString();
}
}
客户端发起请求时的签名示例
public class ApiClient {
public void sendRequest() {
String appId = "your-app-id";
String secret = "your-api-secret";
String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
String nonce = SignUtil.generateNonce();
// 业务参数
Map<String, String> params = new HashMap<>();
params.put("appId", appId);
params.put("timestamp", timestamp);
params.put("nonce", nonce);
params.put("bizParam1", "value1");
params.put("bizParam2", "value2");
// 生成签名(注意:sign参数不能参与签名计算)
String sign = SignUtil.generateSign(params, secret);
params.put("sign", sign);
// 发送HTTP请求 (将params作为请求体或查询参数)
}
}
服务端统一校验过滤器(统一入口)
@Component
public class SignAuthFilter extends OncePerRequestFilter {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
// 1. 从请求中获取签名参数
String sign = request.getHeader("sign");
String appId = request.getHeader("appId");
String nonce = request.getHeader("nonce");
String timestamp = request.getHeader("timestamp");
// 2. 参数完整性校验
if (StringUtils.isEmpty(sign) || StringUtils.isEmpty(appId)
|| StringUtils.isEmpty(nonce) || StringUtils.isEmpty(timestamp)) {
throw new AuthException("签名参数缺失");
}
// 3. 时间戳防重放(允许±5分钟)
long now = System.currentTimeMillis() / 1000;
long requestTime = Long.parseLong(timestamp);
if (now - requestTime > 300 || requestTime - now > 300) {
throw new AuthException("请求已过期");
}
// 4. Nonce防重放(使用Redis Set进行去重,TTL设置为5分钟)
String redisKey = "sign:nonce:" + nonce;
if (Boolean.TRUE.equals(redisTemplate.opsForValue().setIfAbsent(redisKey, "1", 5, TimeUnit.MINUTES))) {
// Nonce不存在,允许继续
} else {
throw new AuthException("重复请求");
}
// 5. 查询该appId对应的SecretKey(从配置中心或数据库获取)
String secret = getSecretByAppId(appId);
// 6. 重新生成签名(不包含sign参数)
Map<String, String> params = new HashMap<>(getParamsFromRequest(request));
params.remove("sign"); // 移除sign参数
String expectedSign = SignUtil.generateSign(params, secret);
// 7. 比对签名
if (!expectedSign.equals(sign)) {
throw new AuthException("签名校验失败");
}
// 校验通过,继续执行
filterChain.doFilter(request, response);
}
/**
* 从请求中提取所有参数(含Header和Query/Body)
*/
private Map<String, String> getParamsFromRequest(HttpServletRequest request) {
Map<String, String> params = new HashMap<>();
// 提取Header参数
params.put("appId", request.getHeader("appId"));
params.put("timestamp", request.getHeader("timestamp"));
params.put("nonce", request.getHeader("nonce"));
// 提取Query参数
for (Map.Entry<String, String[]> entry : request.getParameterMap().entrySet()) {
params.put(entry.getKey(), entry.getValue()[0]);
}
return params;
}
}
统一性关键点说明
| 统一点 | 做法 | 目的 |
|---|---|---|
| 参数排序 | 固定使用 TreeMap 字典序排序 |
保证客户端和服务端拼接顺序完全一致 |
| 密钥管理 | 服务端通过 appId 查找对应的 secret |
支持多客户端不同密钥 |
| 时间戳格式 | 固定 Unix 时间戳(秒级),如 1700000000 |
避免时区问题 |
| 空值处理 | 移除空值和空字符串 | 避免拼接歧义 |
| 统一入口 | 使用 Spring Filter 或 Interceptor |
集中处理,避免重复代码 |
进阶优化建议
-
包含Body签名:如果请求是
POST JSON,需要将 Body 也纳入签名,方法是在参数中增加bodyRaw,值为 Body 的原始 JSON 字符串(建议排序后再签名)。 -
路径签名:如果需要对 URL 路径进行防篡改,可以将
requestURI也作为签名参数urlPath参与计算。 -
异常处理:在 Filter 中不要抛出异常直接返回,建议统一返回 JSON 格式的错误信息,
{"code": 401, "message": "签名校验失败"} -
注意字符集:所有加密和拼接操作都使用
StandardCharsets.UTF_8。 -
日志记录:在 Filter 中打印客户端和服务端计算的签名串(建议脱敏SecretKey),方便排查问题。
统一签名流程 = 参数排序(字典序) + 拼接密钥 + HMAC-SHA256 + 服务端Filter校验
通过以上设计,你的 Java 项目可以实现一个安全、统一、易于维护的参数签名鉴权机制。