mirror of
https://github.com/jeecgboot/JeecgBoot.git
synced 2025-12-26 16:26:41 +08:00
@ -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签名校验失败!";
|
||||
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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<String, String> 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<String, String> 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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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<SysTenant> queryById(@RequestParam(name="id",required=true) String id) {
|
||||
Result<SysTenant> result = new Result<SysTenant>();
|
||||
|
||||
Reference in New Issue
Block a user