Commit e0a6e3988bf177fdb6b900eb95152e0600402844

Authored by YunaiV
1 parent 1e391d62

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

1、单测从集成测试,改成单元测试
2、SignatureAspect 调整代码,提升易读性
3、sign 算法调整,使用 querystring + body + header + appsecret 更容易理解
Showing 9 changed files with 187 additions and 225 deletions
yudao-framework/yudao-spring-boot-starter-protection/pom.xml
@@ -38,8 +38,8 @@ @@ -38,8 +38,8 @@
38 38
39 <!-- Test 测试相关 --> 39 <!-- Test 测试相关 -->
40 <dependency> 40 <dependency>
41 - <groupId>org.springframework.boot</groupId>  
42 - <artifactId>spring-boot-starter-test</artifactId> 41 + <groupId>cn.iocoder.boot</groupId>
  42 + <artifactId>yudao-spring-boot-starter-test</artifactId>
43 <scope>test</scope> 43 <scope>test</scope>
44 </dependency> 44 </dependency>
45 </dependencies> 45 </dependencies>
yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/config/YudaoSignatureAutoConfiguration.java renamed to yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/config/YudaoApiSignatureAutoConfiguration.java
1 package cn.iocoder.yudao.framework.signature.config; 1 package cn.iocoder.yudao.framework.signature.config;
2 2
3 import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration; 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; 4 +import cn.iocoder.yudao.framework.signature.core.aop.ApiSignatureAspect;
  5 +import cn.iocoder.yudao.framework.signature.core.redis.ApiSignatureRedisDAO;
6 import org.springframework.boot.autoconfigure.AutoConfiguration; 6 import org.springframework.boot.autoconfigure.AutoConfiguration;
7 import org.springframework.context.annotation.Bean; 7 import org.springframework.context.annotation.Bean;
8 import org.springframework.data.redis.core.StringRedisTemplate; 8 import org.springframework.data.redis.core.StringRedisTemplate;
9 9
10 /** 10 /**
  11 + * HTTP API 签名的自动配置类
  12 + *
11 * @author Zhougang 13 * @author Zhougang
12 */ 14 */
13 @AutoConfiguration(after = YudaoRedisAutoConfiguration.class) 15 @AutoConfiguration(after = YudaoRedisAutoConfiguration.class)
14 -public class YudaoSignatureAutoConfiguration { 16 +public class YudaoApiSignatureAutoConfiguration {
15 17
16 @Bean 18 @Bean
17 - public SignatureAspect signatureAspect(SignatureRedisDAO signatureRedisDAO) {  
18 - return new SignatureAspect(signatureRedisDAO); 19 + public ApiSignatureAspect signatureAspect(ApiSignatureRedisDAO signatureRedisDAO) {
  20 + return new ApiSignatureAspect(signatureRedisDAO);
19 } 21 }
20 22
21 @Bean 23 @Bean
22 - @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")  
23 - public SignatureRedisDAO signatureRedisDAO(StringRedisTemplate stringRedisTemplate) {  
24 - return new SignatureRedisDAO(stringRedisTemplate); 24 + public ApiSignatureRedisDAO signatureRedisDAO(StringRedisTemplate stringRedisTemplate) {
  25 + return new ApiSignatureRedisDAO(stringRedisTemplate);
25 } 26 }
26 27
27 } 28 }
yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/annotation/ApiSignature.java
@@ -7,7 +7,7 @@ import java.util.concurrent.TimeUnit; @@ -7,7 +7,7 @@ import java.util.concurrent.TimeUnit;
7 7
8 8
9 /** 9 /**
10 - * 签名注解 10 + * HTTP API 签名注解
11 * 11 *
12 * @author Zhougang 12 * @author Zhougang
13 */ 13 */
yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/aop/SignatureAspect.java renamed to yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/aop/ApiSignatureAspect.java
1 package cn.iocoder.yudao.framework.signature.core.aop; 1 package cn.iocoder.yudao.framework.signature.core.aop;
2 2
3 -import cn.hutool.core.collection.CollUtil;  
4 import cn.hutool.core.lang.Assert; 3 import cn.hutool.core.lang.Assert;
  4 +import cn.hutool.core.map.MapUtil;
  5 +import cn.hutool.core.util.ObjUtil;
