【代码优化】framework:优化 HTTP 请求签名的实现

1、单测从集成测试,改成单元测试
2、SignatureAspect 调整代码,提升易读性
3、sign 算法调整,使用 querystring + body + header + appsecret 更容易理解
This commit is contained in:
YunaiV 2024-06-04 19:10:48 +08:00
parent 1e391d626e
commit e0a6e3988b
11 changed files with 301 additions and 339 deletions

View File

@ -38,8 +38,8 @@
<!-- Test 测试相关 --> <!-- Test 测试相关 -->
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>cn.iocoder.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId> <artifactId>yudao-spring-boot-starter-test</artifactId>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
</dependencies> </dependencies>

View File

@ -0,0 +1,28 @@
package cn.iocoder.yudao.framework.signature.config;
import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration;
import cn.iocoder.yudao.framework.signature.core.aop.ApiSignatureAspect;
import cn.iocoder.yudao.framework.signature.core.redis.ApiSignatureRedisDAO;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.core.StringRedisTemplate;
/**
* HTTP API 签名的自动配置类
*
* @author Zhougang
*/
@AutoConfiguration(after = YudaoRedisAutoConfiguration.class)
public class YudaoApiSignatureAutoConfiguration {
@Bean
public ApiSignatureAspect signatureAspect(ApiSignatureRedisDAO signatureRedisDAO) {
return new ApiSignatureAspect(signatureRedisDAO);
}
@Bean
public ApiSignatureRedisDAO signatureRedisDAO(StringRedisTemplate stringRedisTemplate) {
return new ApiSignatureRedisDAO(stringRedisTemplate);
}
}

View File

@ -1,27 +0,0 @@
package cn.iocoder.yudao.framework.signature.config;
import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration;
import cn.iocoder.yudao.framework.signature.core.aop.SignatureAspect;
import cn.iocoder.yudao.framework.signature.core.redis.SignatureRedisDAO;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.core.StringRedisTemplate;
/**
* @author Zhougang
*/
@AutoConfiguration(after = YudaoRedisAutoConfiguration.class)
public class YudaoSignatureAutoConfiguration {
@Bean
public SignatureAspect signatureAspect(SignatureRedisDAO signatureRedisDAO) {
return new SignatureAspect(signatureRedisDAO);
}
@Bean
@SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
public SignatureRedisDAO signatureRedisDAO(StringRedisTemplate stringRedisTemplate) {
return new SignatureRedisDAO(stringRedisTemplate);
}
}

View File

@ -7,7 +7,7 @@ import java.util.concurrent.TimeUnit;
/** /**
* 签名注解 * HTTP API 签名注解
* *
* @author Zhougang * @author Zhougang
*/ */

View File

