使用AOP技术实现Java通用接口验签工具

一、背景

在给第三方提供接口时,我们需要对接口进行验签。具体来说,当外部系统调用我们的接口时,请求中需要携带一个签名,我们接收到请求后,会解析数据并校验签名是否正确,以确保请求的合法性和安全性。

使用AOP技术实现Java通用接口验签工具

为了在不同项目中方便地使用这一功能,我们将签名校验规则封装成一个工具包。使用方只需通过简单的注解即可轻松集成验签功能,无需重复编写验签逻辑,从而提高开发效率并确保一致性。

二、实现原理

  1. 使用AOP来拦截方法
  2. 获取参数值进行组装、校验签名是否一致

三、设计思路

通过俩个注解进行标记所需要进行验签的方法

@Target(ElementType.METHOD) // 注解只能用于方法 @Retention(RetentionPolicy.RUNTIME) // 注解在运行时可见 public @interface SignatureChecker {          // 服务Code     String serviceCode();     // 密钥     String secretKey() default "";     // 默认为true,表示需要验证签名     boolean required() default true;     // 过期时间,单位为分钟     int expireMinutes() default -1;      } 

serviceCode:服务编码,进行区分不同的服务/业务
secretKey:双方约定好的密钥,进行生成签名,可以写在配置文件中。
expireMinutes:标识签名有效时长,默认5分钟,可以配置文件中进行全局修改。

@Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) public @interface SignatureParam {      String requestIdField() default "";     String timestampField() default "";     String signatureField() default "";      } 

对于不同的请求实体,可能对应的字段名不相同,所以我们需要使用一个注解进行标注当前实体验签字段的名称。

当签名字段发生变化时,可以使用requestIdField、timestampField、signatureField 字段进行指定。

四、代码

4.1 代码结构

使用AOP技术实现Java通用接口验签工具

4.2 详细代码

4.2.1 SignatureChecker.class

import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target;  @Target(ElementType.METHOD) // 注解只能用于方法 @Retention(RetentionPolicy.RUNTIME) // 注解在运行时可见 public @interface SignatureChecker {          // 服务Code     String serviceCode();     // 密钥     String secretKey() default "";     // 默认为true,表示需要验证签名     boolean required() default true;     // 过期时间,单位为分钟     int expireMinutes() default -1;      } 

4.2.2 SignatureParam.class

import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target;  @Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) public @interface SignatureParam {      String requestIdField() default "";     String timestampField() default "";     String signatureField() default "";  } 

4.2.3 SignatureAspect.class

