当前位置:网站首页 >  攻略

API接口安全签名验证与防篡改实操指南

时间:2026年05月29日 04:09:33 来源:易频IT社区

签名算法核心设计规范

在进行接口安全资质审核时,签名验证是防止请求被篡改、确保调用方身份合法的核心手段。本指南采用 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 配置注册

API接口安全签名验证与防篡改实操指南

编写完拦截器后,必须通过配置类将其注册到 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_idapp_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); ```

相关推荐

最新

热门

推荐

精选

标签

易频IT社区是综合性互联网IT技术门户网站,专注分享网络技术、服务器运维、网络安全、编程开发、系统架构、云计算、大数据等行业干货,实时更新IT行业资讯、零基础教程、实战案例,为IT从业者、技术爱好者提供专业的学习交流平台。

Copyright © 2021-2026 易频IT社区. All Rights Reserved. 备案号:闽ICP备2023013482号 网站地图