From e0a6e3988bf177fdb6b900eb95152e0600402844 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Tue, 4 Jun 2024 19:10:48 +0800 Subject: [PATCH] =?UTF-8?q?=E3=80=90=E4=BB=A3=E7=A0=81=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E3=80=91framework=EF=BC=9A=E4=BC=98=E5=8C=96=20HTTP=20?= =?UTF-8?q?=E8=AF=B7=E6=B1=82=E7=AD=BE=E5=90=8D=E7=9A=84=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=201=E3=80=81=E5=8D=95=E6=B5=8B=E4=BB=8E=E9=9B=86=E6=88=90?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=EF=BC=8C=E6=94=B9=E6=88=90=E5=8D=95=E5=85=83?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=202=E3=80=81SignatureAspect=20=E8=B0=83?= =?UTF-8?q?=E6=95=B4=E4=BB=A3=E7=A0=81=EF=BC=8C=E6=8F=90=E5=8D=87=E6=98=93?= =?UTF-8?q?=E8=AF=BB=E6=80=A7=203=E3=80=81sign=20=E7=AE=97=E6=B3=95?= =?UTF-8?q?=E8=B0=83=E6=95=B4=EF=BC=8C=E4=BD=BF=E7=94=A8=20querystring=20+?= =?UTF-8?q?=20body=20+=20header=20+=20appsecret=20=E6=9B=B4=E5=AE=B9?= =?UTF-8?q?=E6=98=93=E7=90=86=E8=A7=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pom.xml | 4 +- .../YudaoApiSignatureAutoConfiguration.java | 28 +++ .../YudaoSignatureAutoConfiguration.java | 27 --- .../core/annotation/ApiSignature.java | 2 +- .../core/aop/ApiSignatureAspect.java | 169 ++++++++++++++++++ .../signature/core/aop/SignatureAspect.java | 155 ---------------- ...edisDAO.java => ApiSignatureRedisDAO.java} | 36 ++-- .../framework/signature/package-info.java | 6 + ...ot.autoconfigure.AutoConfiguration.imports | 2 +- .../signature/core/ApiSignatureTest.java | 75 ++++++++ .../signature/core/SignatureTest.java | 136 -------------- 11 files changed, 301 insertions(+), 339 deletions(-) create mode 100644 yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/config/YudaoApiSignatureAutoConfiguration.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/config/YudaoSignatureAutoConfiguration.java create mode 100644 yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/aop/ApiSignatureAspect.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/aop/SignatureAspect.java rename yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/redis/{SignatureRedisDAO.java => ApiSignatureRedisDAO.java} (59%) create mode 100644 yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/package-info.java create mode 100644 yudao-framework/yudao-spring-boot-starter-protection/src/test/java/cn/iocoder/yudao/framework/signature/core/ApiSignatureTest.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-protection/src/test/java/cn/iocoder/yudao/framework/signature/core/SignatureTest.java diff --git a/yudao-framework/yudao-spring-boot-starter-protection/pom.xml b/yudao-framework/yudao-spring-boot-starter-protection/pom.xml index 46326c63aa..7e7279eb84 100644 --- a/yudao-framework/yudao-spring-boot-starter-protection/pom.xml +++ b/yudao-framework/yudao-spring-boot-starter-protection/pom.xml @@ -38,8 +38,8 @@ - org.springframework.boot - spring-boot-starter-test + cn.iocoder.boot + yudao-spring-boot-starter-test test diff --git a/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/config/YudaoApiSignatureAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/config/YudaoApiSignatureAutoConfiguration.java new file mode 100644 index 0000000000..7c6842408a --- /dev/null +++ b/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); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/config/YudaoSignatureAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/config/YudaoSignatureAutoConfiguration.java deleted file mode 100644 index 5b4b8e43e5..0000000000 --- a/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/config/YudaoSignatureAutoConfiguration.java +++ /dev/null @@ -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); - } - -} diff --git a/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/annotation/ApiSignature.java b/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/annotation/ApiSignature.java index e338ae7090..281bcec972 100644 --- a/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/annotation/ApiSignature.java +++ b/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 */ diff --git a/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/aop/ApiSignatureAspect.java b/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/aop/ApiSignatureAspect.java new file mode 100644 index 0000000000..3259dac116 --- /dev/null +++ b/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 parameterMap = getRequestParameterMap(request); // 请求头 + SortedMap 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 getRequestHeaderMap(ApiSignature signature, HttpServletRequest request) { + SortedMap 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 getRequestParameterMap(HttpServletRequest request) { + SortedMap sortedMap = new TreeMap<>(); + for (Map.Entry entry : request.getParameterMap().entrySet()) { + sortedMap.put(entry.getKey(), entry.getValue()[0]); + } + return sortedMap; + } + +} + diff --git a/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/aop/SignatureAspect.java b/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/aop/SignatureAspect.java deleted file mode 100644 index a001419f8b..0000000000 --- a/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/aop/SignatureAspect.java +++ /dev/null @@ -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 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 getRequestHeaders(ApiSignature signature, HttpServletRequest request) { - SortedMap 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 requestParams = request.getParameterMap(); - // 获取 URL 请求参数 - SortedMap sortParamsMap = new TreeMap<>(); - for (Map.Entry entry : requestParams.entrySet()) { - sortParamsMap.put(entry.getKey(), entry.getValue()[0]); - } - // 按 key 排序 - StringBuilder queryString = new StringBuilder(); - for (Map.Entry entry : sortParamsMap.entrySet()) { - queryString.append("&").append(entry.getKey()).append("=").append(entry.getValue()); - } - return queryString.substring(1); - } - -} - diff --git a/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/redis/SignatureRedisDAO.java b/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/redis/ApiSignatureRedisDAO.java similarity index 59% rename from yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/redis/SignatureRedisDAO.java rename to yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/redis/ApiSignatureRedisDAO.java index 326e238ee9..f4aa84910d 100644 --- a/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/redis/SignatureRedisDAO.java +++ b/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; /** * 验签随机数 - *

+ * * 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"; /** * 签名密钥 - *

- * 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); + } + } diff --git a/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/package-info.java b/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/package-info.java new file mode 100644 index 0000000000..4ebd87afb6 --- /dev/null +++ b/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 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)); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-protection/src/test/java/cn/iocoder/yudao/framework/signature/core/SignatureTest.java b/yudao-framework/yudao-spring-boot-starter-protection/src/test/java/cn/iocoder/yudao/framework/signature/core/SignatureTest.java deleted file mode 100644 index a253cd51a2..0000000000 --- a/yudao-framework/yudao-spring-boot-starter-protection/src/test/java/cn/iocoder/yudao/framework/signature/core/SignatureTest.java +++ /dev/null @@ -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 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 entry : headersMap.entrySet()) { - clientSignatureContent.append(entry.getKey()).append(entry.getValue()); - } - // 请求 url - clientSignatureContent.append("/admin-api/infra/demo01-contact/get"); - // 请求参数 - SortedMap paramsMap = new TreeMap<>(); - paramsMap.put("id", "100"); - paramsMap.put("name", "张三"); - StringBuilder queryString = new StringBuilder(); - for (Map.Entry 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 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 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); - } - } - -}