SpringBoot AOP+国密SM2+@Valid非空判断

前言

现在有一个项目需要对请求体和返回体进行加解密操作,首先想到的就是用切面的形式控制加解密。但是在最后测试的时候发现@Valid非空判断的执行逻辑总是在AOP切面之前,导致根本走不到切面位置就会报错。本文讨论如何利用AOP进行加解密和非空判断走不到切面问题。

国密SM2前后端如何加解密请看这篇文章 (后端Hutool+前端sm-crypto)

准备工作

引入所需依赖

        <!-- AOP切面依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
            
        <!-- validator 非空判断所需依赖 -->
        <dependency>
            <groupId>org.hibernate.validator</groupId>
            <artifactId>hibernate-validator</artifactId>
            <version>6.0.17.Final</version>
        </dependency>

基础请求实体类

后端方法接受请求实体类必须继承基础请求实体类,否则无法解密

@Data
public class BaseRequestDTO {
    
    /**
     * 基础请求实体类只需要一个属性即前端传过来的密文对应的key值
     * {
     *    encryptBody:"密文"
     * }
     * 若自定义,则需在使用注解时声明
     * 不想声明则固定为encryptStr
     */
    private String encryptBody;
}

注解

首先编写注解类在类或方法上面使用注解来表示切点

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;


/**
 * 自定义注解,用来标识请求类或者方法是否使用AOP加密解密
 */
@Target({ElementType.TYPE, ElementType.METHOD}) // 可以作用在类上和方法上
@Retention(RetentionPolicy.RUNTIME)  // 运行时起作用
public @interface Secret {

    // 基础请求实体类(用来传递加密数据)必填
    // PS:如果方法是用RequestDTO类来接收,则必须继承此类,该类须自定义
    Class value();

    // 基础请求实体类中传递加密数据的属性名,默认encryptStr
    // 此属性值必须与上面基础请求实体类中自定义的加密属性名一致
    // 在基础请求实体类中如何命名,此处就填写该名
    String encryptStrName() default "encryptStr";

    //注解使用时按照如下格式引用:
    //@Secret(XXX.class)
    //@Secret(value = XXX.class ,encryptStrName="自定义")

}

@Valid判断工具类



import org.hibernate.validator.HibernateValidator;
import org.springframework.stereotype.Component;

import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import java.util.Set;

/**
 * 非空校验工具类
 * 使用说明: 我们一般在接收参数前面加上注解@Valid来进行非空判断,但是这样就走不到切面里面进行解密
 * 所以我们不能添加@Valid注解,而是在切面里面解密绑值后调用方法来判断,此方法和使用注解效果一样
 * 调用位置请看AOP切面
 */
@Component
public class ValidatorUtil {
    /**
     * 实体对象校验
     */
    private final Validator VALIDATOR =
        Validation.byProvider(HibernateValidator.class).configure().failFast(true).
            buildValidatorFactory().getValidator();

    /**
     * 实体对象校验
     *
     * @param obj obj
     * @param <T> t
     */
    public <T> void validate(T obj) {
        Set<ConstraintViolation<T>> constraintViolations = VALIDATOR.validate(obj);
        if (constraintViolations.size() > 0) {
            ConstraintViolation<T> validateInfo = constraintViolations.iterator().next();
            //抛出异常
            throw new MyBaseException(validateInfo.getMessageTemplate());
        }
    }
}

AOP切面


import cn.hutool.core.util.ClassUtil;
import cn.hutool.core.util.HexUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.asymmetric.KeyType;
import cn.hutool.crypto.asymmetric.SM2;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

/**
 * 切面加密解密
 **/
@Aspect
@Component
@Slf4j
class SecretAopController {

    // 是否进行加密解密,通过配置文件注入(不配置默认为true)
    @Value("${isSecret:true}")
    boolean isSecret;

    /**
     * 服务端加解密SM2
     */
    @Autowired
    SM2 sm2;

    /**
     * 非空校验工具类
     */
    @Autowired
    ValidatorUtil validatorUtil;

    // 定义切点,使用了@Secret注解的类 或 使用了@Secret注解的方法
    // @within():用于匹配有该注解类下的所有方法
    // @annotation(): 用于匹配有该注解的方法
    @Pointcut("@within(com.internal.myjmini.common.annotation.Secret) || @annotation(com.internal.myjmini.common.annotation.Secret)")
    public void pointcut() {
    }