5 import cn.hutool.core.util.StrUtil; 6 import cn.hutool.core.util.StrUtil;
6 -import cn.hutool.crypto.SignUtil; 7 +import cn.hutool.crypto.digest.DigestUtil;
7 import cn.iocoder.yudao.framework.common.exception.ServiceException; 8 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; 9 import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
10 import cn.iocoder.yudao.framework.signature.core.annotation.ApiSignature; 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; 11 +import cn.iocoder.yudao.framework.signature.core.redis.ApiSignatureRedisDAO;
13 import jakarta.servlet.http.HttpServletRequest; 12 import jakarta.servlet.http.HttpServletRequest;
14 import lombok.AllArgsConstructor; 13 import lombok.AllArgsConstructor;
15 import lombok.extern.slf4j.Slf4j; 14 import lombok.extern.slf4j.Slf4j;
@@ -17,12 +16,12 @@ import org.aspectj.lang.JoinPoint; @@ -17,12 +16,12 @@ import org.aspectj.lang.JoinPoint;
17 import org.aspectj.lang.annotation.Aspect; 16 import org.aspectj.lang.annotation.Aspect;
18 import org.aspectj.lang.annotation.Before; 17 import org.aspectj.lang.annotation.Before;
19 18
20 -import java.nio.charset.StandardCharsets;  
21 import java.util.Map; 19 import java.util.Map;
22 import java.util.Objects; 20 import java.util.Objects;
23 import java.util.SortedMap; 21 import java.util.SortedMap;
24 import java.util.TreeMap; 22 import java.util.TreeMap;
25 -import java.util.concurrent.TimeUnit; 23 +
  24 +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST;