@ -0,0 +1,169 @@
package cn.iocoder.yudao.framework.signature.core.aop;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestUtil;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
import cn.iocoder.yudao.framework.signature.core.annotation.ApiSignature;
import cn.iocoder.yudao.framework.signature.core.redis.ApiSignatureRedisDAO;
import jakarta.servlet.http.HttpServletRequest;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import java.util.Map;
import java.util.Objects;
import java.util.SortedMap;
import java.util.TreeMap;
import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST;
/**
* 拦截声明了 {@link ApiSignature} 注解的方法实现签名
*
* @author Zhougang
*/
@Aspect
@Slf4j
@AllArgsConstructor
public class ApiSignatureAspect {
private final ApiSignatureRedisDAO signatureRedisDAO;
@Before("@annotation(signature)")
public void beforePointCut(JoinPoint joinPoint, ApiSignature signature) {
// 1. 验证通过直接结束
if (verifySignature(signature, Objects.requireNonNull(ServletUtils.getRequest()))) {
return;
}
// 2. 验证不通过抛出异常
log.error("[beforePointCut][方法{} 参数({}) 签名失败]", joinPoint.getSignature().toString(),
joinPoint.getArgs());
throw new ServiceException(BAD_REQUEST.getCode(),
StrUtil.blankToDefault(signature.message(), BAD_REQUEST.getMsg()));
}
public boolean verifySignature(ApiSignature signature, HttpServletRequest request) {
// 1.1 校验 Header
if (!verifyHeaders(signature, request)) {
return false;
}
// 1.2 校验 appId 是否能获取到对应的 appSecret
String appId = request.getHeader(signature.appId());
String appSecret = signatureRedisDAO.getAppSecret(appId);
Assert.notNull(appSecret, "[appId({})] 找不到对应的 appSecret", appId);
// 2. 校验签名重要
String clientSignature = request.getHeader(signature.sign()); // 客户端签名
String serverSignatureString = buildSignatureString(signature, request, appSecret); // 服务端签名字符串
String serverSignature = DigestUtil.sha256Hex(serverSignatureString); // 服务端签名
if (ObjUtil.notEqual(clientSignature, serverSignature)) {
return false;
}
// 3. nonce 记入缓存防止重复使用重点二此处需要将 ttl 设定为允许 timestamp 时间差的值 x 2
String nonce = request.getHeader(signature.nonce());
signatureRedisDAO.setNonce(nonce, signature.timeout() * 2, signature.timeUnit());
return true;
}
/**
* 校验请求头加签参数
*
* 1. appId 是否为空
* 2. timestamp 是否为空请求是否已经超时默认 10 分钟
* 3. nonce 是否为空随机数是否 10 位以上是否在规定时间内已经访问过了
* 4. sign 是否为空
*
* @param signature signature
* @param request request
* @return 是否校验 Header 通过
*/
private boolean verifyHeaders(ApiSignature signature, HttpServletRequest request) {
// 1. 非空校验
String appId = request.getHeader(signature.appId());
if (StrUtil.isBlank(appId)) {
return false;
}
String timestamp = request.getHeader(signature.timestamp());
if (StrUtil.isBlank(timestamp)) {
return false;
}
String nonce = request.getHeader(signature.nonce());
if (StrUtil.length(nonce) < 10) {
return false;
}
String sign = request.getHeader(signature.sign());
if (StrUtil.isBlank(sign)) {
return false;
}
// 2. 检查 timestamp 是否超出允许的范围 重点一此处需要取绝对值
long expireTime = signature.timeUnit().toMillis(signature.timeout());
long requestTimestamp = Long.parseLong(timestamp);
long timestampDisparity = Math.abs(System.currentTimeMillis() - requestTimestamp);
if (timestampDisparity > expireTime) {
return false;
}
// 3. 检查 nonce 是否存在有且仅能使用一次
return signatureRedisDAO.getNonce(nonce) == null;
}
/**
* 构建签名字符串
*
* 格式为 = 请求参数 + 请求体 + 请求头 + 密钥
*
* @param signature signature
* @param request request
* @param appSecret appSecret
* @return 签名字符串
*/
private String buildSignatureString(ApiSignature signature, HttpServletRequest request, String appSecret) {
SortedMap<String, String> parameterMap = getRequestParameterMap(request); // 请求头
SortedMap<String, String> headerMap = getRequestHeaderMap(signature, request); // 请求参数
String requestBody = StrUtil.nullToDefault(ServletUtils.getBody(request), ""); // 请求体
return MapUtil.join(parameterMap, "&", "=")
+ requestBody
+ MapUtil.join(headerMap, "&", "=")
+ appSecret;
}
/**
* 获取请求头加签参数 Map
*
* @param request 请求
* @param signature 签名注解
* @return signature params
*/
private static SortedMap<String, String> getRequestHeaderMap(ApiSignature signature, HttpServletRequest request) {
SortedMap<String, String> sortedMap = new TreeMap<>();
sortedMap.put(signature.appId(), request.getHeader(signature.appId()));
sortedMap.put(signature.timestamp(), request.getHeader(signature.timestamp()));
sortedMap.put(signature.nonce(), request.getHeader(signature.nonce()));
return sortedMap;
}
/**
* 获取请求参数 Map
*
* @param request 请求
* @return queryParams
*/
private static SortedMap<String, String> getRequestParameterMap(HttpServletRequest request) {
SortedMap<String, String> sortedMap = new TreeMap<>();
for (Map.Entry<String, String[]> entry : request.getParameterMap().entrySet()) {
sortedMap.put(entry.getKey(), entry.getValue()[0]);
}
return sortedMap;
}
}

View File