    // 环绕切面
    @Around("pointcut()")
    public MyResponse around(ProceedingJoinPoint point) throws Throwable {

        // 获取被代理方法参数
        Object[] args = point.getArgs();
        // 获取被代理对象
        Object target = point.getTarget();
        // 获取通知签名
        MethodSignature signature = (MethodSignature) point.getSignature();
        //如果方法有参则继续解密,方法无参直接返回
        if (args.length > 0) {
            try {
                // 获取被代理方法
                Method pointMethod = target.getClass().getMethod(signature.getName(), signature.getParameterTypes());
                // 获取被代理方法上面的注解@Secret
                Secret secret = pointMethod.getAnnotation(Secret.class);

                // 被代理方法上没有,则说明@Secret注解在被代理类上
                if (secret == null) {
                    secret = target.getClass().getAnnotation(Secret.class);
                }
                
                if (secret != null) {
                    // 获取注解上声明的参数类
                    Class clazz = secret.value();
                    // 获取注解上声明的加密参数名
                    String encryptStrName = secret.encryptStrName();
                    for (int i = 0; i < args.length; i++) {
                        // 判断当前参数args[i]类型是否继承参数类
                        if (clazz.isInstance(args[i])) {
                            // 通过反射,获取getEncryptStr()方法
                            Method method = clazz.getMethod(getMethedName(encryptStrName));
                            // 执行getEncryptStr()方法,获取加密数据
                            String encryptStr = (String) method.invoke(args[i]);
                            // 加密字符串是否为空
                            if (StrUtil.isNotBlank(encryptStr)) {
                                //如果加密字符串不是以04开头则添加上
                                encryptStr = StrUtil.prependIfMissing(encryptStr, "04");
                                // 解密
                                try {
                                    String json = StrUtil.utf8Str(sm2.decrypt(encryptStr, KeyType.PrivateKey));
                                    args[i] = JSONUtil.toBean(json, args[i].getClass());
                                } catch (Exception e) {
                                    log.error("解密失败", e.getMessage(), e);
                                    //MyBaseException 自定义异常,请自行编写
                                    throw new MyBaseException("解密失败");
                                }

                            }
                            //非空校验,若有异常直接抛出
                            validatorUtil.validate(args[i]);
                        } else {
                            // 其他类型,比如基本数据类型、包装类型、String,Map不参与解密
                            // 没有继承请求基类的参数也不参与解密
                            // 故若想要某一方法不参与解密,接收参数不继承请求基类即可
                            if (ClassUtil.isBasicType(args[i].getClass())) {
                                //判断是否属于基本数据类型或其包装类
                                log.error("请勿使用基本数据类型接收参数");
//                                throw new MyBaseException("请勿使用基本数据类型接收参数");
                            }
                            log.error("方法参数" + args[i].getClass().getName() + "没有继承" + clazz.getName());
//                            throw new MyBaseException("方法参数" + args[i].getClass().getName() + "没有继承" + clazz.getName());
                        }

                    }
                }
                return encryptData(point, args);
            } catch (NoSuchMethodException e) {
                throw new MyBaseException("@Secret注解指定的类没有encryptStr属性,或encryptStrName参数字段值与属性值不一致");
            } catch (ClassCastException e) {
                throw new MyBaseException("返回值类型非MyResponse,请勿使用其他返回值类型");
            } catch (Throwable throwable) {
                throw throwable;
            }
        } else {
            return encryptData(point, args);
        }

    }

    // 转化方法名
    private String getMethedName(String name) {
        return "get" + StrUtil.upperFirst(name) ;
    }

    //针对返回值进行加密  MyResponse 所有Controller方法统一返回实体类,请自行编写
    private MyResponse encryptData(ProceedingJoinPoint point, Object[] args) throws Throwable {
        // 此方法执行后会继续执行controller里面的方法
        // 直到controller里面的方法执行结束然后再次执行此方法后面的语句
        // 此方法的返回值即为controller里面的方法的返回值
        MyResponse response = (MyResponse) point.proceed(args);
        if (isSecret && ObjectUtil.isNotNull(response.getData())) {
            String jsonStr = JSONUtil.toJsonStr(response.getData());
            String encryptStr = HexUtil.encodeHexStr(sm2.encrypt(jsonStr, KeyType.PublicKey));
            //如果前端使用sm-crypto工具进行解密,需要去掉04前缀,不是则忽略
            encryptStr = StrUtil.subSuf(encryptStr, 2);
            response.setData(encryptStr);
        }
        return response;
    }
}

