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 | 38 | |
| 39 | 39 | <!-- Test 测试相关 --> |
| 40 | 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 | 43 | <scope>test</scope> |
| 44 | 44 | </dependency> |
| 45 | 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 | 1 | package cn.iocoder.yudao.framework.signature.config; |
| 2 | 2 | |
| 3 | 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 | 6 | import org.springframework.boot.autoconfigure.AutoConfiguration; |
| 7 | 7 | import org.springframework.context.annotation.Bean; |
| 8 | 8 | import org.springframework.data.redis.core.StringRedisTemplate; |
| 9 | 9 | |
| 10 | 10 | /** |
| 11 | + * HTTP API 签名的自动配置类 | |
| 12 | + * | |
| 11 | 13 | * @author Zhougang |
| 12 | 14 | */ |
| 13 | 15 | @AutoConfiguration(after = YudaoRedisAutoConfiguration.class) |
| 14 | -public class YudaoSignatureAutoConfiguration { | |
| 16 | +public class YudaoApiSignatureAutoConfiguration { | |
| 15 | 17 | |
| 16 | 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 | 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 | 1 | package cn.iocoder.yudao.framework.signature.core.aop; |
| 2 | 2 | |
| 3 | -import cn.hutool.core.collection.CollUtil; | |
| 4 | 3 | import cn.hutool.core.lang.Assert; |
| 4 | +import cn.hutool.core.map.MapUtil; | |
| 5 | +import cn.hutool.core.util.ObjUtil; | |
| 5 | 6 | import cn.hutool.core.util.StrUtil; |
| 6 | -import cn.hutool.crypto.SignUtil; | |
| 7 | +import cn.hutool.crypto.digest.DigestUtil; | |
| 7 | 8 | import cn.iocoder.yudao.framework.common.exception.ServiceException; |
| 8 | -import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; | |
| 9 | 9 | import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; |
| 10 | 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 | 12 | import jakarta.servlet.http.HttpServletRequest; |
| 14 | 13 | import lombok.AllArgsConstructor; |
| 15 | 14 | import lombok.extern.slf4j.Slf4j; |
| ... | ... | @@ -17,12 +16,12 @@ import org.aspectj.lang.JoinPoint; |
| 17 | 16 | import org.aspectj.lang.annotation.Aspect; |
| 18 | 17 | import org.aspectj.lang.annotation.Before; |
| 19 | 18 | |
| 20 | -import java.nio.charset.StandardCharsets; | |
| 21 | 19 | import java.util.Map; |
| 22 | 20 | import java.util.Objects; |
| 23 | 21 | import java.util.SortedMap; |
| 24 | 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 | 27 | * 拦截声明了 {@link ApiSignature} 注解的方法,实现签名 |
| ... | ... | @@ -32,59 +31,62 @@ import java.util.concurrent.TimeUnit; |
| 32 | 31 | @Aspect |
| 33 | 32 | @Slf4j |
| 34 | 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 | 38 | @Before("@annotation(signature)") |
| 40 | 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 | 54 | if (!verifyHeaders(signature, request)) { |
| 52 | 55 | return false; |
| 53 | 56 | } |
| 54 | - // 校验 appId 是否能获取到对应的 appSecret | |
| 57 | + // 1.2 校验 appId 是否能获取到对应的 appSecret | |
| 55 | 58 | String appId = request.getHeader(signature.appId()); |
| 56 | 59 | String appSecret = signatureRedisDAO.getAppSecret(appId); |
| 57 | 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 | 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 | 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 | 84 | * @param signature signature |
| 85 | 85 | * @param request request |
| 86 | + * @return 是否校验 Header 通过 | |
| 86 | 87 | */ |
| 87 | 88 | private boolean verifyHeaders(ApiSignature signature, HttpServletRequest request) { |
| 89 | + // 1. 非空校验 | |
| 88 | 90 | String appId = request.getHeader(signature.appId()); |
| 89 | 91 | if (StrUtil.isBlank(appId)) { |
| 90 | 92 | return false; |
| ... | ... | @@ -94,32 +96,54 @@ public class SignatureAspect { |
| 94 | 96 | return false; |
| 95 | 97 | } |
| 96 | 98 | String nonce = request.getHeader(signature.nonce()); |
| 97 | - if (StrUtil.isBlank(nonce) || StrUtil.length(nonce) < 10) { | |
| 99 | + if (StrUtil.length(nonce) < 10) { | |
| 98 | 100 | return false; |
| 99 | 101 | } |
| 100 | 102 | String sign = request.getHeader(signature.sign()); |
| 101 | 103 | if (StrUtil.isBlank(sign)) { |
| 102 | 104 | return false; |
| 103 | 105 | } |
| 104 | - // 其他合法性校验 | |
| 106 | + | |
| 107 | + // 2. 检查 timestamp 是否超出允许的范围 (重点一:此处需要取绝对值) | |
| 105 | 108 | long expireTime = signature.timeUnit().toMillis(signature.timeout()); |
| 106 | 109 | long requestTimestamp = Long.parseLong(timestamp); |
| 107 | - // 检查 timestamp 是否超出允许的范围 (重点一:此处需要取绝对值) | |
| 108 | 110 | long timestampDisparity = Math.abs(System.currentTimeMillis() - requestTimestamp); |
| 109 | 111 | if (timestampDisparity > expireTime) { |
| 110 | 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 | 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 | 147 | SortedMap<String, String> sortedMap = new TreeMap<>(); |
| 124 | 148 | sortedMap.put(signature.appId(), request.getHeader(signature.appId())); |
| 125 | 149 | sortedMap.put(signature.timestamp(), request.getHeader(signature.timestamp())); |
| ... | ... | @@ -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 | 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 | 6 | import java.util.concurrent.TimeUnit; |
| 7 | 7 | |
| 8 | 8 | /** |
| 9 | - * API 签名 Redis DAO | |
| 9 | + * HTTP API 签名 Redis DAO | |
| 10 | 10 | * |
| 11 | 11 | * @author Zhougang |
| 12 | 12 | */ |
| 13 | 13 | @AllArgsConstructor |
| 14 | -public class SignatureRedisDAO { | |
| 14 | +public class ApiSignatureRedisDAO { | |
| 15 | 15 | |
| 16 | 16 | private final StringRedisTemplate stringRedisTemplate; |
| 17 | 17 | |
| 18 | 18 | /** |
| 19 | 19 | * 验签随机数 |
| 20 | - * <p> | |
| 20 | + * | |
| 21 | 21 | * KEY 格式:signature_nonce:%s // 参数为 随机数 |
| 22 | 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 | 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 | 39 | public String getNonce(String nonce) { |
| 41 | 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 | 47 | private static String formatNonceKey(String key) { |
| 53 | 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 | 1 | cn.iocoder.yudao.framework.idempotent.config.YudaoIdempotentConfiguration |
| 2 | 2 | cn.iocoder.yudao.framework.lock4j.config.YudaoLock4jConfiguration |
| 3 | 3 | cn.iocoder.yudao.framework.ratelimiter.config.YudaoRateLimiterConfiguration |
| 4 | -cn.iocoder.yudao.framework.signature.config.YudaoSignatureAutoConfiguration | |
| 5 | 4 | \ No newline at end of file |
| 5 | +cn.iocoder.yudao.framework.signature.config.YudaoApiSignatureAutoConfiguration | |
| 6 | 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 | -} |