@ -1,155 +0,0 @@
package cn.iocoder.yudao.framework.signature.core.aop;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SignUtil;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
import cn.iocoder.yudao.framework.signature.core.annotation.ApiSignature;
import cn.iocoder.yudao.framework.signature.core.redis.SignatureRedisDAO;
import cn.iocoder.yudao.framework.web.core.filter.CacheRequestBodyWrapper;
import jakarta.servlet.http.HttpServletRequest;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.Objects;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.concurrent.TimeUnit;
/**
* 拦截声明了 {@link ApiSignature} 注解的方法实现签名
*
* @author Zhougang
*/
@Aspect
@Slf4j
@AllArgsConstructor
public class SignatureAspect {
private final SignatureRedisDAO signatureRedisDAO;
@Before("@annotation(signature)")
public void beforePointCut(JoinPoint joinPoint, ApiSignature signature) {
if (!verifySignature(signature, Objects.requireNonNull(ServletUtils.getRequest()))) {
log.error("[beforePointCut][方法{} 参数({}) 签名失败]", joinPoint.getSignature().toString(),
joinPoint.getArgs());
String message = StrUtil.blankToDefault(signature.message(),
GlobalErrorCodeConstants.BAD_REQUEST.getMsg());
throw new ServiceException(GlobalErrorCodeConstants.BAD_REQUEST.getCode(), message);
}
}
private boolean verifySignature(ApiSignature signature, HttpServletRequest request) {
if (!verifyHeaders(signature, request)) {
return false;
}
// 校验 appId 是否能获取到对应的 appSecret
String appId = request.getHeader(signature.appId());
String appSecret = signatureRedisDAO.getAppSecret(appId);
Assert.notNull(appSecret, "[appId({})] 找不到对应的 appSecret", appId);
// 请求头
SortedMap<String, String> headersMap = getRequestHeaders(signature, request);
// 请求参数
String requestParams = getRequestParams(request);
// 请求体
String requestBody = ServletUtils.isJsonRequest(request) ? ServletUtils.getBody(request) : "";
// 生成服务端签名
String serverSignature = SignUtil.signParamsSha256(headersMap, requestParams + requestBody + appSecret);
// 客户端签名
String clientSignature = request.getHeader(signature.sign());
if (!StrUtil.equals(clientSignature, serverSignature)) {
return false;
}
String nonce = headersMap.get(signature.nonce());
// nonce 记入缓存防止重复使用重点二此处需要将 ttl 设定为允许 timestamp 时间差的值 x 2
signatureRedisDAO.setNonce(nonce, signature.timeout() * 2L, signature.timeUnit());
return true;
}
/**
* 校验请求头加签参数
* 1.appId 是否为空
* 2.timestamp 是否为空请求是否已经超时默认 10 分钟
* 3.nonce 是否为空随机数是否 10 位以上是否在规定时间内已经访问过了
* 4.sign 是否为空
*
* @param signature signature
* @param request request
*/
private boolean verifyHeaders(ApiSignature signature, HttpServletRequest request) {
String appId = request.getHeader(signature.appId());
if (StrUtil.isBlank(appId)) {
return false;
}
String timestamp = request.getHeader(signature.timestamp());
if (StrUtil.isBlank(timestamp)) {
return false;
}
String nonce = request.getHeader(signature.nonce());
if (StrUtil.isBlank(nonce) || StrUtil.length(nonce) < 10) {
return false;
}
String sign = request.getHeader(signature.sign());
if (StrUtil.isBlank(sign)) {
return false;
}
// 其他合法性校验
long expireTime = signature.timeUnit().toMillis(signature.timeout());
long requestTimestamp = Long.parseLong(timestamp);
// 检查 timestamp 是否超出允许的范围 重点一此处需要取绝对值
long timestampDisparity = Math.abs(System.currentTimeMillis() - requestTimestamp);
if (timestampDisparity > expireTime) {
return false;
}
String cacheNonce = signatureRedisDAO.getNonce(nonce);
return StrUtil.isBlank(cacheNonce);
}
/**
* 获取请求头加签参数
*
* @param request request
* @return signature params
*/
private SortedMap<String, String> getRequestHeaders(ApiSignature signature, HttpServletRequest request) {
SortedMap<String, String> sortedMap = new TreeMap<>();
sortedMap.put(signature.appId(), request.getHeader(signature.appId()));
sortedMap.put(signature.timestamp(), request.getHeader(signature.timestamp()));
sortedMap.put(signature.nonce(), request.getHeader(signature.nonce()));
return sortedMap;
}
/**
* 获取 URL 参数
*
* @param request request
* @return queryParams
*/
private String getRequestParams(HttpServletRequest request) {
if (CollUtil.isEmpty(request.getParameterMap())) {
return "";
}
Map<String, String[]> requestParams = request.getParameterMap();
// 获取 URL 请求参数
SortedMap<String, String> sortParamsMap = new TreeMap<>();
for (Map.Entry<String, String[]> entry : requestParams.entrySet()) {
sortParamsMap.put(entry.getKey(), entry.getValue()[0]);
}
// key 排序
StringBuilder queryString = new StringBuilder();
for (Map.Entry<String, String> entry : sortParamsMap.entrySet()) {
queryString.append("&").append(entry.getKey()).append("=").append(entry.getValue());
}
return queryString.substring(1);
}
}