26 25
27 /** 26 /**
28 * 拦截声明了 {@link ApiSignature} 注解的方法,实现签名 27 * 拦截声明了 {@link ApiSignature} 注解的方法,实现签名
@@ -32,59 +31,62 @@ import java.util.concurrent.TimeUnit; @@ -32,59 +31,62 @@ import java.util.concurrent.TimeUnit;
32 @Aspect 31 @Aspect
33 @Slf4j 32 @Slf4j
34 @AllArgsConstructor 33 @AllArgsConstructor
35 -public class SignatureAspect { 34 +public class ApiSignatureAspect {
36 35
37 - private final SignatureRedisDAO signatureRedisDAO; 36 + private final ApiSignatureRedisDAO signatureRedisDAO;
38 37
39 @Before("@annotation(signature)") 38 @Before("@annotation(signature)")
40 public void beforePointCut(JoinPoint joinPoint, ApiSignature signature) { 39 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); 40 + // 1. 验证通过,直接结束
  41 + if (verifySignature(signature, Objects.requireNonNull(ServletUtils.getRequest()))) {
  42 + return;
47 } 43 }
  44 +
  45 + // 2. 验证不通过,抛出异常
  46 + log.error("[beforePointCut][方法{} 参数({}) 签名失败]", joinPoint.getSignature().toString(),
  47 + joinPoint.getArgs());
  48 + throw new ServiceException(BAD_REQUEST.getCode(),
  49 + StrUtil.blankToDefault(signature.message(), BAD_REQUEST.getMsg()));
48 } 50 }
49 51
50 - private boolean verifySignature(ApiSignature signature, HttpServletRequest request) { 52 + public boolean verifySignature(ApiSignature signature, HttpServletRequest request) {
  53 + // 1.1 校验 Header
51 if (!verifyHeaders(signature, request)) { 54 if (!verifyHeaders(signature, request)) {
52 return false; 55 return false;
53 } 56 }
54 - // 校验 appId 是否能获取到对应的 appSecret 57 + // 1.2 校验 appId 是否能获取到对应的 appSecret
55 String appId = request.getHeader(signature.appId()); 58 String appId = request.getHeader(signature.appId());
56 String appSecret = signatureRedisDAO.getAppSecret(appId); 59 String appSecret = signatureRedisDAO.getAppSecret(appId);
57 Assert.notNull(appSecret, "[appId({})] 找不到对应的 appSecret", appId); 60 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)) { 61 +
  62 + // 2. 校验签名【重要!】
  63 + String clientSignature = request.getHeader(signature.sign()); // 客户端签名
  64 + String serverSignatureString = buildSignatureString(signature, request, appSecret); // 服务端签名字符串
  65 + String serverSignature = DigestUtil.sha256Hex(serverSignatureString); // 服务端签名
  66 + if (ObjUtil.notEqual(clientSignature, serverSignature)) {
69 return false; 67 return false;
70 } 68 }
71 - String nonce = headersMap.get(signature.nonce());  
72 - // 将 nonce 记入缓存,防止重复使用(重点二:此处需要将 ttl 设定为允许 timestamp 时间差的值 x 2 )  
73 - signatureRedisDAO.setNonce(nonce, signature.timeout() * 2L, signature.timeUnit()); 69 +
  70 + // 3. 将 nonce 记入缓存,防止重复使用(重点二:此处需要将 ttl 设定为允许 timestamp 时间差的值 x 2 )
  71 + String nonce = request.getHeader(signature.nonce());
  72 + signatureRedisDAO.setNonce(nonce, signature.timeout() * 2, signature.timeUnit());
74 return true; 73 return true;
75 } 74 }
76 75
77 /** 76 /**
78 * 校验请求头加签参数 77 * 校验请求头加签参数
79 - * 1.appId 是否为空  
80 - * 2.timestamp 是否为空,请求是否已经超时,默认 10 分钟  
81 - * 3.nonce 是否为空,随机数是否 10 位以上,是否在规定时间内已经访问过了  
82 - * 4.sign 是否为空 78 + *
  79 + * 1. appId 是否为空
  80 + * 2. timestamp 是否为空,请求是否已经超时,默认 10 分钟
  81 + * 3. nonce 是否为空,随机数是否 10 位以上,是否在规定时间内已经访问过了
  82 + * 4. sign 是否为空
83 * 83 *
84 * @param signature signature 84 * @param signature signature
85 * @param request request 85 * @param request request
  86 + * @return 是否校验 Header 通过
86 */ 87 */
87 private boolean verifyHeaders(ApiSignature signature, HttpServletRequest request) { 88 private boolean verifyHeaders(ApiSignature signature, HttpServletRequest request) {
  89 + // 1. 非空校验
88 String appId = request.getHeader(signature.appId()); 90 String appId = request.getHeader(signature.appId());
89 if (StrUtil.isBlank(appId)) { 91 if (StrUtil.isBlank(appId)) {
90 return false; 92 return false;
@@ -94,32 +96,54 @@ public class SignatureAspect { @@ -94,32 +96,54 @@ public class SignatureAspect {
94 return false; 96 return false;
95 } 97 }
96 String nonce = request.getHeader(signature.nonce()); 98 String nonce = request.getHeader(signature.nonce());
97 - if (StrUtil.isBlank(nonce) || StrUtil.length(nonce) < 10) { 99 + if (StrUtil.length(nonce) < 10) {
98 return false; 100 return false;
99 } 101 }
100 String sign = request.getHeader(signature.sign()); 102 String sign = request.getHeader(signature.sign());
101 if (StrUtil.isBlank(sign)) { 103 if (StrUtil.isBlank(sign)) {
102 return false; 104 return false;
103 } 105 }
104 - // 其他合法性校验 106 +
  107 + // 2. 检查 timestamp 是否超出允许的范围 (重点一:此处需要取绝对值)
105 long expireTime = signature.timeUnit().toMillis(signature.timeout()); 108 long expireTime = signature.timeUnit().toMillis(signature.timeout());
106 long requestTimestamp = Long.parseLong(timestamp); 109 long requestTimestamp = Long.parseLong(timestamp);
107 - // 检查 timestamp 是否超出允许的范围 (重点一:此处需要取绝对值)  
108 long timestampDisparity = Math.abs(System.currentTimeMillis() - requestTimestamp); 110 long timestampDisparity = Math.abs(System.currentTimeMillis() - requestTimestamp);
109 if (timestampDisparity > expireTime) { 111 if (timestampDisparity > expireTime) {
110 return false; 112 return false;
111 } 113 }
112 - String cacheNonce = signatureRedisDAO.getNonce(nonce);  
113 - return StrUtil.isBlank(cacheNonce); 114 +
  115 + // 3. 检查 nonce 是否存在,有且仅能使用一次
  116 + return signatureRedisDAO.getNonce(nonce) == null;
  117 + }
  118 +
  119 + /**
  120 + * 构建签名字符串
  121 + *
  122 + * 格式为 = 请求参数 + 请求体 + 请求头 + 密钥
  123 + *
  124 + * @param signature signature
  125 + * @param request request
  126 + * @param appSecret appSecret
  127 + * @return 签名字符串
  128 + */
  129 + private String buildSignatureString(ApiSignature signature, HttpServletRequest request, String appSecret) {
  130 + SortedMap<String, String> parameterMap = getRequestParameterMap(request); // 请求头
  131 + SortedMap<String, String> headerMap = getRequestHeaderMap(signature, request); // 请求参数
  132 + String requestBody = StrUtil.nullToDefault(ServletUtils.getBody(request), ""); // 请求体
  133 + return MapUtil.join(parameterMap, "&", "=")
  134 + + requestBody
  135 + + MapUtil.join(headerMap, "&", "=")
  136 + + appSecret;
114 } 137 }
115 138
116 /** 139 /**
117 - * 获取请求头加签参数 140 + * 获取请求头加签参数 Map
118 * 141 *
119 - * @param request request 142 + * @param request 请求
  143 + * @param signature 签名注解
120 * @return signature params 144 * @return signature params
121 */ 145 */
122 - private SortedMap<String, String> getRequestHeaders(ApiSignature signature, HttpServletRequest request) { 146 + private static SortedMap<String, String> getRequestHeaderMap(ApiSignature signature, HttpServletRequest request) {
123 SortedMap<String, String> sortedMap = new TreeMap<>(); 147 SortedMap<String, String> sortedMap = new TreeMap<>();
124 sortedMap.put(signature.appId(), request.getHeader(signature.appId())); 148 sortedMap.put(signature.appId(), request.getHeader(signature.appId()));
125 sortedMap.put(signature.timestamp(), request.getHeader(signature.timestamp())); 149 sortedMap.put(signature.timestamp(), request.getHeader(signature.timestamp()));
@@ -128,27 +152,17 @@ public class SignatureAspect { @@ -128,27 +152,17 @@ public class SignatureAspect {
128 } 152 }
129 153
130 /** 154 /**
131 - * 获取 URL 参数 155 + * 获取请求参数 Map
132 * 156 *
133 - * @param request request 157 + * @param request 请求
134 * @return queryParams 158 * @return queryParams
135 */ 159 */
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()); 160 + private static SortedMap<String, String> getRequestParameterMap(HttpServletRequest request) {
  161 + SortedMap<String, String> sortedMap = new TreeMap<>();
  162 + for (Map.Entry<String, String[]> entry : request.getParameterMap().entrySet()) {
  163 + sortedMap.put(entry.getKey(), entry.getValue()[0]);
150 } 164 }
151 - return queryString.substring(1); 165 + return sortedMap;
152 } 166 }
153 167
154 } 168 }
yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/redis/SignatureRedisDAO.java renamed to yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/redis/ApiSignatureRedisDAO.java
@@ -6,50 +6,52 @@ import org.springframework.data.redis.core.StringRedisTemplate; @@ -6,50 +6,52 @@ import org.springframework.data.redis.core.StringRedisTemplate;
6 import java.util.concurrent.TimeUnit; 6 import java.util.concurrent.TimeUnit;
7 7
8 /** 8 /**
9 - * API 签名 Redis DAO 9 + * HTTP API 签名 Redis DAO
10 * 10 *
11 * @author Zhougang 11 * @author Zhougang
12 */ 12 */
13 @AllArgsConstructor 13 @AllArgsConstructor
14 -public class SignatureRedisDAO { 14 +public class ApiSignatureRedisDAO {
15 15
16 private final StringRedisTemplate stringRedisTemplate; 16 private final StringRedisTemplate stringRedisTemplate;
17 17
18 /** 18 /**
19 * 验签随机数 19 * 验签随机数
20 - * <p> 20 + *
21 * KEY 格式:signature_nonce:%s // 参数为 随机数 21 * KEY 格式:signature_nonce:%s // 参数为 随机数
22 * VALUE 格式:String 22 * VALUE 格式:String
23 * 过期时间:不固定 23 * 过期时间:不固定
24 */ 24 */
25 - private static final String SIGNATURE_NONCE = "signature_nonce:%s"; 25 + private static final String SIGNATURE_NONCE = "api_signature_nonce:%s";
26 26
27 /** 27 /**
28 * 签名密钥 28 * 签名密钥
29 - * <p>  
30 - * KEY 格式:signature_appid:%s // 参数为 appid 29 + *
  30 + * HASH 结构
  31 + * KEY 格式:%s // 参数为 appid
31 * VALUE 格式:String 32 * VALUE 格式:String
32 - * 过期时间:预加载到 redis 永不过期 33 + * 过期时间:永不过期(预加载到 Redis)
33 */ 34 */
34 - private static final String SIGNATURE_APPID = "signature_appid:%s"; 35 + private static final String SIGNATURE_APPID = "api_signature_app";
35 36
36 - public String getAppSecret(String appId) {  
37 - return stringRedisTemplate.opsForValue().get(formatAppIdKey(appId));  
38 - } 37 + // ========== 验签随机数 ==========
39 38
40 public String getNonce(String nonce) { 39 public String getNonce(String nonce) {
41 return stringRedisTemplate.opsForValue().get(formatNonceKey(nonce)); 40 return stringRedisTemplate.opsForValue().get(formatNonceKey(nonce));
42 } 41 }
43 42
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); 43 + public void setNonce(String nonce, int time, TimeUnit timeUnit) {
  44 + stringRedisTemplate.opsForValue().set(formatNonceKey(nonce), "", time, timeUnit);
50 } 45 }
51 46
52 private static String formatNonceKey(String key) { 47 private static String formatNonceKey(String key) {
53 return String.format(SIGNATURE_NONCE, key); 48 return String.format(SIGNATURE_NONCE, key);
54 } 49 }
  50 +
  51 + // ========== 签名密钥 ==========
  52 +
  53 + public String getAppSecret(String appId) {
  54 + return (String) stringRedisTemplate.opsForHash().get(SIGNATURE_APPID, appId);
  55 + }
  56 +
