Commit 9eb62fb5a5c32331b78f5a01b0f01af613d55d4f
Committed by
GitHub
Merge pull request #541 from slipper4j/dev-0519-framework-signature
【优化】根据给出的建议优化部分代码逻辑、风格
Showing
7 changed files
with
442 additions
and
2 deletions
yudao-framework/yudao-spring-boot-starter-protection/pom.xml
| ... | ... | @@ -12,7 +12,7 @@ |
| 12 | 12 | <packaging>jar</packaging> |
| 13 | 13 | |
| 14 | 14 | <name>${project.artifactId}</name> |
| 15 | - <description>服务保证,提供分布式锁、幂等、限流、熔断等等功能</description> | |
| 15 | + <description>服务保证,提供分布式锁、幂等、限流、熔断、API 签名等等功能</description> | |
| 16 | 16 | <url>https://github.com/YunaiV/ruoyi-vue-pro</url> |
| 17 | 17 | |
| 18 | 18 | <dependencies> |
| ... | ... | @@ -35,6 +35,13 @@ |
| 35 | 35 | <artifactId>lock4j-redisson-spring-boot-starter</artifactId> |
| 36 | 36 | <optional>true</optional> |
| 37 | 37 | </dependency> |
| 38 | + | |
| 39 | + <!-- Test 测试相关 --> | |
| 40 | + <dependency> | |
| 41 | + <groupId>org.springframework.boot</groupId> | |
| 42 | + <artifactId>spring-boot-starter-test</artifactId> | |
| 43 | + <scope>test</scope> | |
| 44 | + </dependency> | |
| 38 | 45 | </dependencies> |
| 39 | 46 | |
| 40 | 47 | </project> | ... | ... |
yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/config/YudaoSignatureAutoConfiguration.java
0 → 100644
| 1 | +package cn.iocoder.yudao.framework.signature.config; | |
| 2 | + | |
| 3 | +import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration; | |
| 4 | +import cn.iocoder.yudao.framework.signature.core.aop.SignatureAspect; | |
| 5 | +import cn.iocoder.yudao.framework.signature.core.redis.SignatureRedisDAO; | |
| 6 | +import org.springframework.boot.autoconfigure.AutoConfiguration; | |
| 7 | +import org.springframework.context.annotation.Bean; | |
| 8 | +import org.springframework.data.redis.core.StringRedisTemplate; | |
| 9 | + | |
| 10 | +/** | |
| 11 | + * @author Zhougang | |
| 12 | + */ | |
| 13 | +@AutoConfiguration(after = YudaoRedisAutoConfiguration.class) | |
| 14 | +public class YudaoSignatureAutoConfiguration { | |
| 15 | + | |
| 16 | + @Bean | |
| 17 | + public SignatureAspect signatureAspect(SignatureRedisDAO signatureRedisDAO) { | |
| 18 | + return new SignatureAspect(signatureRedisDAO); | |
| 19 | + } | |
| 20 | + | |
| 21 | + @Bean | |
| 22 | + @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection") | |
| 23 | + public SignatureRedisDAO signatureRedisDAO(StringRedisTemplate stringRedisTemplate) { | |
| 24 | + return new SignatureRedisDAO(stringRedisTemplate); | |
| 25 | + } | |
| 26 | + | |
| 27 | +} | ... | ... |
yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/annotation/ApiSignature.java
0 → 100644
| 1 | +package cn.iocoder.yudao.framework.signature.core.annotation; | |
| 2 | + | |
| 3 | +import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; | |
| 4 | + | |
| 5 | +import java.lang.annotation.*; | |
| 6 | +import java.util.concurrent.TimeUnit; | |
| 7 | + | |
| 8 | + | |
| 9 | +/** | |
| 10 | + * 签名注解 | |
| 11 | + * | |
| 12 | + * @author Zhougang | |
| 13 | + */ | |
| 14 | +@Inherited | |
| 15 | +@Documented | |
| 16 | +@Target({ElementType.METHOD, ElementType.TYPE}) | |
| 17 | +@Retention(RetentionPolicy.RUNTIME) | |
| 18 | +public @interface ApiSignature { | |
| 19 | + | |
| 20 | + /** | |
| 21 | + * 同一个请求多长时间内有效 默认 60 秒 | |
| 22 | + */ | |
| 23 | + int timeout() default 60; | |
| 24 | + | |
| 25 | + /** | |
| 26 | + * 时间单位,默认为 SECONDS 秒 | |
| 27 | + */ | |
| 28 | + TimeUnit timeUnit() default TimeUnit.SECONDS; | |
| 29 | + | |
| 30 | + // ========================== 签名参数 ========================== | |
| 31 | + | |
| 32 | + /** | |
| 33 | + * 提示信息,签名失败的提示 | |
| 34 | + * | |
| 35 | + * @see GlobalErrorCodeConstants#BAD_REQUEST | |
| 36 | + */ | |
| 37 | + String message() default "签名不正确"; // 为空时,使用 BAD_REQUEST 错误提示 | |
| 38 | + | |
| 39 | + /** | |
| 40 | + * 签名字段:appId 应用ID | |
| 41 | + */ | |
| 42 | + String appId() default "appId"; | |
| 43 | + | |
| 44 | + /** | |
| 45 | + * 签名字段:timestamp 时间戳 | |
| 46 | + */ | |
| 47 | + String timestamp() default "timestamp"; | |
| 48 | + | |
| 49 | + /** | |
| 50 | + * 签名字段:nonce 随机数,10 位以上 | |
| 51 | + */ | |
| 52 | + String nonce() default "nonce"; | |
| 53 | + | |
| 54 | + /** | |
| 55 | + * sign 客户端签名 | |
| 56 | + */ | |
| 57 | + String sign() default "sign"; | |
| 58 | + | |
| 59 | +} | ... | ... |
yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/aop/SignatureAspect.java
0 → 100644
| 1 | +package cn.iocoder.yudao.framework.signature.core.aop; | |
| 2 | + | |
| 3 | +import cn.hutool.core.collection.CollUtil; | |
| 4 | +import cn.hutool.core.lang.Assert; | |
| 5 | +import cn.hutool.core.util.StrUtil; | |
| 6 | +import cn.hutool.crypto.SignUtil; | |
| 7 | +import cn.iocoder.yudao.framework.common.exception.ServiceException; | |
| 8 | +import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; | |
| 9 | +import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; | |
| 10 | +import cn.iocoder.yudao.framework.signature.core.annotation.ApiSignature; | |
| 11 | +import cn.iocoder.yudao.framework.signature.core.redis.SignatureRedisDAO; | |
| 12 | +import cn.iocoder.yudao.framework.web.core.filter.CacheRequestBodyWrapper; | |
| 13 | +import jakarta.servlet.http.HttpServletRequest; | |
| 14 | +import lombok.AllArgsConstructor; | |
| 15 | +import lombok.extern.slf4j.Slf4j; | |
| 16 | +import org.aspectj.lang.JoinPoint; | |
| 17 | +import org.aspectj.lang.annotation.Aspect; | |
| 18 | +import org.aspectj.lang.annotation.Before; | |
| 19 | + | |
| 20 | +import java.nio.charset.StandardCharsets; | |
| 21 | +import java.util.Map; | |
| 22 | +import java.util.Objects; | |
| 23 | +import java.util.SortedMap; | |
| 24 | +import java.util.TreeMap; | |
| 25 | +import java.util.concurrent.TimeUnit; | |
| 26 | + | |
| 27 | +/** | |
| 28 | + * 拦截声明了 {@link ApiSignature} 注解的方法,实现签名 | |
| 29 | + * | |
| 30 | + * @author Zhougang | |
| 31 | + */ | |
| 32 | +@Aspect | |
| 33 | +@Slf4j | |
| 34 | +@AllArgsConstructor | |
| 35 | +public class SignatureAspect { | |
| 36 | + | |
| 37 | + private final SignatureRedisDAO signatureRedisDAO; | |
| 38 | + | |
| 39 | + @Before("@annotation(signature)") | |
| 40 | + public void beforePointCut(JoinPoint joinPoint, ApiSignature signature) { | |
| 41 | + if (!verifySignature(signature, Objects.requireNonNull(ServletUtils.getRequest()))) { | |
| 42 | + log.error("[beforePointCut][方法{} 参数({}) 签名失败]", joinPoint.getSignature().toString(), | |
| 43 | + joinPoint.getArgs()); | |
| 44 | + String message = StrUtil.blankToDefault(signature.message(), | |
| 45 | + GlobalErrorCodeConstants.BAD_REQUEST.getMsg()); | |
| 46 | + throw new ServiceException(GlobalErrorCodeConstants.BAD_REQUEST.getCode(), message); | |
| 47 | + } | |
| 48 | + } | |
| 49 | + | |
| 50 | + private boolean verifySignature(ApiSignature signature, HttpServletRequest request) { | |
| 51 | + if (!verifyHeaders(signature, request)) { | |
| 52 | + return false; | |
| 53 | + } | |
| 54 | + // 校验 appId 是否能获取到对应的 appSecret | |
| 55 | + String appId = request.getHeader(signature.appId()); | |
| 56 | + String appSecret = signatureRedisDAO.getAppSecret(appId); | |
| 57 | + Assert.notNull(appSecret, "[appId({})] 找不到对应的 appSecret", appId); | |
| 58 | + // 请求头 | |
| 59 | + SortedMap<String, String> headersMap = getRequestHeaders(signature, request); | |
| 60 | + // 请求参数 | |
| 61 | + String requestParams = getRequestParams(request); | |
| 62 | + // 请求体 | |
| 63 | + String requestBody = ServletUtils.isJsonRequest(request) ? ServletUtils.getBody(request) : ""; | |
| 64 | + // 生成服务端签名 | |
| 65 | + String serverSignature = SignUtil.signParamsSha256(headersMap, requestParams + requestBody + appSecret); | |
| 66 | + // 客户端签名 | |
| 67 | + String clientSignature = request.getHeader(signature.sign()); | |
| 68 | + if (!StrUtil.equals(clientSignature, serverSignature)) { | |
| 69 | + return false; | |
| 70 | + } | |
| 71 | + String nonce = headersMap.get(signature.nonce()); | |
| 72 | + // 将 nonce 记入缓存,防止重复使用(重点二:此处需要将 ttl 设定为允许 timestamp 时间差的值 x 2 ) | |
| 73 | + signatureRedisDAO.setNonce(nonce, signature.timeout() * 2L, signature.timeUnit()); | |
| 74 | + return true; | |
| 75 | + } | |
| 76 | + | |
| 77 | + /** | |
| 78 | + * 校验请求头加签参数 | |
| 79 | + * 1.appId 是否为空 | |
| 80 | + * 2.timestamp 是否为空,请求是否已经超时,默认 10 分钟 | |
| 81 | + * 3.nonce 是否为空,随机数是否 10 位以上,是否在规定时间内已经访问过了 | |
| 82 | + * 4.sign 是否为空 | |
| 83 | + * | |
| 84 | + * @param signature signature | |
| 85 | + * @param request request | |
| 86 | + */ | |
| 87 | + private boolean verifyHeaders(ApiSignature signature, HttpServletRequest request) { | |
| 88 | + String appId = request.getHeader(signature.appId()); | |
| 89 | + if (StrUtil.isBlank(appId)) { | |
| 90 | + return false; | |
| 91 | + } | |
| 92 | + String timestamp = request.getHeader(signature.timestamp()); | |
| 93 | + if (StrUtil.isBlank(timestamp)) { | |
| 94 | + return false; | |
| 95 | + } | |
| 96 | + String nonce = request.getHeader(signature.nonce()); | |
| 97 | + if (StrUtil.isBlank(nonce) || StrUtil.length(nonce) < 10) { | |
| 98 | + return false; | |
| 99 | + } | |
| 100 | + String sign = request.getHeader(signature.sign()); | |
| 101 | + if (StrUtil.isBlank(sign)) { | |
| 102 | + return false; | |
| 103 | + } | |
| 104 | + // 其他合法性校验 | |
| 105 | + long expireTime = signature.timeUnit().toMillis(signature.timeout()); | |
| 106 | + long requestTimestamp = Long.parseLong(timestamp); | |
| 107 | + // 检查 timestamp 是否超出允许的范围 (重点一:此处需要取绝对值) | |
| 108 | + long timestampDisparity = Math.abs(System.currentTimeMillis() - requestTimestamp); | |
| 109 | + if (timestampDisparity > expireTime) { | |
| 110 | + return false; | |
| 111 | + } | |
| 112 | + String cacheNonce = signatureRedisDAO.getNonce(nonce); | |
| 113 | + return StrUtil.isBlank(cacheNonce); | |
| 114 | + } | |
| 115 | + | |
| 116 | + /** | |
| 117 | + * 获取请求头加签参数 | |
| 118 | + * | |
| 119 | + * @param request request | |
| 120 | + * @return signature params | |
| 121 | + */ | |
| 122 | + private SortedMap<String, String> getRequestHeaders(ApiSignature signature, HttpServletRequest request) { | |
| 123 | + SortedMap<String, String> sortedMap = new TreeMap<>(); | |
| 124 | + sortedMap.put(signature.appId(), request.getHeader(signature.appId())); | |
| 125 | + sortedMap.put(signature.timestamp(), request.getHeader(signature.timestamp())); | |
| 126 | + sortedMap.put(signature.nonce(), request.getHeader(signature.nonce())); | |
| 127 | + return sortedMap; | |
| 128 | + } | |
| 129 | + | |
| 130 | + /** | |
| 131 | + * 获取 URL 参数 | |
| 132 | + * | |
| 133 | + * @param request request | |
| 134 | + * @return queryParams | |
| 135 | + */ | |
| 136 | + private String getRequestParams(HttpServletRequest request) { | |
| 137 | + if (CollUtil.isEmpty(request.getParameterMap())) { | |
| 138 | + return ""; | |
| 139 | + } | |
| 140 | + Map<String, String[]> requestParams = request.getParameterMap(); | |
| 141 | + // 获取 URL 请求参数 | |
| 142 | + SortedMap<String, String> sortParamsMap = new TreeMap<>(); | |
| 143 | + for (Map.Entry<String, String[]> entry : requestParams.entrySet()) { | |
| 144 | + sortParamsMap.put(entry.getKey(), entry.getValue()[0]); | |
| 145 | + } | |
| 146 | + // 按 key 排序 | |
| 147 | + StringBuilder queryString = new StringBuilder(); | |
| 148 | + for (Map.Entry<String, String> entry : sortParamsMap.entrySet()) { | |
| 149 | + queryString.append("&").append(entry.getKey()).append("=").append(entry.getValue()); | |
| 150 | + } | |
| 151 | + return queryString.substring(1); | |
| 152 | + } | |
| 153 | + | |
| 154 | +} | |
| 155 | + | ... | ... |
yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/redis/SignatureRedisDAO.java
0 → 100644
| 1 | +package cn.iocoder.yudao.framework.signature.core.redis; | |
| 2 | + | |
| 3 | +import lombok.AllArgsConstructor; | |
| 4 | +import org.springframework.data.redis.core.StringRedisTemplate; | |
| 5 | + | |
| 6 | +import java.util.concurrent.TimeUnit; | |
| 7 | + | |
| 8 | +/** | |
| 9 | + * API 签名 Redis DAO | |
| 10 | + * | |
| 11 | + * @author Zhougang | |
| 12 | + */ | |
| 13 | +@AllArgsConstructor | |
| 14 | +public class SignatureRedisDAO { | |
| 15 | + | |
| 16 | + private final StringRedisTemplate stringRedisTemplate; | |
| 17 | + | |
| 18 | + /** | |
| 19 | + * 验签随机数 | |
| 20 | + * <p> | |
| 21 | + * KEY 格式:signature_nonce:%s // 参数为 随机数 | |
| 22 | + * VALUE 格式:String | |
| 23 | + * 过期时间:不固定 | |
| 24 | + */ | |
| 25 | + private static final String SIGNATURE_NONCE = "signature_nonce:%s"; | |
| 26 | + | |
| 27 | + /** | |
| 28 | + * 签名密钥 | |
| 29 | + * <p> | |
| 30 | + * KEY 格式:signature_appid:%s // 参数为 appid | |
| 31 | + * VALUE 格式:String | |
| 32 | + * 过期时间:预加载到 redis 永不过期 | |
| 33 | + */ | |
| 34 | + private static final String SIGNATURE_APPID = "signature_appid:%s"; | |
| 35 | + | |
| 36 | + public String getAppSecret(String appId) { | |
| 37 | + return stringRedisTemplate.opsForValue().get(formatAppIdKey(appId)); | |
| 38 | + } | |
| 39 | + | |
| 40 | + public String getNonce(String nonce) { | |
| 41 | + return stringRedisTemplate.opsForValue().get(formatNonceKey(nonce)); | |
| 42 | + } | |
| 43 | + | |
| 44 | + public void setNonce(String nonce, long time, TimeUnit timeUnit) { | |
| 45 | + stringRedisTemplate.opsForValue().set(formatNonceKey(nonce), nonce, time, timeUnit); | |
| 46 | + } | |
| 47 | + | |
| 48 | + private static String formatAppIdKey(String key) { | |
| 49 | + return String.format(SIGNATURE_APPID, key); | |
| 50 | + } | |
| 51 | + | |
| 52 | + private static String formatNonceKey(String key) { | |
| 53 | + return String.format(SIGNATURE_NONCE, key); | |
| 54 | + } | |
| 55 | +} | ... | ... |
yudao-framework/yudao-spring-boot-starter-protection/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
| 1 | 1 | cn.iocoder.yudao.framework.idempotent.config.YudaoIdempotentConfiguration |
| 2 | 2 | cn.iocoder.yudao.framework.lock4j.config.YudaoLock4jConfiguration |
| 3 | -cn.iocoder.yudao.framework.ratelimiter.config.YudaoRateLimiterConfiguration | |
| 4 | 3 | \ No newline at end of file |
| 4 | +cn.iocoder.yudao.framework.ratelimiter.config.YudaoRateLimiterConfiguration | |
| 5 | +cn.iocoder.yudao.framework.signature.config.YudaoSignatureAutoConfiguration | |
| 5 | 6 | \ No newline at end of file | ... | ... |
yudao-framework/yudao-spring-boot-starter-protection/src/test/java/cn/iocoder/yudao/framework/signature/core/SignatureTest.java
0 → 100644
| 1 | +package cn.iocoder.yudao.framework.signature.core; | |
| 2 | + | |
| 3 | +import cn.hutool.core.lang.Snowflake; | |
| 4 | +import cn.hutool.crypto.digest.DigestUtil; | |
| 5 | +import cn.hutool.http.HttpRequest; | |
| 6 | +import cn.hutool.http.HttpResponse; | |
| 7 | +import cn.hutool.http.HttpUtil; | |
| 8 | +import org.junit.jupiter.api.Test; | |
| 9 | + | |
| 10 | +import java.util.Map; | |
| 11 | +import java.util.SortedMap; | |
| 12 | +import java.util.TreeMap; | |
| 13 | + | |
| 14 | +/** | |
| 15 | + * {@link SignatureTest} 的单元测试 | |
| 16 | + */ | |
| 17 | +public class SignatureTest { | |
| 18 | + | |
| 19 | + @Test | |
| 20 | + public void testSignatureGet() { | |
| 21 | + String appId = "xxxxxx"; | |
| 22 | + Snowflake snowflake = new Snowflake(); | |
| 23 | + | |
| 24 | + // 验签请求头前端需传入字段 | |
| 25 | + SortedMap<String, String> headersMap = new TreeMap<>(); | |
| 26 | + headersMap.put("appId", appId); | |
| 27 | + headersMap.put("timestamp", String.valueOf(System.currentTimeMillis())); | |
| 28 | + headersMap.put("nonce", String.valueOf(snowflake.nextId())); | |
| 29 | + | |
| 30 | + // 客户端加签内容 | |
| 31 | + StringBuilder clientSignatureContent = new StringBuilder(); | |
| 32 | + // 请求头 | |
| 33 | + for (Map.Entry<String, String> entry : headersMap.entrySet()) { | |
| 34 | + clientSignatureContent.append(entry.getKey()).append(entry.getValue()); | |
| 35 | + } | |
| 36 | + // 请求 url | |
| 37 | + clientSignatureContent.append("/admin-api/infra/demo01-contact/get"); | |
| 38 | + // 请求参数 | |
| 39 | + SortedMap<String, String> paramsMap = new TreeMap<>(); | |
| 40 | + paramsMap.put("id", "100"); | |
| 41 | + paramsMap.put("name", "张三"); | |
| 42 | + StringBuilder queryString = new StringBuilder(); | |
| 43 | + for (Map.Entry<String, String> entry : paramsMap.entrySet()) { | |
| 44 | + queryString.append("&").append(entry.getKey()).append("=").append(entry.getValue()); | |
| 45 | + } | |
| 46 | + clientSignatureContent.append(queryString.substring(1)); | |
| 47 | + // 密钥 | |
| 48 | + clientSignatureContent.append("d3cbeed9baf4e68673a1f69a2445359a20022b7c28ea2933dd9db9f3a29f902b"); | |
| 49 | + System.out.println("加签内容:" + clientSignatureContent); | |
| 50 | + // 加签 | |
| 51 | + headersMap.put("sign", DigestUtil.sha256Hex(clientSignatureContent.toString())); | |
| 52 | + headersMap.put("Authorization", "Bearer xxx"); | |
| 53 | + | |
| 54 | + HttpRequest get = HttpUtil.createGet("http://localhost:48080/admin-api/infra/demo01-contact/get?id=100&name=张三"); | |
| 55 | + get.addHeaders(headersMap); | |
| 56 | + System.out.println("执行结果==" + get.execute()); | |
| 57 | + } | |
| 58 | + | |
| 59 | + @Test | |
| 60 | + public void testSignaturePost() { | |
| 61 | + String appId = "xxxxxx"; | |
| 62 | + Snowflake snowflake = new Snowflake(); | |
| 63 | + | |
| 64 | + // 验签请求头前端需传入字段 | |
| 65 | + SortedMap<String, String> headersMap = new TreeMap<>(); | |
| 66 | + headersMap.put("appId", appId); | |
| 67 | + headersMap.put("timestamp", String.valueOf(System.currentTimeMillis())); | |
| 68 | + headersMap.put("nonce", String.valueOf(snowflake.nextId())); | |
| 69 | + | |
| 70 | + // 客户端加签内容 | |
| 71 | + StringBuilder clientSignatureContent = new StringBuilder(); | |
| 72 | + // 请求头 | |
| 73 | + for (Map.Entry<String, String> entry : headersMap.entrySet()) { | |
| 74 | + clientSignatureContent.append(entry.getKey()).append(entry.getValue()); | |
| 75 | + } | |
| 76 | + // 请求 url | |
| 77 | + clientSignatureContent.append("/admin-api/infra/demo01-contact/create"); | |
| 78 | + // 请求体 | |
| 79 | + String body = "{\n" + | |
| 80 | + " \"password\": \"1\",\n" + | |
| 81 | + " \"date\": \"2024-04-24 16:28:00\",\n" + | |
| 82 | + " \"user\": {\n" + | |
| 83 | + " \"area\": \"浦东新区\",\n" + | |
| 84 | + " \"1\": \"xx\",\n" + | |
| 85 | + " \"2\": \"xx\",\n" + | |
| 86 | + " \"province\": \"上海市\",\n" + | |
| 87 | + " \"data\": {\n" + | |
| 88 | + " \"99\": \"xx\",\n" + | |
| 89 | + " \"1\": \"xx\",\n" + | |
| 90 | + " \"100\": \"xx\",\n" + | |
| 91 | + " \"2\": \"xx\",\n" + | |
| 92 | + " \"3\": \"xx\",\n" + | |
| 93 | + " \"array\": [\n" + | |
| 94 | + " {\n" + | |
| 95 | + " \"3\": \"aa\",\n" + | |
| 96 | + " \"4\": \"aa\",\n" + | |
| 97 | + " \"2\": \"aa\",\n" + | |
| 98 | + " \"1\": \"aa\"\n" + | |
| 99 | + " },\n" + | |
| 100 | + " {\n" + | |
| 101 | + " \"99\": \"aa\",\n" + | |
| 102 | + " \"100\": \"aa\",\n" + | |
| 103 | + " \"88\": \"aa\",\n" + | |
| 104 | + " \"120\": \"aa\"\n" + | |
| 105 | + " }\n" + | |
| 106 | + " ]\n" + | |
| 107 | + " },\n" + | |
| 108 | + " \"sex\": \"男\",\n" + | |
| 109 | + " \"name\": \"张三\",\n" + | |
| 110 | + " \"array\": [\n" + | |
| 111 | + " \"1\",\n" + | |
| 112 | + " \"3\",\n" + | |
| 113 | + " \"5\",\n" + | |
| 114 | + " \"2\"\n" + | |
| 115 | + " ]\n" + | |
| 116 | + " },\n" + | |
| 117 | + " \"username\": \"xiaoming\"\n" + | |
| 118 | + "}"; | |
| 119 | + clientSignatureContent.append(body); | |
| 120 | + | |
| 121 | + // 密钥 | |
| 122 | + clientSignatureContent.append("d3cbeed9baf4e68673a1f69a2445359a20022b7c28ea2933dd9db9f3a29f902b"); | |
| 123 | + System.out.println("加签内容:" + clientSignatureContent); | |
| 124 | + // 加签 | |
| 125 | + headersMap.put("sign", DigestUtil.sha256Hex(clientSignatureContent.toString())); | |
| 126 | + headersMap.put("Authorization", "Bearer xxx"); | |
| 127 | + | |
| 128 | + HttpRequest post = HttpUtil.createPost("http://localhost:48080/admin-api/infra/demo01-contact/create"); | |
| 129 | + post.addHeaders(headersMap); | |
| 130 | + post.body(body); | |
| 131 | + try (HttpResponse execute = post.execute()) { | |
| 132 | + System.out.println("执行结果==" + execute); | |
| 133 | + } | |
| 134 | + } | |
| 135 | + | |
| 136 | +} | ... | ... |