View File

@ -6,50 +6,52 @@ import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
/** /**
* API 签名 Redis DAO * HTTP API 签名 Redis DAO
* *
* @author Zhougang * @author Zhougang
*/ */
@AllArgsConstructor @AllArgsConstructor
public class SignatureRedisDAO { public class ApiSignatureRedisDAO {
private final StringRedisTemplate stringRedisTemplate; private final StringRedisTemplate stringRedisTemplate;
/** /**
* 验签随机数 * 验签随机数
* <p> *
* KEY 格式signature_nonce:%s // 参数为 随机数 * KEY 格式signature_nonce:%s // 参数为 随机数
* VALUE 格式String * VALUE 格式String
* 过期时间不固定 * 过期时间不固定
*/ */
private static final String SIGNATURE_NONCE = "signature_nonce:%s"; private static final String SIGNATURE_NONCE = "api_signature_nonce:%s";
/** /**
* 签名密钥 * 签名密钥
* <p> *
* KEY 格式signature_appid:%s // 参数为 appid * HASH 结构
* KEY 格式%s // 参数为 appid
* VALUE 格式String * VALUE 格式String
* 过期时间预加载到 redis 永不过期 * 过期时间永不过期预加载到 Redis
*/ */
private static final String SIGNATURE_APPID = "signature_appid:%s"; private static final String SIGNATURE_APPID = "api_signature_app";
public String getAppSecret(String appId) { // ========== 验签随机数 ==========
return stringRedisTemplate.opsForValue().get(formatAppIdKey(appId));
}
public String getNonce(String nonce) { public String getNonce(String nonce) {
return stringRedisTemplate.opsForValue().get(formatNonceKey(nonce)); return stringRedisTemplate.opsForValue().get(formatNonceKey(nonce));
} }
public void setNonce(String nonce, long time, TimeUnit timeUnit) { public void setNonce(String nonce, int time, TimeUnit timeUnit) {
stringRedisTemplate.opsForValue().set(formatNonceKey(nonce), nonce, time, timeUnit); stringRedisTemplate.opsForValue().set(formatNonceKey(nonce), "", time, timeUnit);
}
private static String formatAppIdKey(String key) {
return String.format(SIGNATURE_APPID, key);
} }
private static String formatNonceKey(String key) { private static String formatNonceKey(String key) {
return String.format(SIGNATURE_NONCE, key); return String.format(SIGNATURE_NONCE, key);
} }
// ========== 签名密钥 ==========
public String getAppSecret(String appId) {
return (String) stringRedisTemplate.opsForHash().get(SIGNATURE_APPID, appId);
}
} }

View File

@ -0,0 +1,6 @@
/**
* HTTP API 签名校验安全性
*
* @see <a href="https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=4_3>微信支付 —— 安全规范</a>
*/
package cn.iocoder.yudao.framework.signature;

View File

