前言
现在有一个项目需要对请求体和返回体进行加解密操作,首先想到的就是用切面的形式控制加解密。但是在最后测试的时候发现@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);
}
}