Merge remote-tracking branch 'base-version/main' into dev

This commit is contained in:
chenbowen
2025-10-17 17:45:33 +08:00
106 changed files with 4200 additions and 1377 deletions

View File

@@ -1,11 +1,20 @@
package com.zt.plat.framework.signature.config;
import com.zt.plat.framework.redis.config.ZtRedisAutoConfiguration;
import com.zt.plat.framework.signature.core.aop.ApiSignatureAspect;
import com.zt.plat.framework.signature.core.ApiSignatureVerifier;
import com.zt.plat.framework.signature.core.config.ApiSignatureProperties;
import com.zt.plat.framework.signature.core.redis.ApiSignatureRedisDAO;
import com.zt.plat.framework.signature.core.web.ApiSignatureHandlerInterceptor;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.util.CollectionUtils;
import org.springframework.web.servlet.config.annotation.InterceptorRegistration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.List;
/**
* HTTP API 签名的自动配置类
@@ -13,16 +22,47 @@ import org.springframework.data.redis.core.StringRedisTemplate;
* @author Zhougang
*/
@AutoConfiguration(after = ZtRedisAutoConfiguration.class)
@EnableConfigurationProperties(ApiSignatureProperties.class)
public class ZtApiSignatureAutoConfiguration {
@Bean
public ApiSignatureAspect signatureAspect(ApiSignatureRedisDAO signatureRedisDAO) {
return new ApiSignatureAspect(signatureRedisDAO);
}
@Bean
public ApiSignatureRedisDAO signatureRedisDAO(StringRedisTemplate stringRedisTemplate) {
return new ApiSignatureRedisDAO(stringRedisTemplate);
}
@Bean
public ApiSignatureVerifier apiSignatureVerifier(ApiSignatureRedisDAO signatureRedisDAO) {
return new ApiSignatureVerifier(signatureRedisDAO);
}
@Bean
public ApiSignatureHandlerInterceptor apiSignatureHandlerInterceptor(ApiSignatureVerifier verifier,
ApiSignatureProperties properties) {
return new ApiSignatureHandlerInterceptor(verifier, properties);
}
@Bean
public WebMvcConfigurer apiSignatureWebMvcConfigurer(ApiSignatureHandlerInterceptor interceptor,
ApiSignatureProperties properties) {
return new WebMvcConfigurer() {
@Override
public void addInterceptors(InterceptorRegistry registry) {
if (!properties.isEnabled()) {
return;
}
InterceptorRegistration registration = registry.addInterceptor(interceptor);
List<String> includePaths = properties.getIncludePaths();
if (CollectionUtils.isEmpty(includePaths)) {
registration.addPathPatterns("/**");
} else {
registration.addPathPatterns(includePaths.toArray(new String[0]));
}
List<String> excludePaths = properties.getExcludePaths();
if (!CollectionUtils.isEmpty(excludePaths)) {
registration.excludePathPatterns(excludePaths.toArray(new String[0]));
}
}
};
}
}

View File