@ -1,4 +1,4 @@
cn.iocoder.yudao.framework.idempotent.config.YudaoIdempotentConfiguration cn.iocoder.yudao.framework.idempotent.config.YudaoIdempotentConfiguration
cn.iocoder.yudao.framework.lock4j.config.YudaoLock4jConfiguration cn.iocoder.yudao.framework.lock4j.config.YudaoLock4jConfiguration
cn.iocoder.yudao.framework.ratelimiter.config.YudaoRateLimiterConfiguration cn.iocoder.yudao.framework.ratelimiter.config.YudaoRateLimiterConfiguration
cn.iocoder.yudao.framework.signature.config.YudaoSignatureAutoConfiguration cn.iocoder.yudao.framework.signature.config.YudaoApiSignatureAutoConfiguration

View File

@ -0,0 +1,75 @@
package cn.iocoder.yudao.framework.signature.core;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.crypto.digest.DigestUtil;
import cn.iocoder.yudao.framework.signature.core.annotation.ApiSignature;
import cn.iocoder.yudao.framework.signature.core.aop.ApiSignatureAspect;
import cn.iocoder.yudao.framework.signature.core.redis.ApiSignatureRedisDAO;
import jakarta.servlet.http.HttpServletRequest;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
import java.util.concurrent.TimeUnit;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;
/**
* {@link ApiSignatureTest} 的单元测试
*/
@ExtendWith(MockitoExtension.class)
public class ApiSignatureTest {
@InjectMocks
private ApiSignatureAspect apiSignatureAspect;
@Mock
private ApiSignatureRedisDAO signatureRedisDAO;
@Test
public void testSignatureGet() throws IOException {
// 搞一个签名
Long timestamp = System.currentTimeMillis();
String nonce = IdUtil.randomUUID();
String appId = "xxxxxx";
String appSecret = "yyyyyy";
String signString = "k1=v1&v1=k1testappId=xxxxxx&nonce=" + nonce + "&timestamp=" + timestamp + "yyyyyy";
String sign = DigestUtil.sha256Hex(signString);
// 准备参数
ApiSignature apiSignature = mock(ApiSignature.class);
when(apiSignature.appId()).thenReturn("appId");
when(apiSignature.timestamp()).thenReturn("timestamp");
when(apiSignature.nonce()).thenReturn("nonce");
when(apiSignature.sign()).thenReturn("sign");
when(apiSignature.timeout()).thenReturn(60);
when(apiSignature.timeUnit()).thenReturn(TimeUnit.SECONDS);
HttpServletRequest request = mock(HttpServletRequest.class);
when(request.getHeader(eq("appId"))).thenReturn(appId);
when(request.getHeader(eq("timestamp"))).thenReturn(String.valueOf(timestamp));
when(request.getHeader(eq("nonce"))).thenReturn(nonce);
when(request.getHeader(eq("sign"))).thenReturn(sign);
when(request.getParameterMap()).thenReturn(MapUtil.<String, String[]>builder()
.put("v1", new String[]{"k1"}).put("k1", new String[]{"v1"}).build());
when(request.getContentType()).thenReturn("application/json");
when(request.getReader()).thenReturn(new BufferedReader(new StringReader("test")));
// mock 方法
when(signatureRedisDAO.getAppSecret(eq(appId))).thenReturn(appSecret);
// 调用
boolean result = apiSignatureAspect.verifySignature(apiSignature, request);
// 断言结果
assertTrue(result);
// 断言调用
verify(signatureRedisDAO).setNonce(eq(nonce), eq(120), eq(TimeUnit.SECONDS));
}
}

View File

