本文目录导读:

Java中实现服务签名(Service Signing)通常用于确保API请求的完整性(未被篡改)和身份验证(请求来自合法客户端),最常见的模式是HMAC(哈希消息认证码) 或非对称加密(RSA/ECDSA)。
以下是一个基于HMAC-SHA256的案例实现,这是目前最主流、性能最好的方式(如阿里云、腾讯云、亚马逊AWS均采用类似方案)。
核心流程概述
-
客户端(调用方):
- 拥有
AppSecret(对称密钥)或PrivateKey(私钥)。 - 构造请求参数(URL、Body、Headers、Timestamp)。
- 按照特定规则排序并拼接成待签名字符串。
sign = HmacSHA256(待签名字符串, AppSecret)或RSA.sign(待签名字符串, PrivateKey)。- 将
sign与AppId(公钥标识)、Timestamp、Nonce(随机数)一起放入请求头。
- 拥有
-
服务端(验证方):
- 接收请求,从Header取出
AppId、Timestamp、Sign。 - 检查Timestamp是否在有效期内(防重放攻击,如5分钟)。
- 根据
AppId从数据库/缓存中查询对应的AppSecret。 - 使用完全相同的规则拼接待签名字符串。
serverSign = HmacSHA256(待签名字符串, AppSecret)。- 比对
serverSign与客户端传来的Sign是否相等。
- 接收请求,从Header取出
Java 代码实现
我们将实现一个通用的签名工具类 SignUtil。
添加 Maven 依赖
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.16.0</version>
</dependency>
服务端签名验证工具类 (SignUtil.java)
import org.apache.commons.codec.digest.HmacUtils;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.*;
public class SignUtil {
private static final String CHARSET_UTF8 = "UTF-8";
// 时间戳过期时间(毫秒),5分钟
private static final long EXPIRE_TIME = 5 * 60 * 1000L;
/**
* 服务端:验证签名
* @param method HTTP方法 (GET, POST, PUT 等)
* @param requestPath 请求路径 (如 /api/user/query,不包含?后的参数)
* @param headers 请求头 (包含 X-Nonce, X-Timestamp, X-AppId, X-Sign)
* @param body 请求体字符串 (如果是GET则为空)
* @param secretProvider 根据 appId 获取 secret 的函数接口
* @return true 验证通过,false 验证失败
*/
public static boolean verifySignature(String method, String requestPath,
Map<String, String> headers, String body,
SecretProvider secretProvider) {
try {
// 1. 从请求头获取签名参数
String appId = getHeader(headers, "X-AppId");
String nonce = getHeader(headers, "X-Nonce");
String timestampStr = getHeader(headers, "X-Timestamp");
String clientSign = getHeader(headers, "X-Sign");
if (appId == null || nonce == null || timestampStr == null || clientSign == null) {
return false;
}
// 2. 验证时间戳是否过期 (防重放)
long timestamp = Long.parseLong(timestampStr);
if (Math.abs(System.currentTimeMillis() - timestamp) > EXPIRE_TIME) {
return false; // 请求过期
}
// 3. 获取该 AppId 对应的 Secret (实际项目从DB/Redis获取)
String secret = secretProvider.getSecretByAppId(appId);
if (secret == null || secret.isEmpty()) {
return false; // AppId 不存在
}
// 4. 构建待签名字符串 (与客户端方式完全一致)
String signStr = buildSignString(method, requestPath, headers, body, nonce, timestamp);
// 5. 计算服务端签名
String serverSign = sign(signStr, secret);
// 6. 比对签名
return serverSign.equals(clientSign);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 客户端:生成签名
*/
public static String generateSignature(String method, String requestPath,
Map<String, String> headers, String body,
String appId, String secret) {
String nonce = UUID.randomUUID().toString().replace("-", "");
String timestamp = String.valueOf(System.currentTimeMillis());
String signStr = buildSignString(method, requestPath, headers, body, nonce, Long.parseLong(timestamp));
return sign(signStr, secret);
}
/**
* 核心:构建待签名字符串
*/
private static String buildSignString(String method, String requestPath,
Map<String, String> headers, String body,
String nonce, long timestamp) {
StringBuilder sb = new StringBuilder();
// 1. HTTP方法 (大写)
sb.append(method.toUpperCase()).append("\n");
// 2. 请求路径
sb.append(requestPath).append("\n");
// 3. 请求头排序拼接 (只选取特定头,避免因头顺序变化导致签名失败)
// 这里我们只加入 X-Nonce 和 X-Timestamp,实际项目可加入 Content-Type 等
TreeMap<String, String> sortedHeaders = new TreeMap<>();
sortedHeaders.put("X-Nonce", nonce);
sortedHeaders.put("X-Timestamp", String.valueOf(timestamp));
// 加入其他必要的头(如 Content-MD5 等)
if (headers != null) {
// 可以添加自定义头,但必须排序
}
for (Map.Entry<String, String> entry : sortedHeaders.entrySet()) {
sb.append(entry.getKey()).append(": ").append(entry.getValue()).append("\n");
}
// 4. 请求体 (可进行URL编码或MD5)
if (body != null && !body.isEmpty()) {
// 简单做法:直接拼入原始body,更安全的做法是计算Body的MD5或SHA256
// sb.append( DigestUtils.md5Hex(body) );
sb.append(body);
}
return sb.toString();
}
/**
* 执行 HMAC-SHA256 签名
*/
private static String sign(String data, String secret) {
return HmacUtils.hmacSha256Hex(secret, data);
}
/**
* 获取请求头值(忽略大小写)
*/
private static String getHeader(Map<String, String> headers, String key) {
if (headers == null) return null;
for (Map.Entry<String, String> entry : headers.entrySet()) {
if (entry.getKey().equalsIgnoreCase(key)) {
return entry.getValue();
}
}
return null;
}
/**
* 函数式接口:根据 AppId 获取 Secret
*/
@FunctionalInterface
public interface SecretProvider {
String getSecretByAppId(String appId);
}
}
服务端 Spring Boot 拦截器示例
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.util.*;
public class SignInterceptor implements HandlerInterceptor {
private SignUtil.SecretProvider secretProvider;
public SignInterceptor(SignUtil.SecretProvider secretProvider) {
this.secretProvider = secretProvider;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1. 获取请求方法和路径
String method = request.getMethod();
String requestPath = request.getRequestURI(); // 不含?的参数
// 2. 获取请求体 (注意:需确保不会多次读取流,建议使用ContentCachingRequestWrapper)
String body = getBodyString(request);
// 3. 获取请求头
Map<String, String> headers = new HashMap<>();
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String name = headerNames.nextElement();
headers.put(name, request.getHeader(name));
}
// 4. 校验签名
boolean valid = SignUtil.verifySignature(method, requestPath, headers, body, secretProvider);
if (!valid) {
response.setStatus(401);
response.getWriter().write("{\"code\":401,\"message\":\"Invalid signature\"}");
return false;
}
return true;
}
private String getBodyString(HttpServletRequest request) {
try (BufferedReader br = request.getReader()) {
StringBuilder sb = new StringBuilder();
String line;
while ((line = br.readLine()) != null) {
sb.append(line);
}
return sb.toString();
} catch (Exception e) {
return "";
}
}
}
客户端调用示例 (使用 Java HttpClient)
import java.net.http.*;
import java.net.URI;
import java.util.*;
public class ClientExample {
public static void main(String[] args) throws Exception {
String appId = "testApp";
String secret = "1234567890abcdef"; // 与服务端存储的一致
Map<String, String> headers = new HashMap<>();
headers.put("X-AppId", appId);
String method = "POST";
String path = "/api/order/create";
String body = "{\"productId\":123}";
// 生成签名
String sign = SignUtil.generateSignature(method, path, headers, body, appId, secret);
headers.put("X-Sign", sign);
// 注意:generateSignature 内部也会生成 timestamp 和 nonce 放入签名计算,
// 但未放入headers,需要客户端自行放入
headers.put("X-Timestamp", String.valueOf(System.currentTimeMillis()));
headers.put("X-Nonce", UUID.randomUUID().toString().replace("-", ""));
// 发送HTTP请求
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("http://localhost:8080" + path))
.header("Content-Type", "application/json")
.header("X-AppId", appId)
.header("X-Sign", headers.get("X-Sign"))
.header("X-Timestamp", headers.get("X-Timestamp"))
.header("X-Nonce", headers.get("X-Nonce"))
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
}
}
高级优化与安全建议
-
防重放攻击:
- 除了
Timestamp过期验证(5~15分钟),建议服务端缓存已使用的Nonce(如Redis Set,过期时间与Timestamp一致),防止同一个Nonce被重复使用。
- 除了
-
请求体摘要:
- 当请求体很大时,直接拼接原始 body 效率低且可能被篡改,建议改为
SHA256(body)或MD5(body)再拼入签名字符串。 - 同时在请求头中加入
Content-MD5或X-Body-Hash,服务端先验证 body 完整性,再校验签名。
- 当请求体很大时,直接拼接原始 body 效率低且可能被篡改,建议改为
-
参数归一化:
- 对于
GET请求(Query参数),需要对所有参数按照 Key 排序,并拼接成key1=value1&key2=value2形式,value需进行URLEncoder.encode(value, "UTF-8")。
- 对于
-
密钥管理:
AppSecret是核心凭据,禁止硬编码在客户端源码中,应通过环境变量、配置中心或动态获取(如云端密钥管理服务 KMS)。- 服务端存储
Secret建议使用加密存储(如 AES 加密),业务逻辑只解密后用于签名比对。
-
非对称签名(RSA):
- 如果无法安全分发对称密钥(例如系统间公钥交换),可使用 RSA/ECDSA:
- 客户端用私钥签名。
- 服务端用公钥验签。
- 示例:
Signature.getInstance("SHA256withRSA"),signature.sign()/signature.verify()。
- 如果无法安全分发对称密钥(例如系统间公钥交换),可使用 RSA/ECDSA:
-
Spring Boot 注解支持:
- 可自定义
@SignValidate注解,配合 AOP 或 Filter 拦截,自动完成签名校验,降低业务代码侵入。
- 可自定义
- 最稳定、最广泛的方案是
HMAC-SHA256,配合Timestamp + Nonce防重放。 - 实现要点:待签名字符串的拼接规则必须客户端与服务端完全一致(大小写、顺序、空行等)。
- 安全性:签名只能防止篡改和伪造,不能防止窃听,务必配合 HTTPS 使用。
代码可直接应用于生产环境的微服务间调用鉴权或开放 API 签名验证。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播