@@ -0,0 +1,111 @@
package com.zt.plat.framework.signature.core;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestUtil;
import com.zt.plat.framework.common.exception.ServiceException;
import com.zt.plat.framework.common.exception.enums.GlobalErrorCodeConstants;
import com.zt.plat.framework.common.util.servlet.ServletUtils;
import com.zt.plat.framework.signature.core.model.ApiSignatureRule;
import com.zt.plat.framework.signature.core.redis.ApiSignatureRedisDAO;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
import static com.zt.plat.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST;
/**
* API 签名校验器
*/
@Slf4j
@RequiredArgsConstructor
public class ApiSignatureVerifier {
private final ApiSignatureRedisDAO signatureRedisDAO;
public boolean verify(ApiSignatureRule rule, HttpServletRequest request) {
// 1. 校验请求头
verifyHeaders(rule, request);
// 2. 校验 appId 对应的 appSecret 是否存在
String appId = request.getHeader(rule.getAppId());
String appSecret = signatureRedisDAO.getAppSecret(appId);
Assert.notNull(appSecret, "[appId({})] 找不到对应的 appSecret", appId);
// 3. 校验签名
String clientSignature = request.getHeader(rule.getSign());
String serverSignatureString = buildSignatureString(rule, request, appSecret);
String serverSignature = DigestUtil.sha256Hex(serverSignatureString);
if (ObjUtil.notEqual(clientSignature, serverSignature)) {
throw new ServiceException(BAD_REQUEST.getCode(), rule.getMessage());
}
// 4. 缓存 nonce防止重复请求
String nonce = request.getHeader(rule.getNonce());
if (BooleanUtil.isFalse(signatureRedisDAO.setNonce(appId, nonce, rule.getTimeout() * 2, rule.getTimeUnit()))) {
String timestamp = request.getHeader(rule.getTimestamp());
log.info("[verifySignature][appId({}) timestamp({}) nonce({}) sign({}) 存在重复请求]", appId, timestamp, nonce, clientSignature);
throw new ServiceException(GlobalErrorCodeConstants.REPEATED_REQUESTS.getCode(), "存在重复请求");
}
return true;
}
private void verifyHeaders(ApiSignatureRule rule, HttpServletRequest request) {
String appId = request.getHeader(rule.getAppId());
String timestamp = request.getHeader(rule.getTimestamp());
String nonce = request.getHeader(rule.getNonce());
String sign = request.getHeader(rule.getSign());
if (StrUtil.isBlank(appId) || StrUtil.isBlank(timestamp) || StrUtil.isBlank(sign) || StrUtil.length(nonce) < 10) {
throw new ServiceException(BAD_REQUEST.getCode(), rule.getMessage());
}
long expireTime = rule.getTimeUnit().toMillis(rule.getTimeout());
long requestTimestamp;
try {
requestTimestamp = Long.parseLong(timestamp);
} catch (NumberFormatException ex) {
throw new ServiceException(BAD_REQUEST.getCode(), rule.getMessage());
}
long timestampDisparity = Math.abs(System.currentTimeMillis() - requestTimestamp);
if (timestampDisparity > expireTime) {
throw new ServiceException(BAD_REQUEST.getCode(), rule.getMessage());
}
if (signatureRedisDAO.getNonce(appId, nonce) != null) {
throw new ServiceException(BAD_REQUEST.getCode(), rule.getMessage());
}
}
private String buildSignatureString(ApiSignatureRule rule, HttpServletRequest request, String appSecret) {
SortedMap<String, String> parameterMap = getRequestParameterMap(request);
SortedMap<String, String> headerMap = getRequestHeaderMap(rule, request);
String requestBody = StrUtil.nullToDefault(ServletUtils.getBody(request), "");
return MapUtil.join(parameterMap, "&", "=")
+ requestBody
+ MapUtil.join(headerMap, "&", "=")
+ appSecret;
}
private SortedMap<String, String> getRequestHeaderMap(ApiSignatureRule rule, HttpServletRequest request) {
SortedMap<String, String> sortedMap = new TreeMap<>();
sortedMap.put(rule.getAppId(), request.getHeader(rule.getAppId()));
sortedMap.put(rule.getTimestamp(), request.getHeader(rule.getTimestamp()));
sortedMap.put(rule.getNonce(), request.getHeader(rule.getNonce()));
return sortedMap;
}
private 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,29 +1,18 @@
package com.zt.plat.framework.signature.core.aop;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestUtil;
import com.zt.plat.framework.common.exception.ServiceException;
import com.zt.plat.framework.common.exception.enums.GlobalErrorCodeConstants;
import com.zt.plat.framework.common.util.servlet.ServletUtils;
import com.zt.plat.framework.signature.core.ApiSignatureVerifier;
import com.zt.plat.framework.signature.core.annotation.ApiSignature;
import com.zt.plat.framework.signature.core.config.ApiSignatureProperties;
import com.zt.plat.framework.signature.core.model.ApiSignatureRule;
import com.zt.plat.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 com.zt.plat.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST;
/**
* 拦截声明了 {@link ApiSignature} 注解的方法,实现签名
@@ -32,143 +21,33 @@ import static com.zt.plat.framework.common.exception.enums.GlobalErrorCodeConsta
*/
@Aspect
@Slf4j
@AllArgsConstructor
@Deprecated
public class ApiSignatureAspect {
private final ApiSignatureRedisDAO signatureRedisDAO;
private final ApiSignatureVerifier verifier;
private final ApiSignatureProperties properties;
public ApiSignatureAspect(ApiSignatureRedisDAO signatureRedisDAO) {
this(new ApiSignatureVerifier(signatureRedisDAO), new ApiSignatureProperties());
}
public ApiSignatureAspect(ApiSignatureVerifier verifier, ApiSignatureProperties properties) {
this.verifier = verifier;
this.properties = properties;
}
@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()));
HttpServletRequest request = Objects.requireNonNull(ServletUtils.getRequest());
ApiSignatureRule rule = ApiSignatureRule.from(signature, properties);
verifier.verify(rule, request);
log.debug("[beforePointCut][方法{} 参数({}) 签名校验通过]", joinPoint.getSignature(), joinPoint.getArgs());
}
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());
if (BooleanUtil.isFalse(signatureRedisDAO.setNonce(appId, nonce, signature.timeout() * 2, signature.timeUnit()))) {
String timestamp = request.getHeader(signature.timestamp());
log.info("[verifySignature][appId({}) timestamp({}) nonce({}) sign({}) 存在重复请求]", appId, timestamp, nonce, clientSignature);
throw new ServiceException(GlobalErrorCodeConstants.REPEATED_REQUESTS.getCode(), "存在重复请求");
}
ApiSignatureRule rule = ApiSignatureRule.from(signature, properties);
verifier.verify(rule, request);
return true;
}
/**
* 校验请求头加签参数
* <p>
* 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(appId, nonce) == null;
}
/**
* 构建签名字符串
* <p>
* 格式为 = 请求参数 + 请求体 + 请求头 + 密钥
*
* @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

@@ -0,0 +1,68 @@
package com.zt.plat.framework.signature.core.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* API 签名配置
*/
@Data
@ConfigurationProperties(prefix = "zt.api-signature")
public class ApiSignatureProperties {
/**
* 是否开启全局签名校验
*/
private boolean enabled = true;
/**
* 签名有效期
*/
private int timeout = 60;
/**
* 时间单位
*/
private TimeUnit timeUnit = TimeUnit.SECONDS;
/**
* 校验失败时的提示信息
*/
private String message = "签名不正确";
/**
* 请求头appId
*/
private String appId = "appId";
/**
* 请求头timestamp
*/
private String timestamp = "timestamp";
/**
* 请求头nonce
*/
private String nonce = "nonce";
/**
* 请求头sign
*/
private String sign = "sign";
/**
* 需要进行签名校验的路径,默认全量
*/
private List<String> includePaths = new ArrayList<>(Arrays.asList("/**"));
/**
* 无需签名校验的路径
*/
private List<String> excludePaths = new ArrayList<>(Arrays.asList("/error", "/swagger-ui/**", "/v3/api-docs/**"));
}

