Prechádzať zdrojové kódy

【代码优化】framework:优化 HTTP 请求签名的实现
1、单测从集成测试,改成单元测试
2、SignatureAspect 调整代码,提升易读性
3、sign 算法调整,使用 querystring + body + header + appsecret 更容易理解

YunaiV 1 rok pred
rodič
commit
e0a6e3988b
11 zmenil súbory, kde vykonal 301 pridanie a 339 odobranie
  1. 2 2
      yudao-framework/yudao-spring-boot-starter-protection/pom.xml
  2. 28 0
      yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/config/YudaoApiSignatureAutoConfiguration.java
  3. 0 27
      yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/config/YudaoSignatureAutoConfiguration.java
  4. 1 1
      yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/annotation/ApiSignature.java
  5. 169 0
      yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/aop/ApiSignatureAspect.java
  6. 0 155
      yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/aop/SignatureAspect.java
  7. 19 17
      yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/redis/ApiSignatureRedisDAO.java
  8. 6 0
      yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/package-info.java
  9. 1 1
      yudao-framework/yudao-spring-boot-starter-protection/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
  10. 75 0
      yudao-framework/yudao-spring-boot-starter-protection/src/test/java/cn/iocoder/yudao/framework/signature/core/ApiSignatureTest.java
  11. 0 136
      yudao-framework/yudao-spring-boot-starter-protection/src/test/java/cn/iocoder/yudao/framework/signature/core/SignatureTest.java

+ 2 - 2
yudao-framework/yudao-spring-boot-starter-protection/pom.xml

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

+ 28 - 0
yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/config/YudaoApiSignatureAutoConfiguration.java

@@ -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);
+    }
+
+}

+ 0 - 27
yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/config/YudaoSignatureAutoConfiguration.java

@@ -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);
-    }
-
-}

+ 1 - 1
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;
 
 
 /**
- * 签名注解
+ * HTTP API 签名注解
  *
  * @author Zhougang
  */

+ 169 - 0
yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/aop/ApiSignatureAspect.java

@@ -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;
+    }
+
+}
+

+ 0 - 155
yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/aop/SignatureAspect.java

@@ -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);
-    }
-
-}
-

+ 19 - 17
yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/redis/SignatureRedisDAO.java → 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;
 import java.util.concurrent.TimeUnit;
 
 /**
- * API 签名 Redis DAO
+ * HTTP API 签名 Redis DAO
  *
  * @author Zhougang
  */
 @AllArgsConstructor
-public class SignatureRedisDAO {
+public class ApiSignatureRedisDAO {
 
     private final StringRedisTemplate stringRedisTemplate;
 
     /**
      * 验签随机数
-     * <p>
+     *
      * KEY 格式:signature_nonce:%s // 参数为 随机数
      * 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
-     * 过期时间:预加载到 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) {
         return stringRedisTemplate.opsForValue().get(formatNonceKey(nonce));
     }
 
-    public void setNonce(String nonce, long time, TimeUnit timeUnit) {
-        stringRedisTemplate.opsForValue().set(formatNonceKey(nonce), nonce, time, timeUnit);
-    }
-
-    private static String formatAppIdKey(String key) {
-        return String.format(SIGNATURE_APPID, key);
+    public void setNonce(String nonce, int time, TimeUnit timeUnit) {
+        stringRedisTemplate.opsForValue().set(formatNonceKey(nonce), "", time, timeUnit);
     }
 
     private static String formatNonceKey(String key) {
         return String.format(SIGNATURE_NONCE, key);
     }
+
+    // ========== 签名密钥 ==========
+
+    public String getAppSecret(String appId) {
+        return (String) stringRedisTemplate.opsForHash().get(SIGNATURE_APPID, appId);
+    }
+
 }

+ 6 - 0
yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/package-info.java

@@ -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;

+ 1 - 1
yudao-framework/yudao-spring-boot-starter-protection/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

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

+ 75 - 0
yudao-framework/yudao-spring-boot-starter-protection/src/test/java/cn/iocoder/yudao/framework/signature/core/ApiSignatureTest.java

@@ -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));
+    }
+
+}

+ 0 - 136
yudao-framework/yudao-spring-boot-starter-protection/src/test/java/cn/iocoder/yudao/framework/signature/core/SignatureTest.java

@@ -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);
-        }
-    }
-
-}