签名算法核心设计规范
在进行接口安全资质审核时,签名验证是防止请求被篡改、确保调用方身份合法的核心手段。本指南采用 AppID + Timestamp + Sign 的通用鉴权模式。所有参与签名的参数必须按照字典序排序,并拼接成特定格式的字符串,最后通过 MD5 或 SHA256 进行加密。
签名生成规则如下:
- 剔除空值:移除所有参数值为 null 或空字符串的键值对。
- 字典序排序:将剩余参数按照 Key 的字母升序排列。
- 字符串拼接:将排序后的参数拼接为
key1value1key2value2 的形式。
- 追加密钥:在拼接字符串末尾追加双方约定的
AppSecret。
- 加密计算:对最终字符串进行 MD5 运算并转为大写,即为签名值。
服务端签名校验工具类
首先创建一个独立的工具类 SignatureUtil.java,用于处理签名计算与校验逻辑。此类不依赖任何第三方框架,可直接复制使用。
完整代码如下:
```java
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Map;
import java.util.TreeMap;
public class SignatureUtil {
/
计算签名
@param params 请求参数Map
@param appSecret 分配给客户端的密钥
@return 签名字符串
/
public static String generateSign(Map
params, String appSecret) {
// 1. 使用TreeMap自动进行字典序排序
Map sortedParams = new TreeMap<>(params);
StringBuilder sb = new StringBuilder();
// 2. 拼接参数
for (Map.Entry entry : sortedParams.entrySet()) {
String value = entry.getValue();
// 过滤空值
if (value != null && !value.isEmpty()) {
sb.append(entry.getKey()).append(value);
}
}
// 3. 追加密钥
sb.append(appSecret);
// 4. MD5加密并转大写
return md5(sb.toString()).toUpperCase();
}
/
验证签名
@param params 客户端传来的所有参数(包含sign)
@param appSecret 密钥
@return true-验证通过,false-验证失败
/
public static boolean verifySign(Map params, String appSecret) {
String clientSign = params.get("sign");
if (clientSign == null || clientSign.isEmpty()) {
return false;
}
// 移除sign参数本身,不参与签名计算
Map signParams = new TreeMap<>(params);
signParams.remove("sign");
String serverSign = generateSign(signParams, appSecret);
return serverSign.equals(clientSign);
}
private static String md5(String str) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] bytes = md.digest(str.getBytes(StandardCharsets.UTF_8));
StringBuilder hexString = new StringBuilder();
for (byte b : bytes) {
String hex = Integer.toHexString(b & 0xFF);
if (hex.length() == 1) {
hexString.append('0');
}
hexString.append(hex);
}
return hexString.toString();
} catch (Exception e) {
throw new RuntimeException("MD5 calculation failed", e);
}
}
}
```
请求拦截器与防重放逻辑
为了自动拦截所有请求进行校验,我们需要实现 Spring Boot 的 HandlerInterceptor。同时,为了防止重放攻击,必须校验请求的时间戳。我们设定请求时间戳与服务器当前时间差不得超过 5 分钟(300000毫秒)。
创建 AuthInterceptor.java:
```java
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
@Component
public class AuthInterceptor implements HandlerInterceptor {
// 模拟数据库存储的AppSecret,实际项目中请替换为查询数据库或配置中心
private static final String APP_SECRET = "my_secret_key_123";
private static final long TIME_EXPIRE = 300000; // 5分钟有效期
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1. 获取所有请求参数
Map params = new HashMap<>();
Enumeration paramNames = request.getParameterNames();
while (paramNames.hasMoreElements()) {
String name = paramNames.nextElement();
params.put(name, request.getParameter(name));
}
// 2. 校验必填参数
if (!params.containsKey("appId") || !params.containsKey("timestamp") || !params.containsKey("sign")) {
returnError(response, "缺少必要鉴权参数");
return false;
}
// 3. 防重放攻击:校验时间戳
long timestamp = Long.parseLong(params.get("timestamp"));
long now = System.currentTimeMillis();
if (Math.abs(now - timestamp) > TIME_EXPIRE) {
returnError(response, "请求已过期,请检查服务器时间");
return false;
}
// 4. 验证签名
boolean isValid = SignatureUtil.verifySign(params, APP_SECRET);
if (!isValid) {
returnError(response, "签名验证失败");
return false;
}
return true;
}
private void returnError(HttpServletResponse response, String msg) throws IOException {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(401);
PrintWriter writer = response.getWriter();
writer.write("{\"code\": 401, \"msg\": \"" + msg + "\"}");
writer.flush();
writer.close();
}
}
```
Spring Boot 配置注册

