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