import com.alibaba.fastjson2.JSON; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.reflect.MethodSignature; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import org.tao.anno.SignatureChecker; import org.tao.anno.SignatureParam; import org.tao.config.SignatureProperties; import org.tao.ecxeption.SignatureValidationException; import org.tao.util.SignatureUtil;  import javax.annotation.Resource; import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.util.Map;  @Aspect @Component public class SignatureAspect {     private static final Logger logger = LoggerFactory.getLogger(SignatureAspect.class);      @Resource     private SignatureProperties signatureProperties;      @Around("@annotation(org.tao.anno.SignatureChecker)")     public Object validateSignature(ProceedingJoinPoint joinPoint) throws Throwable {         MethodSignature signature = (MethodSignature) joinPoint.getSignature();         Method method = signature.getMethod();         // 获取方法的所有参数         Annotation[][] parameterAnnotations = method.getParameterAnnotations();         Object[] args = joinPoint.getArgs();          Map<String, Object> paramMap = null;         SignatureChecker signatureChecker = method.getAnnotation(SignatureChecker.class);         if (signatureChecker != null && signatureChecker.required()) {             SignatureParam signatureParam = null;             // 遍历参数注解,找到被 @SignatureParam 注解修饰的参数             outerLoop:             for (int i = 0; i < parameterAnnotations.length; i++) {                 for (Annotation anno : parameterAnnotations[i]) {                     if (anno instanceof SignatureParam) {                         signatureParam = (SignatureParam) anno;                         Object paramValue = args[i];                         try {                             paramMap = JSON.parseObject(JSON.toJSONString(paramValue), Map.class);                         } catch (Exception e) {                             logger.error("[签名校验] 失败,请检查参数是否正确, paramValue => {}, message => {}", JSON.toJSONString(paramValue), e.getMessage());                         }                         break outerLoop;                     }                 }             }             try {                 validateSignature(signatureChecker, signatureParam, paramMap);             } catch (SignatureValidationException e) {                 logger.warn("[签名校验] 校验失败,paramMap => {}, message => {}", JSON.toJSONString(paramMap), e.getMessage());                 throw e;             }         }          // 继续执行原方法         return joinPoint.proceed();     }      private void validateSignature(SignatureChecker checker, SignatureParam signatureParam, Map<String, Object> paramMap) throws SignatureValidationException {         // 获取服务编码         String servicedCodeNew = checker.serviceCode();         if (StringUtils.isEmpty(servicedCodeNew)) {             throw new SignatureValidationException("缺失服务编码,请联系管理员配置!");         }         // 获取密钥         String secretKey = StringUtils.isEmpty(checker.secretKey()) ? signatureProperties.getSecretKeys().get(servicedCodeNew) : checker.secretKey();         if (StringUtils.isEmpty(secretKey)) {             throw new SignatureValidationException("缺失验签密钥,请联系管理员配置!");         }          String signatureField = StringUtils.isEmpty(signatureParam.signatureField()) ? signatureProperties.getSignatureField() : signatureParam.signatureField();         String requestIdField = StringUtils.isEmpty(signatureParam.requestIdField()) ? signatureProperties.getRequestIdField() : signatureParam.requestIdField();         String timestampField = StringUtils.isEmpty(signatureParam.timestampField()) ? signatureProperties.getTimestampField() : signatureParam.timestampField();         // 获取请求参数         String requestId = paramMap.get(requestIdField) == null ? null : paramMap.get(requestIdField).toString();         String signature = paramMap.get(signatureField) == null ? null : paramMap.get(signatureField).toString();         Long timestamp = paramMap.get(timestampField) == null ? null : Long.parseLong(paramMap.get(timestampField).toString());          if (requestId == null || signature == null || timestamp == null) {             throw new SignatureValidationException("缺失验签参数,请检查!");         }          // 校验时间戳         validateTimestamp(timestamp, checker.expireMinutes());         // 校验签名         if (!SignatureUtil.verifySignature(requestId + timestamp, secretKey, signature)) {             throw new SignatureValidationException("签名校验不通过!");         }     }      private void validateTimestamp(long timestamp, int expireMinutes) throws SignatureValidationException {         // 如果是-1,则使用配置文件中的默认值         expireMinutes = expireMinutes == -1 ? signatureProperties.getExpireMinutes() : expireMinutes;         // 如果是0,则代表永久有效,不进行时间判断         if (expireMinutes == 0) {             return;         } else if (expireMinutes <= 0) {             throw new SignatureValidationException("过期时间配置无效,请检查!");         }          long currentTime = System.currentTimeMillis();         if (timestamp > currentTime + 5 * 60 * 1000) {             throw new SignatureValidationException("调用端时间与服务器时间未同步,请检查!");         } else if (currentTime - timestamp > (long) expireMinutes * 60 * 1000) {             throw new SignatureValidationException("请求已过期,请重新请求!");         }     } } 

4.2.4 SignatureAutoConfiguration.class

import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.tao.aop.SignatureAspect;  /**  * @author: handsometaoa  * @description  * @date: 2025/3/29 11:00  */   @Configuration @EnableConfigurationProperties(SignatureProperties.class) public class SignatureAutoConfiguration {      @Bean     @ConditionalOnMissingBean     public SignatureAspect signatureAspect() {         return new SignatureAspect();     }   }  

4.2.5 SignatureProperties.class

import org.springframework.boot.context.properties.ConfigurationProperties;  import java.util.HashMap; import java.util.Map;  @ConfigurationProperties(prefix = "signature") public class SignatureProperties {      private String requestIdField = "requestId";     private String timestampField = "timestamp";     private String signatureField = "signature";     private Integer expireMinutes = 5;     private Map<String, String> secretKeys = new HashMap<>();      public Map<String, String> getSecretKeys() {         return secretKeys;     }      public void setSecretKeys(Map<String, String> secretKeys) {         this.secretKeys = secretKeys;     }      public Integer getExpireMinutes() {         return expireMinutes;     }      public void setExpireMinutes(Integer expireMinutes) {         this.expireMinutes = expireMinutes;     }      public String getRequestIdField() {         return requestIdField;     }      public void setRequestIdField(String requestIdField) {         this.requestIdField = requestIdField;     }      public String getSignatureField() {         return signatureField;     }      public void setSignatureField(String signatureField) {         this.signatureField = signatureField;     }      public String getTimestampField() {         return timestampField;     }      public void setTimestampField(String timestampField) {         this.timestampField = timestampField;     } } 

4.2.7 GlobalExceptionHandler

import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice;  import java.util.HashMap; import java.util.Map;  @RestControllerAdvice public class GlobalExceptionHandler {      @ExceptionHandler(SignatureValidationException.class)     public ResponseEntity<Map<String, Object>> handleSignatureValidationException(SignatureValidationException ex) {         Map<String, Object> response = new HashMap<>();         response.put("code", 500);         response.put("message", ex.getMessage());         return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR);     } } 

4.2.8 SignatureValidationException.class

public class SignatureValidationException extends RuntimeException {     public SignatureValidationException(String message) {         super(message);     } } 

4.2.9 SignatureUtil.class

  import org.springframework.util.DigestUtils;  /**  * @author: handsometaoa  * @description  * @date: 2025/3/29 11:00  */ public class SignatureUtil {      /**      * 校验签名是否正确      *      * @param params    请求参数      * @param sign      客户端传递的签名      * @param secretKey 密钥      * @return 是否校验通过      */     public static boolean verifySignature(String params, String secretKey, String sign) {         String serverSign = generateSignature(params, secretKey);         return serverSign.equals(sign);     }      /**      * 生成签名      *      * @param params    请求参数      * @param secretKey 密钥      * @return 生成的签名      */     private static String generateSignature(String params, String secretKey) {         String rawData = params + secretKey;         return DigestUtils.md5DigestAsHex(rawData.getBytes());     }   }  

4.2.10 spring.factories.class

org.springframework.boot.autoconfigure.EnableAutoConfiguration= org.tao.config.SignatureAutoConfiguration 

4.2.11 pom.xml

  <dependencies>     <dependency>       <groupId>org.springframework.boot</groupId>       <artifactId>spring-boot-starter-aop</artifactId>       <version>2.1.3.RELEASE</version>       <optional>true</optional>     </dependency>     <dependency>       <groupId>org.springframework.boot</groupId>       <artifactId>spring-boot-autoconfigure</artifactId>       <version>2.1.3.RELEASE</version>       <optional>true</optional>     </dependency>     <dependency>       <groupId>org.springframework.boot</groupId>       <artifactId>spring-boot-starter-web</artifactId>       <version>2.1.3.RELEASE</version>       <optional>true</optional>     </dependency>     <dependency>       <groupId>com.alibaba.fastjson2</groupId>       <artifactId>fastjson2</artifactId>       <version>2.0.31</version>       <optional>true</optional>     </dependency>   </dependencies> 

五、使用方式

5.1 集成方式 (俩种方式)

  1. 打成 jar 包引入依赖
  2. 直接将代码拷进自己项目

5.2 使用说明

  1. 在方法上添加 @SignatureChecker 注解,包含密钥的参数前添加 @SignatureParam 注解;举例:假设需要给XX业务进行验签,约定密钥为XXX,请求参数分别为 request_id、timeStamp、signature
@PostMapping("test") @SignatureChecker(serviceCode = "XX", secretKey = "XXX") public String test(@RequestBody @SignatureParam(requestIdField = "request_id", timestampField = "timeStamp") Request request) {     return "test"; } 
  1. 基于 注解值 > 配置值 > 默认值 (约定大于配置)
  • 密钥 secretKey : 注解(secretKey) > 配置值 (signature.secretKeys 中定义的)
  • 有效期 expireMinutes:注解(expireMinutes)> 配置值 (signature.expireMinutes)> 默认值(5)
  • 验签字段:注解(requestIdField、timestampField、signatureField)> 配置值 (signature.requestIdField) > 默认值(requestId、timestamp、signature)
  1. 请求接口
    使用AOP技术实现Java通用接口验签工具

六、最后

当然代码还是有很多不足的地方,仅供学习参考。
源码:https://github.com/handsometaoa/signutare-kit

发表评论

评论已关闭。

相关文章