编写完拦截器后,必须通过配置类将其注册到 Spring MVC 的执行链中,指定拦截路径。通常拦截所有 API 接口,但排除登录、注册等公开接口。
创建 WebConfig.java:
```java
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;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private AuthInterceptor authInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authInterceptor)
.addPathPatterns("/api/") // 拦截 /api/ 下的所有路径
.excludePathPatterns("/api/public/login", "/api/public/register"); // 排除公开接口
}
}
```
客户端签名生成示例
服务端配置完成后,客户端(调用方)需要按照相同的算法生成签名。以下提供一个标准的 Java 客户端调用示例,模拟发起 HTTP GET 请求。
创建 ApiClient.java:
```java
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;
public class ApiClient {
private static final String APP_ID = "client_001";
private static final String APP_SECRET = "my_secret_key_123";
private static final String GATEWAY_URL = "http://localhost:8080/api/user/info";
public static void main(String[] args) throws Exception {
// 1. 准备业务参数
Map params = new HashMap<>();
params.put("userId", "1001");
params.put("type", "detail");
// 2. 添加公共鉴权参数
params.put("appId", APP_ID);
params.put("timestamp", String.valueOf(System.currentTimeMillis()));
// 3. 计算签名
String sign = SignatureUtil.generateSign(params, APP_SECRET);
params.put("sign", sign);
// 4. 构建带参数的URL
String fullUrl = buildUrlWithParams(GATEWAY_URL, params);
System.out.println("Request URL: " + fullUrl);
// 5. 发送请求
sendGetRequest(fullUrl);
}
private static String buildUrlWithParams(String baseUrl, Map params) throws Exception {
StringBuilder urlBuilder = new StringBuilder(baseUrl);
if (params != null && !params.isEmpty()) {
urlBuilder.append("?");
// 使用TreeMap保证参数在URL中的顺序也是字典序(虽然HTTP协议不要求,但利于调试)
Map sortedParams = new TreeMap<>(params);
boolean first = true;
for (Map.Entry entry : sortedParams.entrySet()) {
if (!first) {
urlBuilder.append("&");
}
urlBuilder.append(entry.getKey())
.append("=")
.append(URLEncoder.encode(entry.getValue(), "UTF-8"));
first = false;
}
}
return urlBuilder.toString();
}
private static void sendGetRequest(String urlString) throws Exception {
URL url = new URL(urlString);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
int responseCode = conn.getResponseCode();
System.out.println("Response Code: " + responseCode);
if (responseCode == 200) {
BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String inputLine;
StringBuilder response = new StringBuilder();
while ((inputLine = in.readLine()) != null) {
response.append(inputLine);
}
in.close();
System.out.println("Response Body: " + response.toString());
} else {
// 读取错误流
BufferedReader in = new BufferedReader(new InputStreamReader(conn.getErrorStream()));
String inputLine;
StringBuilder response = new StringBuilder();
while ((inputLine = in.readLine()) != null) {
response.append(inputLine);
}
in.close();
System.out.println("Error Body: " + response.toString());
}
}
}
```
Postman 自动化签名脚本
在前后端联调阶段,手动计算签名非常繁琐。Postman 提供了 Pre-request Script 功能,可以在请求发送前自动执行 JS 脚本生成签名。请将以下脚本粘贴到 Postman 请求的 Pre-request Script 选项卡中。
注意:Postman 的环境变量中需预先设置 app_id 和 app_secret。
```javascript
// 获取环境变量中的密钥
var appId = pm.environment.get("app_id");
var appSecret = pm.environment.get("app_secret");
// 获取当前时间戳
var timestamp = new Date().getTime();
pm.request.headers.add({key: "timestamp", value: timestamp});
// 获取所有请求参数(包括Params和Body)
// 注意:此处仅演示处理Query Params,如果是Form-data或x-www-form-urlencoded需额外处理
var params = {};
pm.request.url.query.each(function(param) {
// 只添加非空参数
if (param.value && param.value !== "") {
params[param.key] = param.value;
}
});
// 1. 字典序排序
var sortedKeys = Object.keys(params).sort();
var signStr = "";
// 2. 拼接字符串
for (var i = 0; i < sortedKeys.length; i++) {
var key = sortedKeys[i];
signStr += key + params[key];
}
// 3. 追加密钥
signStr += appSecret;
// 4. MD5加密(Postman内置CryptoJS库)
var sign = CryptoJS.MD5(signStr).toString().toUpperCase();
// 5. 将签名添加到请求头或参数中
pm.request.headers.add({key: "sign", value: sign});
pm.request.headers.add({key: "appId", value: appId});
console.log("Generated Sign String: " + signStr);
console.log("Final Sign: " + sign);
```