View File

@@ -0,0 +1,50 @@
package com.zt.plat.framework.signature.core.model;
import cn.hutool.core.util.StrUtil;
import com.zt.plat.framework.signature.core.annotation.ApiSignature;
import com.zt.plat.framework.signature.core.config.ApiSignatureProperties;
import lombok.Builder;
import lombok.Getter;
import java.util.concurrent.TimeUnit;
/**
* 签名校验规则
*/
@Getter
@Builder
public class ApiSignatureRule {
private final int timeout;
private final TimeUnit timeUnit;
private final String message;
private final String appId;
private final String timestamp;
private final String nonce;
private final String sign;
public static ApiSignatureRule from(ApiSignatureProperties properties) {
return ApiSignatureRule.builder()
.timeout(properties.getTimeout())
.timeUnit(properties.getTimeUnit())
.message(properties.getMessage())
.appId(properties.getAppId())
.timestamp(properties.getTimestamp())
.nonce(properties.getNonce())
.sign(properties.getSign())
.build();
}
public static ApiSignatureRule from(ApiSignature signature, ApiSignatureProperties defaults) {
return ApiSignatureRule.builder()
.timeout(signature.timeout())
.timeUnit(signature.timeUnit())
.message(StrUtil.blankToDefault(signature.message(), defaults.getMessage()))
.appId(signature.appId())
.timestamp(signature.timestamp())
.nonce(signature.nonce())
.sign(signature.sign())
.build();
}
}

View File

