From 8e5584821e722498b21523a38efe22eef57ae638 Mon Sep 17 00:00:00 2001 From: zhougang <921366807@qq.com> Date: Sun, 19 May 2024 15:34:45 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=E3=80=90=E6=96=B0=E5=A2=9E=E3=80=91=20prot?= =?UTF-8?q?ection=20=E6=A8=A1=E5=9D=97=E6=96=B0=E5=A2=9E=20signature=20?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0=20API=20=E7=AD=BE=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pom.xml | 9 +- .../YudaoSignatureAutoConfiguration.java | 27 +++ .../signature/core/annotation/Signature.java | 57 ++++++ .../signature/core/aop/SignatureAspect.java | 170 ++++++++++++++++++ .../core/redis/SignatureRedisDAO.java | 56 ++++++ .../signature/core/SignatureTest.java | 136 ++++++++++++++ .../core/filter/CacheRequestBodyWrapper.java | 4 + 7 files changed, 458 insertions(+), 1 deletion(-) create 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/annotation/Signature.java create mode 100644 yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/aop/SignatureAspect.java create mode 100644 yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/redis/SignatureRedisDAO.java create 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 bbb5b12eb..025bb7591 100644 --- a/yudao-framework/yudao-spring-boot-starter-protection/pom.xml +++ b/yudao-framework/yudao-spring-boot-starter-protection/pom.xml @@ -12,7 +12,7 @@ jar ${project.artifactId} - 服务保证,提供分布式锁、幂等、限流、熔断等等功能 + 服务保证,提供分布式锁、幂等、限流、熔断、API签名等等功能 https://github.com/YunaiV/ruoyi-vue-pro @@ -35,6 +35,13 @@ lock4j-redisson-spring-boot-starter true + + + + org.springframework.boot + spring-boot-starter-test + test + 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 new file mode 100644 index 000000000..5b4b8e43e --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/config/YudaoSignatureAutoConfiguration.java @@ -0,0 +1,27 @@ +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/Signature.java b/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/annotation/Signature.java new file mode 100644 index 000000000..1b7e12786 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/annotation/Signature.java @@ -0,0 +1,57 @@ +package cn.iocoder.yudao.framework.signature.core.annotation; + +import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; + +import java.lang.annotation.*; + + +/** + * 签名注解 + * + * @author Zhougang + */ +@Inherited +@Documented +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface Signature { + + /** + * 同一个请求多长时间内有效 默认 10分钟 + */ + long expireTime() default 600000L; + + // ========================== 签名参数 ========================== + + /** + * 提示信息,签名失败的提示 + * + * @see GlobalErrorCodeConstants#BAD_REQUEST + */ + String message() default "签名不正确"; // 为空时,使用 BAD_REQUEST 错误提示 + + /** + * 签名字段:appId 应用ID + */ + String appId() default "appId"; + + /** + * 签名字段:timestamp 时间戳 + */ + String timestamp() default "timestamp"; + + /** + * 签名字段:nonce 随机数,10 位以上 + */ + String nonce() default "nonce"; + + /** + * sign 客户端签名 + */ + String sign() default "sign"; + + /** + * url 客户端不需要传递,但是可以用来加签(如: /{id} 带有动态参数的 url ,如果没有动态参数可设置为 false 不进行加签) + */ + boolean urlEnable() default true; +} 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 new file mode 100644 index 000000000..dc1510465 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/aop/SignatureAspect.java @@ -0,0 +1,170 @@ +package cn.iocoder.yudao.framework.signature.core.aop; + +import cn.hutool.core.collection.CollUtil; +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.Signature; +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 org.springframework.util.Assert; + +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 Signature} 注解的方法,实现签名 + * + * @author Zhougang + */ +@Aspect +@Slf4j +@AllArgsConstructor +public class SignatureAspect { + + private final SignatureRedisDAO signatureRedisDAO; + + @Before("@annotation(signature)") + public void beforePointCut(JoinPoint joinPoint, Signature signature) { + if (!verifySignature(signature, Objects.requireNonNull(ServletUtils.getRequest()))) { + log.info("[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(Signature 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, "找不到对应的 appSecret"); + // 请求头 + SortedMap headersMap = getRequestHeaders(signature, request); + // 如:/user/{id} url 带有动态参数的情况 + String urlParams = signature.urlEnable() ? request.getServletPath() : ""; + // 请求参数 + String requestParams = getRequestParams(request); + // 请求体 + String requestBody = getRequestBody(request); + // 生成服务端签名 + String serverSignature = SignUtil.signParamsSha256(headersMap, + urlParams + 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.expireTime(), TimeUnit.MILLISECONDS); + return true; + } + + /** + * 校验请求头加签参数 + * 1.appId 是否为空 + * 2.timestamp 是否为空,请求是否已经超时,默认 10 分钟 + * 3.nonce 是否为空,随机数是否 10 位以上,是否在规定时间内已经访问过了 + * 4.sign 是否为空 + * + * @param signature signature + * @param request request + */ + private boolean verifyHeaders(Signature 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) || nonce.length() < 10) { + return false; + } + String sign = request.getHeader(signature.sign()); + if (StrUtil.isBlank(sign)) { + return false; + } + // 其他合法性校验 + long expireTime = signature.expireTime(); + 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(Signature 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); + } + + /** + * 获取请求体参数 + * + * @param request request + * @return body + */ + private String getRequestBody(HttpServletRequest request) { + CacheRequestBodyWrapper requestWrapper = new CacheRequestBodyWrapper(request); + // 获取 body + return new String(requestWrapper.getBody(), StandardCharsets.UTF_8); + } + +} + 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/SignatureRedisDAO.java new file mode 100644 index 000000000..d00fe7f8d --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/redis/SignatureRedisDAO.java @@ -0,0 +1,56 @@ +package cn.iocoder.yudao.framework.signature.core.redis; + +import lombok.AllArgsConstructor; +import org.springframework.data.redis.core.StringRedisTemplate; + +import java.util.concurrent.TimeUnit; + +/** + * API 签名 Redis DAO + * + * @author Zhougang + */ +@AllArgsConstructor +public class SignatureRedisDAO { + + private final StringRedisTemplate stringRedisTemplate; + + /** + * 验签随机数 + *

