-
Notifications
You must be signed in to change notification settings - Fork 6.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #541 from slipper4j/dev-0519-framework-signature
【优化】根据给出的建议优化部分代码逻辑、风格
- Loading branch information
Showing
7 changed files
with
442 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
27 changes: 27 additions & 0 deletions
27
...ain/java/cn/iocoder/yudao/framework/signature/config/YudaoSignatureAutoConfiguration.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} | ||
|
||
} |
59 changes: 59 additions & 0 deletions
59
...tion/src/main/java/cn/iocoder/yudao/framework/signature/core/annotation/ApiSignature.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
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; | ||
|
||
|
||
/** | ||
* 签名注解 | ||
* | ||
* @author Zhougang | ||
*/ | ||
@Inherited | ||
@Documented | ||
@Target({ElementType.METHOD, ElementType.TYPE}) | ||
@Retention(RetentionPolicy.RUNTIME) | ||
public @interface ApiSignature { | ||
|
||
/** | ||
* 同一个请求多长时间内有效 默认 60 秒 | ||
*/ | ||
int timeout() default 60; | ||
|
||
/** | ||
* 时间单位,默认为 SECONDS 秒 | ||
*/ | ||
TimeUnit timeUnit() default TimeUnit.SECONDS; | ||
|
||
// ========================== 签名参数 ========================== | ||
|
||
/** | ||
* 提示信息,签名失败的提示 | ||
* | ||
* @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"; | ||
|
||
} |
155 changes: 155 additions & 0 deletions
155
...otection/src/main/java/cn/iocoder/yudao/framework/signature/core/aop/SignatureAspect.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
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<String, String> 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<String, String> getRequestHeaders(ApiSignature signature, HttpServletRequest request) { | ||
SortedMap<String, String> sortedMap = new TreeMap<>(); | ||
sortedMap.put(signature.appId(), request.getHeader(signature.appId())); | ||
sortedMap.put(signature.timestamp(), request.getHeader(signature.timestamp())); | ||
sortedMap.put(signature.nonce(), request.getHeader(signature.nonce())); | ||
return sortedMap; | ||
} | ||
|
||
/** | ||
* 获取 URL 参数 | ||
* | ||
* @param request request | ||
* @return queryParams | ||
*/ | ||
private String getRequestParams(HttpServletRequest request) { | ||
if (CollUtil.isEmpty(request.getParameterMap())) { | ||
return ""; | ||
} | ||
Map<String, String[]> requestParams = request.getParameterMap(); | ||
// 获取 URL 请求参数 | ||
SortedMap<String, String> sortParamsMap = new TreeMap<>(); | ||
for (Map.Entry<String, String[]> entry : requestParams.entrySet()) { | ||
sortParamsMap.put(entry.getKey(), entry.getValue()[0]); | ||
} | ||
// 按 key 排序 | ||
StringBuilder queryString = new StringBuilder(); | ||
for (Map.Entry<String, String> entry : sortParamsMap.entrySet()) { | ||
queryString.append("&").append(entry.getKey()).append("=").append(entry.getValue()); | ||
} | ||
return queryString.substring(1); | ||
} | ||
|
||
} | ||
|
55 changes: 55 additions & 0 deletions
55
...tion/src/main/java/cn/iocoder/yudao/framework/signature/core/redis/SignatureRedisDAO.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
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; | ||
|
||
/** | ||
* 验签随机数 | ||
* <p> | ||
* KEY 格式:signature_nonce:%s // 参数为 随机数 | ||
* VALUE 格式:String | ||
* 过期时间:不固定 | ||
*/ | ||
private static final String SIGNATURE_NONCE = "signature_nonce:%s"; | ||
|
||
/** | ||
* 签名密钥 | ||
* <p> | ||
* 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) { | ||
stringRedisTemplate.opsForValue().set(formatNonceKey(nonce), nonce, time, 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); | ||
} | ||
} |
3 changes: 2 additions & 1 deletion
3
...esources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
cn.iocoder.yudao.framework.ratelimiter.config.YudaoRateLimiterConfiguration | ||
cn.iocoder.yudao.framework.signature.config.YudaoSignatureAutoConfiguration |
Oops, something went wrong.