@@ -0,0 +1,80 @@
package com.zt.plat.framework.signature.core.web;
import com.zt.plat.framework.signature.core.ApiSignatureVerifier;
import com.zt.plat.framework.signature.core.annotation.ApiSignature;
import com.zt.plat.framework.signature.core.config.ApiSignatureProperties;
import com.zt.plat.framework.signature.core.model.ApiSignatureRule;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.CollectionUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.util.UrlPathHelper;
import java.util.List;
/**
* 全局 API 签名拦截器
*/
@RequiredArgsConstructor
public class ApiSignatureHandlerInterceptor implements HandlerInterceptor {
private final ApiSignatureVerifier verifier;
private final ApiSignatureProperties properties;
private final AntPathMatcher pathMatcher = new AntPathMatcher();
private final UrlPathHelper urlPathHelper = new UrlPathHelper();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
if (!properties.isEnabled()) {
return true;
}
if (!(handler instanceof HandlerMethod handlerMethod)) {
return true;
}
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
return true;
}
String lookupPath = urlPathHelper.getLookupPathForRequest(request);
if (shouldSkip(lookupPath)) {
return true;
}
ApiSignatureRule rule = createRule(handlerMethod);
verifier.verify(rule, request);
return true;
}
private boolean shouldSkip(String path) {
List<String> includePaths = properties.getIncludePaths();
if (!CollectionUtils.isEmpty(includePaths)) {
boolean matched = includePaths.stream().anyMatch(pattern -> pathMatcher.match(pattern, path));
if (!matched) {
return true;
}
}
List<String> excludePaths = properties.getExcludePaths();
if (!CollectionUtils.isEmpty(excludePaths)) {
for (String pattern : excludePaths) {
if (pathMatcher.match(pattern, path)) {
return true;
}
}
}
return false;
}
private ApiSignatureRule createRule(HandlerMethod handlerMethod) {
ApiSignature signature = handlerMethod.getMethodAnnotation(ApiSignature.class);
if (signature != null) {
return ApiSignatureRule.from(signature, properties);
}
signature = handlerMethod.getBeanType().getAnnotation(ApiSignature.class);
if (signature != null) {
return ApiSignatureRule.from(signature, properties);
}
return ApiSignatureRule.from(properties);
}
}

View File

@@ -1,4 +1,3 @@
com.zt.plat.framework.idempotent.config.ZtIdempotentConfiguration
com.zt.plat.framework.lock4j.config.ZtLock4jConfiguration
com.zt.plat.framework.ratelimiter.config.ZtRateLimiterConfiguration
com.zt.plat.framework.signature.config.ZtApiSignatureAutoConfiguration

View File

@@ -3,13 +3,13 @@ package com.zt.plat.framework.signature.core;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.crypto.digest.DigestUtil;
import com.zt.plat.framework.signature.core.annotation.ApiSignature;
import com.zt.plat.framework.signature.core.aop.ApiSignatureAspect;
import com.zt.plat.framework.signature.core.ApiSignatureVerifier;
import com.zt.plat.framework.signature.core.config.ApiSignatureProperties;
import com.zt.plat.framework.signature.core.model.ApiSignatureRule;
import com.zt.plat.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;
@@ -28,9 +28,6 @@ import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
public class ApiSignatureTest {
@InjectMocks
private ApiSignatureAspect apiSignatureAspect;
@Mock
private ApiSignatureRedisDAO signatureRedisDAO;
@@ -45,13 +42,16 @@ public class ApiSignatureTest {
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);
ApiSignatureProperties properties = new ApiSignatureProperties();
ApiSignatureRule apiSignature = ApiSignatureRule.builder()
.appId("appId")
.timestamp("timestamp")
.nonce("nonce")
.sign("sign")
.timeout(60)
.timeUnit(TimeUnit.SECONDS)
.message(properties.getMessage())
.build();
HttpServletRequest request = mock(HttpServletRequest.class);
when(request.getHeader(eq("appId"))).thenReturn(appId);
when(request.getHeader(eq("timestamp"))).thenReturn(String.valueOf(timestamp));
@@ -62,11 +62,13 @@ public class ApiSignatureTest {
when(request.getContentType()).thenReturn("application/json");
when(request.getReader()).thenReturn(new BufferedReader(new StringReader("test")));
// mock 方法
when(signatureRedisDAO.getNonce(eq(appId), eq(nonce))).thenReturn(null);
when(signatureRedisDAO.getAppSecret(eq(appId))).thenReturn(appSecret);
when(signatureRedisDAO.setNonce(eq(appId), eq(nonce), eq(120), eq(TimeUnit.SECONDS))).thenReturn(true);
// 调用
boolean result = apiSignatureAspect.verifySignature(apiSignature, request);
ApiSignatureVerifier verifier = new ApiSignatureVerifier(signatureRedisDAO);
boolean result = verifier.verify(apiSignature, request);
// 断言结果
assertTrue(result);
}