@ -1,136 +0,0 @@
package cn.iocoder.yudao.framework.signature.core;
import cn.hutool.core.lang.Snowflake;
import cn.hutool.crypto.digest.DigestUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import cn.hutool.http.HttpUtil;
import org.junit.jupiter.api.Test;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
/**
* {@link SignatureTest} 的单元测试
*/
public class SignatureTest {
@Test
public void testSignatureGet() {
String appId = "xxxxxx";
Snowflake snowflake = new Snowflake();
// 验签请求头前端需传入字段
SortedMap<String, String> headersMap = new TreeMap<>();
headersMap.put("appId", appId);
headersMap.put("timestamp", String.valueOf(System.currentTimeMillis()));
headersMap.put("nonce", String.valueOf(snowflake.nextId()));
// 客户端加签内容
StringBuilder clientSignatureContent = new StringBuilder();
// 请求头
for (Map.Entry<String, String> entry : headersMap.entrySet()) {
clientSignatureContent.append(entry.getKey()).append(entry.getValue());
}
// 请求 url
clientSignatureContent.append("/admin-api/infra/demo01-contact/get");
// 请求参数
SortedMap<String, String> paramsMap = new TreeMap<>();
paramsMap.put("id", "100");
paramsMap.put("name", "张三");
StringBuilder queryString = new StringBuilder();
for (Map.Entry<String, String> entry : paramsMap.entrySet()) {
queryString.append("&").append(entry.getKey()).append("=").append(entry.getValue());
}
clientSignatureContent.append(queryString.substring(1));
// 密钥
clientSignatureContent.append("d3cbeed9baf4e68673a1f69a2445359a20022b7c28ea2933dd9db9f3a29f902b");
System.out.println("加签内容:" + clientSignatureContent);
// 加签
headersMap.put("sign", DigestUtil.sha256Hex(clientSignatureContent.toString()));
headersMap.put("Authorization", "Bearer xxx");
HttpRequest get = HttpUtil.createGet("http://localhost:48080/admin-api/infra/demo01-contact/get?id=100&name=张三");
get.addHeaders(headersMap);
System.out.println("执行结果==" + get.execute());
}
@Test
public void testSignaturePost() {
String appId = "xxxxxx";
Snowflake snowflake = new Snowflake();
// 验签请求头前端需传入字段
SortedMap<String, String> headersMap = new TreeMap<>();
headersMap.put("appId", appId);
headersMap.put("timestamp", String.valueOf(System.currentTimeMillis()));
headersMap.put("nonce", String.valueOf(snowflake.nextId()));
// 客户端加签内容
StringBuilder clientSignatureContent = new StringBuilder();
// 请求头
for (Map.Entry<String, String> entry : headersMap.entrySet()) {
clientSignatureContent.append(entry.getKey()).append(entry.getValue());
}
// 请求 url
clientSignatureContent.append("/admin-api/infra/demo01-contact/create");
// 请求体
String body = "{\n" +
" \"password\": \"1\",\n" +
" \"date\": \"2024-04-24 16:28:00\",\n" +
" \"user\": {\n" +
" \"area\": \"浦东新区\",\n" +
" \"1\": \"xx\",\n" +
" \"2\": \"xx\",\n" +
" \"province\": \"上海市\",\n" +
" \"data\": {\n" +
" \"99\": \"xx\",\n" +
" \"1\": \"xx\",\n" +
" \"100\": \"xx\",\n" +
" \"2\": \"xx\",\n" +
" \"3\": \"xx\",\n" +
" \"array\": [\n" +
" {\n" +
" \"3\": \"aa\",\n" +
" \"4\": \"aa\",\n" +
" \"2\": \"aa\",\n" +
" \"1\": \"aa\"\n" +
" },\n" +
" {\n" +
" \"99\": \"aa\",\n" +
" \"100\": \"aa\",\n" +
" \"88\": \"aa\",\n" +
" \"120\": \"aa\"\n" +
" }\n" +
" ]\n" +
" },\n" +
" \"sex\": \"\",\n" +
" \"name\": \"张三\",\n" +
" \"array\": [\n" +
" \"1\",\n" +
" \"3\",\n" +
" \"5\",\n" +
" \"2\"\n" +
" ]\n" +
" },\n" +
" \"username\": \"xiaoming\"\n" +
"}";
clientSignatureContent.append(body);
// 密钥
clientSignatureContent.append("d3cbeed9baf4e68673a1f69a2445359a20022b7c28ea2933dd9db9f3a29f902b");
System.out.println("加签内容:" + clientSignatureContent);
// 加签
headersMap.put("sign", DigestUtil.sha256Hex(clientSignatureContent.toString()));
headersMap.put("Authorization", "Bearer xxx");
HttpRequest post = HttpUtil.createPost("http://localhost:48080/admin-api/infra/demo01-contact/create");
post.addHeaders(headersMap);
post.body(body);
try (HttpResponse execute = post.execute()) {
System.out.println("执行结果==" + execute);
}
}
}