55 } 57 }
yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/package-info.java 0 → 100644
  1 +/**
  2 + * HTTP API 签名,校验安全性
  3 + *
  4 + * @see <a href="https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=4_3>微信支付 —— 安全规范</a>
  5 + */
  6 +package cn.iocoder.yudao.framework.signature;
0 \ No newline at end of file 7 \ No newline at end of file
yudao-framework/yudao-spring-boot-starter-protection/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
1 cn.iocoder.yudao.framework.idempotent.config.YudaoIdempotentConfiguration 1 cn.iocoder.yudao.framework.idempotent.config.YudaoIdempotentConfiguration
2 cn.iocoder.yudao.framework.lock4j.config.YudaoLock4jConfiguration 2 cn.iocoder.yudao.framework.lock4j.config.YudaoLock4jConfiguration
3 cn.iocoder.yudao.framework.ratelimiter.config.YudaoRateLimiterConfiguration 3 cn.iocoder.yudao.framework.ratelimiter.config.YudaoRateLimiterConfiguration
4 -cn.iocoder.yudao.framework.signature.config.YudaoSignatureAutoConfiguration  
5 \ No newline at end of file 4 \ No newline at end of file
  5 +cn.iocoder.yudao.framework.signature.config.YudaoApiSignatureAutoConfiguration