+ * KEY 格式:signature_nonce:%s // 参数为 随机数 + * VALUE 格式:String + * 过期时间:不固定 + */ + private static final String SIGNATURE_NONCE = "signature_nonce:%s"; + + /** + * 签名密钥 + *

+ * KEY 格式:signature_appid:%s // 参数为 appid + * VALUE 格式:String + * 过期时间:预加载到 redis 永不过期 + */ + private static final String SIGNATURE_APPID = "signature_appid:%s"; + + 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) { + // 将 nonce 记入缓存,防止重复使用(重点二:此处需要将 ttl 设定为允许 timestamp 时间差的值 x 2 ) + stringRedisTemplate.opsForValue().set(formatNonceKey(nonce), nonce, time * 2, timeUnit); + } + + private static String formatAppIdKey(String key) { + return String.format(SIGNATURE_APPID, key); + } + + private static String formatNonceKey(String key) { + return String.format(SIGNATURE_NONCE, key); + } +} 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 new file mode 100644 index 000000000..a253cd51a --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-protection/src/test/java/cn/iocoder/yudao/framework/signature/core/SignatureTest.java @@ -0,0 +1,136 @@ +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); + } + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/CacheRequestBodyWrapper.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/CacheRequestBodyWrapper.java index 8e80fa591..e181edeb4 100644 --- a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/CacheRequestBodyWrapper.java +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/CacheRequestBodyWrapper.java @@ -23,6 +23,10 @@ public class CacheRequestBodyWrapper extends HttpServletRequestWrapper { */ private final byte[] body; + public byte[] getBody() { + return body; + } + public CacheRequestBodyWrapper(HttpServletRequest request) { super(request); body = ServletUtils.getBodyBytes(request); From 5bc01901948de01ef94d6186389a566aa6b44858 Mon Sep 17 00:00:00 2001 From: zhougang <921366807@qq.com> Date: Sun, 19 May 2024 15:39:22 +0800 Subject: [PATCH 2/3] =?UTF-8?q?=E3=80=90=E4=BC=98=E5=8C=96=E3=80=91=20?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yudao-framework/yudao-spring-boot-starter-protection/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yudao-framework/yudao-spring-boot-starter-protection/pom.xml b/yudao-framework/yudao-spring-boot-starter-protection/pom.xml index 025bb7591..46326c63a 100644 --- a/yudao-framework/yudao-spring-boot-starter-protection/pom.xml +++ b/yudao-framework/yudao-spring-boot-starter-protection/pom.xml @@ -12,7 +12,7 @@ jar ${project.artifactId} - 服务保证,提供分布式锁、幂等、限流、熔断、API签名等等功能 + 服务保证,提供分布式锁、幂等、限流、熔断、API 签名等等功能 https://github.com/YunaiV/ruoyi-vue-pro From 5f278ac23b392d6ecd9452cc50fdfc9e77af97e8 Mon Sep 17 00:00:00 2001 From: zhougang Date: Tue, 28 May 2024 15:09:01 +0800 Subject: [PATCH 3/3] =?UTF-8?q?=E3=80=90=E6=96=B0=E5=A2=9E=E3=80=91?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E9=83=A8=E5=88=86=E4=BB=A3=E7=A0=81=E9=A3=8E?= =?UTF-8?q?=E6=A0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{Signature.java => ApiSignature.java} | 16 ++++--- .../signature/core/aop/SignatureAspect.java | 43 ++++++------------- .../core/redis/SignatureRedisDAO.java | 3 +- ...ot.autoconfigure.AutoConfiguration.imports | 3 +- .../core/filter/CacheRequestBodyWrapper.java | 4 -- 5 files changed, 26 insertions(+), 43 deletions(-) rename yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/annotation/{Signature.java => ApiSignature.java} (75%) diff --git a/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/annotation/Signature.java b/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/annotation/ApiSignature.java similarity index 75% rename from yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/annotation/Signature.java rename to yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/annotation/ApiSignature.java index 1b7e12786..e338ae709 100644 --- a/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/annotation/Signature.java +++ b/yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/signature/core/annotation/ApiSignature.java @@ -3,6 +3,7 @@ package cn.iocoder.yudao.framework.signature.core.annotation; import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; import java.lang.annotation.*; +import java.util.concurrent.TimeUnit; /** @@ -14,12 +15,17 @@ import java.lang.annotation.*; @Documented @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) -public @interface Signature { +public @interface ApiSignature { /** - * 同一个请求多长时间内有效 默认 10分钟 + * 同一个请求多长时间内有效 默认 60 秒 */ - long expireTime() default 600000L; + int timeout() default 60; + + /** + * 时间单位,默认为 SECONDS 秒 + */ + TimeUnit timeUnit() default TimeUnit.SECONDS; // ========================== 签名参数 ========================== @@ -50,8 +56,4 @@ public @interface Signature { */ String sign() default "sign"; - /** - * url 客户端不需要传递,但是可以用来加签(如: /{id} 带有动态参数的 url ,如果没有动态参数可设置为 false 不进行加签) - */ - boolean urlEnable() default true; } 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 index dc1510465..a001419f8 100644 --- 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 @@ -1,12 +1,13 @@ 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.Signature; +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; @@ -15,7 +16,6 @@ import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; -import org.springframework.util.Assert; import java.nio.charset.StandardCharsets; import java.util.Map; @@ -25,7 +25,7 @@ import java.util.TreeMap; import java.util.concurrent.TimeUnit; /** - * 拦截声明了 {@link Signature} 注解的方法,实现签名 + * 拦截声明了 {@link ApiSignature} 注解的方法,实现签名 * * @author Zhougang */ @@ -37,9 +37,9 @@ public class SignatureAspect { private final SignatureRedisDAO signatureRedisDAO; @Before("@annotation(signature)") - public void beforePointCut(JoinPoint joinPoint, Signature signature) { + public void beforePointCut(JoinPoint joinPoint, ApiSignature signature) { if (!verifySignature(signature, Objects.requireNonNull(ServletUtils.getRequest()))) { - log.info("[beforePointCut][方法{} 参数({}) 签名失败]", joinPoint.getSignature().toString(), + log.error("[beforePointCut][方法{} 参数({}) 签名失败]", joinPoint.getSignature().toString(), joinPoint.getArgs()); String message = StrUtil.blankToDefault(signature.message(), GlobalErrorCodeConstants.BAD_REQUEST.getMsg()); @@ -47,25 +47,22 @@ public class SignatureAspect { } } - private boolean verifySignature(Signature signature, HttpServletRequest request) { + 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, "找不到对应的 appSecret"); + Assert.notNull(appSecret, "[appId({})] 找不到对应的 appSecret", appId); // 请求头 SortedMap headersMap = getRequestHeaders(signature, request); - // 如:/user/{id} url 带有动态参数的情况 - String urlParams = signature.urlEnable() ? request.getServletPath() : ""; // 请求参数 String requestParams = getRequestParams(request); // 请求体 - String requestBody = getRequestBody(request); + String requestBody = ServletUtils.isJsonRequest(request) ? ServletUtils.getBody(request) : ""; // 生成服务端签名 - String serverSignature = SignUtil.signParamsSha256(headersMap, - urlParams + requestParams + requestBody + appSecret); + String serverSignature = SignUtil.signParamsSha256(headersMap, requestParams + requestBody + appSecret); // 客户端签名 String clientSignature = request.getHeader(signature.sign()); if (!StrUtil.equals(clientSignature, serverSignature)) { @@ -73,7 +70,7 @@ public class SignatureAspect { } String nonce = headersMap.get(signature.nonce()); // 将 nonce 记入缓存,防止重复使用(重点二:此处需要将 ttl 设定为允许 timestamp 时间差的值 x 2 ) - signatureRedisDAO.setNonce(nonce, signature.expireTime(), TimeUnit.MILLISECONDS); + signatureRedisDAO.setNonce(nonce, signature.timeout() * 2L, signature.timeUnit()); return true; } @@ -87,7 +84,7 @@ public class SignatureAspect { * @param signature signature * @param request request */ - private boolean verifyHeaders(Signature signature, HttpServletRequest request) { + private boolean verifyHeaders(ApiSignature signature, HttpServletRequest request) { String appId = request.getHeader(signature.appId()); if (StrUtil.isBlank(appId)) { return false; @@ -97,7 +94,7 @@ public class SignatureAspect { return false; } String nonce = request.getHeader(signature.nonce()); - if (StrUtil.isBlank(nonce) || nonce.length() < 10) { + if (StrUtil.isBlank(nonce) || StrUtil.length(nonce) < 10) { return false; } String sign = request.getHeader(signature.sign()); @@ -105,7 +102,7 @@ public class SignatureAspect { return false; } // 其他合法性校验 - long expireTime = signature.expireTime(); + long expireTime = signature.timeUnit().toMillis(signature.timeout()); long requestTimestamp = Long.parseLong(timestamp); // 检查 timestamp 是否超出允许的范围 (重点一:此处需要取绝对值) long timestampDisparity = Math.abs(System.currentTimeMillis() - requestTimestamp); @@ -122,7 +119,7 @@ public class SignatureAspect { * @param request request * @return signature params */ - private SortedMap getRequestHeaders(Signature signature, HttpServletRequest request) { + 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())); @@ -154,17 +151,5 @@ public class SignatureAspect { return queryString.substring(1); } - /** - * 获取请求体参数 - * - * @param request request - * @return body - */ - private String getRequestBody(HttpServletRequest request) { - CacheRequestBodyWrapper requestWrapper = new CacheRequestBodyWrapper(request); - // 获取 body - return new String(requestWrapper.getBody(), StandardCharsets.UTF_8); - } - } 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/SignatureRedisDAO.java index d00fe7f8d..326e238ee 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/SignatureRedisDAO.java @@ -42,8 +42,7 @@ public class SignatureRedisDAO { } public void setNonce(String nonce, long time, TimeUnit timeUnit) { - // 将 nonce 记入缓存,防止重复使用(重点二:此处需要将 ttl 设定为允许 timestamp 时间差的值 x 2 ) - stringRedisTemplate.opsForValue().set(formatNonceKey(nonce), nonce, time * 2, timeUnit); + stringRedisTemplate.opsForValue().set(formatNonceKey(nonce), nonce, time, timeUnit); } private static String formatAppIdKey(String key) { diff --git a/yudao-framework/yudao-spring-boot-starter-protection/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/yudao-framework/yudao-spring-boot-starter-protection/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index d7cd3a883..6cab74e75 100644 --- a/yudao-framework/yudao-spring-boot-starter-protection/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/yudao-framework/yudao-spring-boot-starter-protection/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -1,3 +1,4 @@ cn.iocoder.yudao.framework.idempotent.config.YudaoIdempotentConfiguration cn.iocoder.yudao.framework.lock4j.config.YudaoLock4jConfiguration -cn.iocoder.yudao.framework.ratelimiter.config.YudaoRateLimiterConfiguration \ No newline at end of file +cn.iocoder.yudao.framework.ratelimiter.config.YudaoRateLimiterConfiguration +cn.iocoder.yudao.framework.signature.config.YudaoSignatureAutoConfiguration \ No newline at end of file diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/CacheRequestBodyWrapper.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/CacheRequestBodyWrapper.java index e181edeb4..8e80fa591 100644 --- a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/CacheRequestBodyWrapper.java +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/CacheRequestBodyWrapper.java @@ -23,10 +23,6 @@ public class CacheRequestBodyWrapper extends HttpServletRequestWrapper { */ private final byte[] body; - public byte[] getBody() { - return body; - } - public CacheRequestBodyWrapper(HttpServletRequest request) { super(request); body = ServletUtils.getBodyBytes(request);