本文目录导读:

我来为你提供一个完整的Spring Boot接口签名实现案例。
项目结构
src/main/java/com/example/signature/
├── config/
│ └── SignConfig.java
├── interceptor/
│ └── SignInterceptor.java
├── annotation/
│ └── SignVerify.java
├── controller/
│ └── UserController.java
└── utils/
└── SignUtil.java
完整实现代码
1 签名工具类
package com.example.signature.utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;
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.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;
public class SignUtil {
private static final Logger logger = LoggerFactory.getLogger(SignUtil.class);
/**
* 生成签名
* @param params 请求参数(不包含sign)
* @param secret 密钥
* @return 签名
*/
public static String generateSign(Map<String, String> params, String secret) {
// 移除sign和空值参数
Map<String, String> sortedParams = new TreeMap<>();
if (params != null) {
for (Map.Entry<String, String> entry : params.entrySet()) {
if (!"sign".equals(entry.getKey()) && StringUtils.hasText(entry.getValue())) {
sortedParams.put(entry.getKey(), entry.getValue());
}
}
}
// 拼接参数
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, String> entry : sortedParams.entrySet()) {
sb.append(entry.getKey()).append("=").append(entry.getValue()).append("&");
}
// 拼接密钥
String stringToSign = sb.append("key=").append(secret).toString();
logger.debug("待签名字符串: {}", stringToSign);
return md5(stringToSign);
}
/**
* MD5加密
*/
public static String md5(String data) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] digest = md.digest(data.getBytes(StandardCharsets.UTF_8));
return bytesToHex(digest);
} catch (Exception e) {
logger.error("MD5加密失败", e);
return null;
}
}
/**
* HMAC-SHA256签名
*/
public static String hmacSha256(String data, String secret) {
try {
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKeySpec = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
mac.init(secretKeySpec);
byte[] digest = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(digest);
} catch (Exception e) {
logger.error("HMAC-SHA256签名失败", e);
return null;
}
}
/**
* 字节数组转十六进制
*/
private 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().toUpperCase();
}
/**
* 生成随机签名
*/
public static String generateRandomSign(String appId, String timestamp, String nonce, String secret) {
Map<String, String> params = new HashMap<>();
params.put("appId", appId);
params.put("timestamp", timestamp);
params.put("nonce", nonce);
return generateSign(params, secret);
}
/**
* 验证非空
*/
public static boolean checkNonNull(Map<String, String> params) {
if (params == null || params.isEmpty()) {
return false;
}
for (String value : params.values()) {
if (!StringUtils.hasText(value)) {
return false;
}
}
return true;
}
}
2 自定义注解
package com.example.signature.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 签名验证注解
*/
@Target({ElementType.METHOD, ElementType.TYPE}) // 方法或类上使用
@Retention(RetentionPolicy.RUNTIME)
public @interface SignVerify {
/**
* 是否需要验证
*/
boolean required() default true;
/**
* 超时时间(毫秒)
*/
long timeout() default 300000; // 默认5分钟
}
3 拦截器
package com.example.signature.interceptor;
import com.alibaba.fastjson.JSON;
import com.example.signature.annotation.SignVerify;
import com.example.signature.utils.SignUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
@Component
public class SignInterceptor implements HandlerInterceptor {
private static final Logger logger = LoggerFactory.getLogger(SignInterceptor.class);
// 密钥管理(实际项目中应该从配置中心或数据库中获取)
private static final Map<String, String> CLIENT_SECRETS = new HashMap<>();
static {
// 演示用的客户端密钥
CLIENT_SECRETS.put("test_app", "test_secret_key_123456");
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 如果不是控制器方法,直接放行
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
// 获取注解
SignVerify methodSign = handlerMethod.getMethodAnnotation(SignVerify.class);
SignVerify classSign = handlerMethod.getBeanType().getAnnotation(SignVerify.class);
// 如果没有注解,直接放行
if (methodSign == null && classSign == null) {
return true;
}
// 判断是否需要验证
SignVerify signVerify = methodSign != null ? methodSign : classSign;
if (!signVerify.required()) {
return true;
}
// 获取请求参数
Map<String, String> params = getParams(request);
// 验证参数
if (!verifyParams(params)) {
return responseError(response, "请求参数不完整");
}
// 验证时间戳
String timestamp = params.get("timestamp");
if (!verifyTimestamp(timestamp, signVerify.timeout())) {
return responseError(response, "请求已过期");
}
// 获取签名
String clientSign = params.get("sign");
String appId = params.get("appId");
// 获取客户端密钥
String secret = CLIENT_SECRETS.get(appId);
if (secret == null) {
return responseError(response, "无效的appId");
}
// 验证签名
String serverSign = SignUtil.generateSign(params, secret);
logger.debug("客户端签名: {}, 服务端签名: {}", clientSign, serverSign);
if (!clientSign.toUpperCase().equals(serverSign)) {
return responseError(response, "签名验证失败");
}
return true;
}
/**
* 获取请求参数
*/
private Map<String, String> getParams(HttpServletRequest request) throws IOException {
Map<String, String> params = new HashMap<>();
// 获取GET参数
Map<String, String[]> parameterMap = request.getParameterMap();
for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
params.put(entry.getKey(), entry.getValue()[0]);
}
// 获取POST body参数
if ("POST".equalsIgnoreCase(request.getMethod())) {
BufferedReader reader = request.getReader();
if (reader != null) {
try {
String line;
StringBuilder body = new StringBuilder();
while ((line = reader.readLine()) != null) {
body.append(line);
}
if (body.length() > 0) {
// 解析body参数(这里简化处理,只处理JSON格式)
String bodyStr = body.toString();
if (bodyStr.startsWith("{")) {
com.alibaba.fastjson.JSONObject jsonObject = JSON.parseObject(bodyStr);
for (String key : jsonObject.keySet()) {
if (!params.containsKey(key)) {
params.put(key, jsonObject.getString(key));
}
}
}
}
} finally {
reader.close();
}
}
}
return params;
}
/**
* 验证参数完整性
*/
private boolean verifyParams(Map<String, String> params) {
return params.containsKey("appId")
&& params.containsKey("timestamp")
&& params.containsKey("nonce")
&& params.containsKey("sign");
}
/**
* 验证时间戳
*/
private boolean verifyTimestamp(String timestamp, long timeout) {
try {
long clientTime = Long.parseLong(timestamp);
long currentTime = System.currentTimeMillis();
return Math.abs(currentTime - clientTime) <= timeout;
} catch (NumberFormatException e) {
return false;
}
}
/**
* 返回错误信息
*/
private boolean responseError(HttpServletResponse response, String message) throws IOException {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(401);
PrintWriter writer = response.getWriter();
writer.write("{\"code\":401, \"message\":\"" + message + "\"}");
writer.flush();
writer.close();
return false;
}
}
4 配置类
package com.example.signature.config;
import com.example.signature.interceptor.SignInterceptor;
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 SignConfig implements WebMvcConfigurer {
@Autowired
private SignInterceptor signInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(signInterceptor)
.addPathPatterns("/api/**") // 添加需要拦截的路径
.excludePathPatterns("/api/public/**"); // 排除不需要拦截的路径
}
}
5 控制器示例
package com.example.signature.controller;
import com.example.signature.annotation.SignVerify;
import com.example.signature.utils.SignUtil;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
@RestController
@RequestMapping("/api")
@SignVerify // 类级别注解,所有接口都需要验证
public class UserController {
/**
* 需要签名验证的接口
*/
@PostMapping("/user/info")
public Map<String, Object> getUserInfo(@RequestBody Map<String, String> params) {
Map<String, Object> result = new HashMap<>();
result.put("code", 200);
result.put("message", "success");
result.put("data", "用户信息:" + params.get("userId"));
return result;
}
/**
* 不需要签名验证的接口(覆盖类级注解)
*/
@GetMapping("/public/config")
@SignVerify(required = false)
public Map<String, Object> getPublicConfig() {
Map<String, Object> result = new HashMap<>();
result.put("code", 200);
result.put("message", "success");
result.put("data", "公共配置");
return result;
}
/**
* 获取测试签名(用于演示)
*/
@GetMapping("/getTestSign")
public Map<String, String> getTestSign(
@RequestParam String appId,
@RequestParam String timestamp,
@RequestParam String nonce) {
String secret = "test_secret_key_123456";
Map<String, String> testParams = new HashMap<>();
testParams.put("appId", appId);
testParams.put("timestamp", timestamp);
testParams.put("nonce", nonce);
String sign = SignUtil.generateSign(testParams, secret);
Map<String, String> result = new HashMap<>();
result.put("sign", sign);
result.put("appId", appId);
result.put("timestamp", timestamp);
result.put("nonce", nonce);
return result;
}
}
6 客户端调用示例
package com.example.signature.client;
import org.springframework.web.client.RestTemplate;
import org.springframework.http.*;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
public class SignClient {
// 客户端信息
private static final String APP_ID = "test_app";
private static final String SECRET = "test_secret_key_123456";
private static final String BASE_URL = "http://localhost:8080/api";
public static void main(String[] args) {
// 构造请求参数
Map<String, String> params = new HashMap<>();
params.put("appId", APP_ID);
params.put("timestamp", String.valueOf(System.currentTimeMillis()));
params.put("nonce", UUID.randomUUID().toString().replace("-", ""));
params.put("userId", "123456");
// 生成签名
String sign = com.example.signature.utils.SignUtil.generateSign(params, SECRET);
params.put("sign", sign);
// 发送请求
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<Map<String, String>> entity = new HttpEntity<>(params, headers);
String url = BASE_URL + "/user/info";
ResponseEntity<String> response = restTemplate.exchange(
url, HttpMethod.POST, entity, String.class);
System.out.println("请求URL: " + url);
System.out.println("请求参数: " + params);
System.out.println("响应结果: " + response.getBody());
}
}
POM依赖
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.25</version>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
签名流程说明
1 签名步骤
- 客户端准备参数:appId、timestamp、nonce、业务参数
- 生成待签名字符串:将参数按字母排序,用&拼接
- 生成签名:使用MD5或HMAC-SHA256算法
- 服务端验证:获取参数、验证时间戳、重新生成签名并对比
2 安全特性
- 时间戳验证:防止重放攻击
- 随机数nonce:保证每次请求唯一性
- 签名验证:防止参数篡改
- appId隔离:支持多客户端
3 扩展建议
- 支持更多签名算法(RSA、AES等)
- 添加nonce缓存,防止重复请求
- 集成Redis,实现令牌管理
- 支持接口限流
- 添加统一的错误码体系
这样,一个完整的Spring Boot接口签名案例就实现了,你可以根据实际需求进行调整和扩展。