6 \ No newline at end of file 6 \ No newline at end of file
yudao-framework/yudao-spring-boot-starter-protection/src/test/java/cn/iocoder/yudao/framework/signature/core/ApiSignatureTest.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.signature.core;
  2 +
  3 +import cn.hutool.core.map.MapUtil;
  4 +import cn.hutool.core.util.IdUtil;
  5 +import cn.hutool.crypto.digest.DigestUtil;
  6 +import cn.iocoder.yudao.framework.signature.core.annotation.ApiSignature;
  7 +import cn.iocoder.yudao.framework.signature.core.aop.ApiSignatureAspect;
  8 +import cn.iocoder.yudao.framework.signature.core.redis.ApiSignatureRedisDAO;
  9 +import jakarta.servlet.http.HttpServletRequest;
  10 +import org.junit.jupiter.api.Test;
  11 +import org.junit.jupiter.api.extension.ExtendWith;
  12 +import org.mockito.InjectMocks;
  13 +import org.mockito.Mock;
  14 +import org.mockito.junit.jupiter.MockitoExtension;
  15 +
  16 +import java.io.BufferedReader;
  17 +import java.io.IOException;
  18 +import java.io.StringReader;
  19 +import java.util.concurrent.TimeUnit;
  20 +
  21 +import static org.junit.jupiter.api.Assertions.assertTrue;
  22 +import static org.mockito.ArgumentMatchers.eq;
  23 +import static org.mockito.Mockito.*;
  24 +
  25 +/**
  26 + * {@link ApiSignatureTest} 的单元测试
  27 + */
  28 +@ExtendWith(MockitoExtension.class)
  29 +public class ApiSignatureTest {
  30 +
  31 + @InjectMocks
  32 + private ApiSignatureAspect apiSignatureAspect;
  33 +
  34 + @Mock
  35 + private ApiSignatureRedisDAO signatureRedisDAO;
  36 +
  37 + @Test
  38 + public void testSignatureGet() throws IOException {
  39 + // 搞一个签名
  40 + Long timestamp = System.currentTimeMillis();
  41 + String nonce = IdUtil.randomUUID();
  42 + String appId = "xxxxxx";
  43 + String appSecret = "yyyyyy";
  44 + String signString = "k1=v1&v1=k1testappId=xxxxxx&nonce=" + nonce + "&timestamp=" + timestamp + "yyyyyy";
  45 + String sign = DigestUtil.sha256Hex(signString);
  46 +
  47 + // 准备参数
  48 + ApiSignature apiSignature = mock(ApiSignature.class);
  49 + when(apiSignature.appId()).thenReturn("appId");
  50 + when(apiSignature.timestamp()).thenReturn("timestamp");
  51 + when(apiSignature.nonce()).thenReturn("nonce");
  52 + when(apiSignature.sign()).thenReturn("sign");
  53 + when(apiSignature.timeout()).thenReturn(60);
  54 + when(apiSignature.timeUnit()).thenReturn(TimeUnit.SECONDS);
  55 + HttpServletRequest request = mock(HttpServletRequest.class);
  56 + when(request.getHeader(eq("appId"))).thenReturn(appId);
  57 + when(request.getHeader(eq("timestamp"))).thenReturn(String.valueOf(timestamp));
  58 + when(request.getHeader(eq("nonce"))).thenReturn(nonce);
  59 + when(request.getHeader(eq("sign"))).thenReturn(sign);
  60 + when(request.getParameterMap()).thenReturn(MapUtil.<String, String[]>builder()
  61 + .put("v1", new String[]{"k1"}).put("k1", new String[]{"v1"}).build());
  62 + when(request.getContentType()).thenReturn("application/json");
  63 + when(request.getReader()).thenReturn(new BufferedReader(new StringReader("test")));
  64 + // mock 方法
  65 + when(signatureRedisDAO.getAppSecret(eq(appId))).thenReturn(appSecret);
  66 +
  67 + // 调用
  68 + boolean result = apiSignatureAspect.verifySignature(apiSignature, request);
  69 + // 断言结果
  70 + assertTrue(result);
  71 + // 断言调用
  72 + verify(signatureRedisDAO).setNonce(eq(nonce), eq(120), eq(TimeUnit.SECONDS));
  73 + }
  74 +
  75 +}
yudao-framework/yudao-spring-boot-starter-protection/src/test/java/cn/iocoder/yudao/framework/signature/core/SignatureTest.java deleted
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 -}