From 67795493bdc579e489d3ab12e52a1793c4f8a0ee Mon Sep 17 00:00:00 2001 From: JEECG <445654970@qq.com> Date: Mon, 15 Dec 2025 17:07:22 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=8A=A0=E7=AD=BE=E6=B3=A8?= =?UTF-8?q?=E8=A7=A3=20@SignatureCheck=EF=BC=8C=E9=92=88=E5=AF=B9=E8=8E=B7?= =?UTF-8?q?=E5=8F=96=E7=A7=9F=E6=88=B7=E4=BF=A1=E6=81=AF=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E5=8A=A0=E7=AD=BE=20=E3=80=90=E4=B8=A5=E9=87=8D=E5=AE=89?= =?UTF-8?q?=E5=85=A8=E6=BC=8F=E6=B4=9E=E3=80=91=E7=94=A8=E6=88=B7=E5=8F=AF?= =?UTF-8?q?=E5=8A=A0=E5=85=A5=E4=BB=BB=E6=84=8F=E7=A7=9F=E6=88=B7=20#9196?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sign/annotation/SignatureCheck.java | 32 ++++ .../sign/aspect/SignatureCheckAspect.java | 93 ++++++++++++ .../sign/interceptor/SignAuthInterceptor.java | 140 +++++++++++------- .../controller/SysTenantController.java | 2 + 4 files changed, 211 insertions(+), 56 deletions(-) create mode 100644 jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/sign/annotation/SignatureCheck.java create mode 100644 jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/sign/aspect/SignatureCheckAspect.java diff --git a/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/sign/annotation/SignatureCheck.java b/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/sign/annotation/SignatureCheck.java new file mode 100644 index 000000000..54e30b18f --- /dev/null +++ b/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/sign/annotation/SignatureCheck.java @@ -0,0 +1,32 @@ +package org.jeecg.config.sign.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 签名校验注解 + * 用于方法级别的签名验证,功能等同于yml中的jeecg.signUrls配置 + * 参考DragSignatureAspect的设计思路,使用AOP切面实现 + * + * @author GitHub Copilot + * @since 2025-12-15 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface SignatureCheck { + + /** + * 是否启用签名校验 + * @return true-启用(默认), false-禁用 + */ + boolean enabled() default true; + + /** + * 签名校验失败时的错误消息 + * @return 错误消息 + */ + String errorMessage() default "Sign签名校验失败!"; + +} diff --git a/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/sign/aspect/SignatureCheckAspect.java b/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/sign/aspect/SignatureCheckAspect.java new file mode 100644 index 000000000..ff10bee1f --- /dev/null +++ b/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/sign/aspect/SignatureCheckAspect.java @@ -0,0 +1,93 @@ +package org.jeecg.config.sign.aspect; + +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.aspectj.lang.annotation.Pointcut; +import org.aspectj.lang.reflect.MethodSignature; +import org.jeecg.config.sign.annotation.SignatureCheck; +import org.jeecg.config.sign.interceptor.SignAuthInterceptor; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import java.lang.reflect.Method; + +/** + * 基于AOP的签名验证切面 + * 复用SignAuthInterceptor的成熟签名验证逻辑 + * + * @author GitHub Copilot + * @since 2025-12-15 + */ +@Aspect +@Slf4j +@Component("signatureCheckAspect") +public class SignatureCheckAspect { + + /** + * 复用SignAuthInterceptor的签名验证逻辑 + */ + private final SignAuthInterceptor signAuthInterceptor = new SignAuthInterceptor(); + + /** + * 验签切点:拦截所有标记了@SignatureCheck注解的方法 + */ + @Pointcut("@annotation(org.jeecg.config.sign.annotation.SignatureCheck)") + private void signatureCheckPointCut() { + } + + /** + * 开始验签 + */ + @Before("signatureCheckPointCut()") + public void doSignatureValidation(JoinPoint point) throws Exception { + // 获取方法上的注解 + MethodSignature signature = (MethodSignature) point.getSignature(); + Method method = signature.getMethod(); + SignatureCheck signatureCheck = method.getAnnotation(SignatureCheck.class); + + log.info("AOP签名验证: {}.{}", method.getDeclaringClass().getSimpleName(), method.getName()); + + // 如果注解被禁用,直接返回 + if (!signatureCheck.enabled()) { + log.info("签名验证已禁用,跳过"); + return; + } + + ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + if (attributes == null) { + log.error("无法获取请求上下文"); + throw new IllegalArgumentException("无法获取请求上下文"); + } + + HttpServletRequest request = attributes.getRequest(); + log.info("X-SIGN: {}, X-TIMESTAMP: {}", request.getHeader("X-SIGN"), request.getHeader("X-TIMESTAMP")); + + try { + // 直接调用SignAuthInterceptor的验证逻辑 + signAuthInterceptor.validateSignature(request); + log.info("AOP签名验证通过"); + + } catch (IllegalArgumentException e) { + // 使用注解中配置的错误消息,或者保留原始错误消息 + String errorMessage = signatureCheck.errorMessage(); + log.error("AOP签名验证失败: {}", e.getMessage()); + + if ("Sign签名校验失败!".equals(errorMessage)) { + // 如果是默认错误消息,使用原始的详细错误信息 + throw e; + } else { + // 如果是自定义错误消息,使用自定义消息 + throw new IllegalArgumentException(errorMessage, e); + } + } catch (Exception e) { + // 包装其他异常 + String errorMessage = signatureCheck.errorMessage(); + log.error("AOP签名验证异常: {}", e.getMessage()); + throw new IllegalArgumentException(errorMessage, e); + } + } +} diff --git a/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/sign/interceptor/SignAuthInterceptor.java b/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/sign/interceptor/SignAuthInterceptor.java index 72e6547bb..92ae8112c 100644 --- a/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/sign/interceptor/SignAuthInterceptor.java +++ b/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/sign/interceptor/SignAuthInterceptor.java @@ -1,12 +1,10 @@ package org.jeecg.config.sign.interceptor; -import java.io.PrintWriter; -import java.util.SortedMap; - +import com.alibaba.fastjson.JSON; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; - +import lombok.extern.slf4j.Slf4j; import org.jeecg.common.api.vo.Result; import org.jeecg.common.constant.CommonConstant; import org.jeecg.common.util.DateUtils; @@ -16,9 +14,8 @@ import org.jeecg.config.sign.util.HttpUtils; import org.jeecg.config.sign.util.SignUtil; import org.springframework.web.servlet.HandlerInterceptor; -import com.alibaba.fastjson.JSON; - -import lombok.extern.slf4j.Slf4j; +import java.io.PrintWriter; +import java.util.SortedMap; /** * 签名拦截器 @@ -33,63 +30,94 @@ public class SignAuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { - log.debug("Sign Interceptor request URI = " + request.getRequestURI()); - HttpServletRequest requestWrapper = new BodyReaderHttpServletRequestWrapper(request); - //获取全部参数(包括URL和body上的) - SortedMap allParams = HttpUtils.getAllParams(requestWrapper); - //对参数进行签名验证 - String headerSign = request.getHeader(CommonConstant.X_SIGN); - String xTimestamp = request.getHeader(CommonConstant.X_TIMESTAMP); + log.info("签名拦截器 Interceptor request URI = " + request.getRequestURI()); - if(oConvertUtils.isEmpty(xTimestamp)){ - Result result = Result.error("Sign签名校验失败,时间戳为空!"); - log.error("Sign 签名校验失败!Header xTimestamp 为空"); - //校验失败返回前端 - response.setCharacterEncoding("UTF-8"); - response.setContentType("application/json; charset=utf-8"); - PrintWriter out = response.getWriter(); - out.print(JSON.toJSON(result)); - return false; - } - - //客户端时间 - Long clientTimestamp = Long.parseLong(xTimestamp); - - int length = 14; - int length1000 = 1000; - //1.校验签名时间(兼容X_TIMESTAMP的新老格式) - if (xTimestamp.length() == length) { - //a. X_TIMESTAMP格式是 yyyyMMddHHmmss (例子:20220308152143) - if ((DateUtils.getCurrentTimestamp() - clientTimestamp) > MAX_EXPIRE) { - log.error("签名验证失败:X-TIMESTAMP已过期,注意系统时间和服务器时间是否有误差!"); - throw new IllegalArgumentException("签名验证失败:X-TIMESTAMP已过期"); - } - } else { - //b. X_TIMESTAMP格式是 时间戳 (例子:1646552406000) - if ((System.currentTimeMillis() - clientTimestamp) > (MAX_EXPIRE * length1000)) { - log.error("签名验证失败:X-TIMESTAMP已过期,注意系统时间和服务器时间是否有误差!"); - throw new IllegalArgumentException("签名验证失败:X-TIMESTAMP已过期"); - } - } - - //2.校验签名 - boolean isSigned = SignUtil.verifySign(allParams,headerSign); - - if (isSigned) { - log.debug("Sign 签名通过!Header Sign : {}",headerSign); + try { + // 调用验证逻辑 + validateSignature(request); return true; - } else { - log.debug("sign allParams: {}", allParams); - log.error("request URI = " + request.getRequestURI()); - log.error("Sign 签名校验失败!Header Sign : {}",headerSign); - //校验失败返回前端 + } catch (IllegalArgumentException e) { + // 验证失败,返回错误响应 + log.error("Sign 签名校验失败!{}", e.getMessage()); response.setCharacterEncoding("UTF-8"); response.setContentType("application/json; charset=utf-8"); PrintWriter out = response.getWriter(); - Result result = Result.error("Sign签名校验失败!"); + Result result = Result.error(e.getMessage()); out.print(JSON.toJSON(result)); return false; } } + + /** + * 签名验证核心逻辑 + * 提取出来供AOP切面复用 + * @param request HTTP请求 + * @throws IllegalArgumentException 验证失败时抛出异常 + */ + public void validateSignature(HttpServletRequest request) throws IllegalArgumentException { + try { + log.debug("开始签名验证: {} {}", request.getMethod(), request.getRequestURI()); + + HttpServletRequest requestWrapper = new BodyReaderHttpServletRequestWrapper(request); + //获取全部参数(包括URL和body上的) + SortedMap allParams = HttpUtils.getAllParams(requestWrapper); + log.debug("提取参数: {}", allParams); + + //对参数进行签名验证 + String headerSign = request.getHeader(CommonConstant.X_SIGN); + String xTimestamp = request.getHeader(CommonConstant.X_TIMESTAMP); + + if(oConvertUtils.isEmpty(xTimestamp)){ + log.error("Sign签名校验失败,时间戳为空!"); + throw new IllegalArgumentException("Sign签名校验失败,请求参数不完整!"); + } + + //客户端时间 + Long clientTimestamp = Long.parseLong(xTimestamp); + + int length = 14; + int length1000 = 1000; + //1.校验签名时间(兼容X_TIMESTAMP的新老格式) + if (xTimestamp.length() == length) { + //a. X_TIMESTAMP格式是 yyyyMMddHHmmss (例子:20220308152143) + long currentTimestamp = DateUtils.getCurrentTimestamp(); + long timeDiff = currentTimestamp - clientTimestamp; + log.debug("时间戳验证(yyyyMMddHHmmss): 时间差{}秒", timeDiff); + + if (timeDiff > MAX_EXPIRE) { + log.error("时间戳已过期: {}秒 > {}秒", timeDiff, MAX_EXPIRE); + throw new IllegalArgumentException("签名验证失败,请求时效性验证失败!"); + } + } else { + //b. X_TIMESTAMP格式是 时间戳 (例子:1646552406000) + long currentTime = System.currentTimeMillis(); + long timeDiff = currentTime - clientTimestamp; + long maxExpireMs = MAX_EXPIRE * length1000; + log.debug("时间戳验证(Unix): 时间差{}ms", timeDiff); + + if (timeDiff > maxExpireMs) { + log.error("时间戳已过期: {}ms > {}ms", timeDiff, maxExpireMs); + throw new IllegalArgumentException("签名验证失败,请求时效性验证失败!"); + } + } + + //2.校验签名 + boolean isSigned = SignUtil.verifySign(allParams,headerSign); + + if (isSigned) { + log.debug("签名验证通过"); + } else { + log.error("签名验证失败, 参数: {}", allParams); + throw new IllegalArgumentException("Sign签名校验失败!"); + } + } catch (IllegalArgumentException e) { + // 重新抛出签名验证异常 + throw e; + } catch (Exception e) { + // 包装其他异常(如IOException) + log.error("签名验证异常: {}", e.getMessage()); + throw new IllegalArgumentException("Sign签名校验失败:" + e.getMessage()); + } + } } diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/SysTenantController.java b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/SysTenantController.java index a725e858f..87a7f5bfa 100644 --- a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/SysTenantController.java +++ b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/SysTenantController.java @@ -21,6 +21,7 @@ import org.jeecg.common.util.PasswordUtil; import org.jeecg.common.util.TokenUtils; import org.jeecg.common.util.oConvertUtils; import org.jeecg.config.mybatis.MybatisPlusSaasConfig; +import org.jeecg.config.sign.annotation.SignatureCheck; import org.jeecg.modules.base.service.BaseCommonService; import org.jeecg.modules.system.entity.*; import org.jeecg.modules.system.service.ISysTenantPackService; @@ -260,6 +261,7 @@ public class SysTenantController { * @param id * @return */ + @SignatureCheck @RequestMapping(value = "/queryById", method = RequestMethod.GET) public Result queryById(@RequestParam(name="id",required=true) String id) { Result result = new Result();