使用

定义接受参数请求类

import lombok.Data;
import javax.validation.constraints.NotBlank;

//继承基础请求实体类
//在需要判断的属性上添加注解
@Data
public class LoginRequestDTO extends BaseRequestDTO{


    @NotBlank(message = "用户名不能为空")
    private String userName;

    @NotBlank(message = "密码不能为空")
    private String passWord;
}

定义Controller

@RestController
@RequiredArgsConstructor
@RequestMapping("/user")
// 放在类上对所有方法进行加解密
// @Secret(value = BaseRequestDTO.class,encryptStrName = "encryptBody")
public class UserController {

    //放在方法上,可单独对某一方法进行加解密
    @Secret(value = BaseRequestDTO.class,encryptStrName = "encryptBody")
    @PostMapping("/login")
    //接受参数前不能使用@Valid注解,在切面当中调用方法判断
    public MyResponse login(  LoginRequestDTO login)  {
        System.out.println(login);
        return new MyResponse().success().data(login);
    }


}
  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
实现接口调用频率限制可以使用AOP和ConcurrentHashMap结合的方式。 首先,在Spring Boot中,我们可以使用AOP来拦截接口的调用。我们可以定义一个切面,使用@Aspect注解标注,然后在切入点方法中定义需要拦截的注解。 例如,我们可以定义一个@FrequencyLimit注解,用于标注需要限制调用频率的方法: ```java @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.METHOD }) public @interface FrequencyLimit { // 限制时间段,单位为秒 int interval() default 60; // 时间段内最大请求次数 int maxCount() default 10; } ``` 然后,在切面中,我们可以拦截该注解标注的方法,并且进行限制调用频率的操作。可以使用ConcurrentHashMap来存储每个接口的调用次数和最后一次调用时间。 ```java @Component @Aspect public class FrequencyLimitAspect { private ConcurrentHashMap<String, Long> lastRequestTimeMap = new ConcurrentHashMap<>(); private ConcurrentHashMap<String, Integer> requestCountMap = new ConcurrentHashMap<>(); @Around("@annotation(frequencyLimit)") public Object frequencyLimit(ProceedingJoinPoint joinPoint, FrequencyLimit frequencyLimit) throws Throwable { Object result = null; String methodName = joinPoint.getSignature().toLongString(); long currentTime = System.currentTimeMillis(); int interval = frequencyLimit.interval(); int maxCount = frequencyLimit.maxCount(); synchronized (this) { // 获取最后一次请求时间和请求次数 Long lastRequestTime = lastRequestTimeMap.get(methodName); Integer requestCount = requestCountMap.get(methodName); if (lastRequestTime == null || currentTime - lastRequestTime >= interval * 1000) { // 如果该接口在限制时间段内没有被调用过,则重置请求次数和最后一次请求时间 lastRequestTimeMap.put(methodName, currentTime); requestCountMap.put(methodName, 1); } else { // 如果该接口在限制时间段内已经被调用过,则增加请求次数 requestCount++; if (requestCount > maxCount) { // 如果请求次数超过了限制,则抛出异常 throw new RuntimeException("Exceeded maximum request limit"); } lastRequestTimeMap.put(methodName, currentTime); requestCountMap.put(methodName, requestCount); } } // 调用原始方法 result = joinPoint.proceed(); return result; } } ``` 在切面中,我们使用synchronized关键字来保证线程安全,因为ConcurrentHashMap并不能完全保证线程安全。同时,我们使用了@Around注解来拦截被@FrequencyLimit注解标注的方法,然后在方法中实现限制调用频率的逻辑。 这样,我们就可以实现接口调用频率限制了。在需要限制调用频率的方法中,我们只需要加上@FrequencyLimit注解即可。例如: ```java @GetMapping("/test") @FrequencyLimit(interval = 60, maxCount = 10) public String test() { return "test"; } ``` 这样,每个IP地址每分钟内最多只能调用该方法10次,超过次数会抛出异常。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值