在我们日常的业务需求中,经常会遇到需要对存储的用户敏感数据进行加密处理的场景,如用户的身份信息、住址、身份证号等等,本文我们就讨论下,业务系统(后端)如何实现数据存储(基于MySQL)的加解密功能。
技术栈:springboot、mybatis、mysql等
第一步:定义注解@Encrypt
@Target(ElementType.METHOD)//注解的范围是类、接口、枚举的方法上
@Retention(RetentionPolicy.RUNTIME)//被虚拟机保存,可用反射机制读取
public @interface Encrypt{
/**
* 入参需要加密的字段
* @return
*/
String[] paramFields() default {};
/**
* 响应参数需解密的字段
* @return
*/
String[] respFields() default {};
}
第二步:开发拦截器处理类
@Slf4j
@Aspect
@Component
public class EncryptAspect {
@Pointcut("@annotation(com.xxx.annotation.Encrypt)")
public void encryPointCut() {
}
@Around("encryPointCut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
final Sm4Intercept annotation = method.getAnnotation(Encrypt.class);
if (null == annotation) {
return joinPoint.proceed();
}
//加密入参对象属性值
encryptRequest(joinPoint, annotation);
//执行目标方法
Object response = joinPoint.proceed();
//解密响应结果对象的属性值
decryptResponse(response, annotation);
return response;
}
/**
* 加密请求入参对象
*
* @param joinPoint
* @param annotation
*/
private void encryptRequest(ProceedingJoinPoint joinPoint, Sm4Intercept annotation) {
//获取接口的入参列表
final Object[] params = joinPoint.getArgs();
if (!CollectionUtil.isEmpty(params)) {
//接口入参
Object param = params[0];
//接口入参对象的属性列表
Field[] fields = param.getClass().getDeclaredFields();
if (!CollectionUtil.isEmpty(annotation.paramFields())) {
//遍历加密入参的属性值
Arrays.stream(annotation.paramFields()).forEach(target -> {
Field field = Arrays.stream(fields)
.filter(f -> f.getName().equals(target))
.findFirst()
.orElse(null);
if (null != field) {
//反射获取目标属性值
Object fieldValue = getFieldValue(param, field.getName());
if (null != fieldValue) {
String encryFieldValue = EncryptUtil.encryptEcb(key, fieldValue.toString());
log.info("类{}的属性{}的值{}已被加密为{}", param.getClass().getName(), target, fieldValue, encryFieldValue);
setFieldValue(param, field.getName(), encryFieldValue);
}
}
});
}
}
}
/**
* 解密响应结果对象的属性值
*
* @param object
* @param annotation
*/
private void decryptResponse(Object object, Sm4Intercept annotation) {
//返回结果是list时
if (object instanceof List) {
decryptListObject((List) object, annotation);
return;
}
//返回结果为单对象时
decryptObject(object, annotation);
}
/**
* 解密list中对象的属性值
*
* @param list
* @param annotation
*/
private void decryptListObject(List list, Sm4Intercept annotation) {
list.stream().forEach(record -> decryptObject(record, annotation));
}
/**
* 解密单对象的属性值
*
* @param record
* @param annotation
*/
private void decryptObject(Object record, Sm4Intercept annotation) {
//接口返回对象的属性列表
Field[] fields = record.getClass().getDeclaredFields();
if (!CollectionUtil.isEmpty(annotation.respFields())) {
//遍历加密入参的属性值
Arrays.stream(annotation.respFields()).forEach(target -> {
Field field = Arrays.stream(fields)
.filter(f -> f.getName().equals(target))
.findFirst()
.orElse(null);
if (null != field) {
//反射获取目标属性值
Object fieldValue = getFieldValue(record, field.getName());
if (null != fieldValue) {
String decryFieldValue = EncryptUtil.decryptEcb(key, fieldValue.toString());
log.info("类{}的属性{}的值{}已被解密为{}", record.getClass().getName(), target, fieldValue, decryFieldValue);
setFieldValue(record, field.getName(), decryFieldValue);
}
}
});
}
}
/**
* 通过反射,用属性名称获得属性值
*
* @param thisClass 需要获取属性值的类
* @param fieldName 该类的属性名称
* @return
*/
private Object getFieldValue(Object thisClass, String fieldName) {
Object value = new Object();
try {
Method method = thisClass.getClass().getMethod(getMethodName(fieldName, "get"));
value = method.invoke(thisClass);
} catch (Exception e) {
}
return value;
}
/**
* 通过反射,设置属性值
*
* @param thisClass
* @param fieldName
* @param fieldValue
*/
private void setFieldValue(Object thisClass, String fieldName, Object fieldValue) {
try {
Method method = thisClass.getClass().getMethod(getMethodName(fieldName, "set"), String.class);
method.invoke(thisClass, fieldValue);
} catch (Exception e) {
}
}
/**
* 获取方法名称(getXXX,setXXX)
*
* @param fieldName
* @param methodPrefix
* @return
*/
private String getMethodName(String fieldName, String methodPrefix) {
return methodPrefix.concat(fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1));
}
}
第三步:mapper中定义注解@Encrypt进行数据拦截
@Mapper
public interface UserInfoMapper extends BaseMapper {
/**
* 如果查询条件中包含username,则在mapper执行前进行加密
* 如果返回数据中包含username及address等信息,则进行解密
* @param vo
* @return
*/
@Encrypt(paramFields = {"userName"}, respFields = {"userName", "address"})
List findUserInfo(UserSearchVo vo);
}
这样,便实现了在数据查询(或更新、插入等)时,完成入参及返回数据的加解密操作。不过这种处理方式仅限于数据操作是通过Dao的mapper接口调用时,如果想处理更多场景,如通过mybatis-plus的Wraper方式进行数据处理时,则考虑用后面的第二种处理方式。
MyBatis 允许你在映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:
- Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
- ParameterHandler (getParameterObject, setParameters)
- ResultSetHandler (handleResultSets, handleOutputParameters)
- StatementHandler (prepare, parameterize, batch, update, query)
这些类中方法的细节可以通过查看每个方法的签名来发现,或者直接查看 MyBatis 发行包中的源代码。 如果你想做的不仅仅是监控方法的调用,那么你最好相当了解要重写的方法的行为。 因为在试图修改或重写已有方法的行为时,很可能会破坏 MyBatis 的核心模块。 这些都是更底层的类和方法,所以使用插件的时候要特别当心。
通过 MyBatis 提供的强大机制,使用插件是非常简单的,只需实现 Interceptor 接口,并指定想要拦截的方法签名即可。
插件的使用参考实现如下(mybatis官方文档)
@Slf4j
@Component
@Intercepts({@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class,
RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class
})})
public class ExamplePlugin implements Interceptor {
private Properties properties = new Properties();
public Object intercept(Invocation invocation) throws Throwable {
// implement pre processing if need
Object returnObject = invocation.proceed();
// implement post processing if need
return returnObject;
}
public void setProperties(Properties properties) {
this.properties = properties;
}
}
即在mybatis的Executor的query方法执行前后进行拦截,如事先定义好需要加解密的“配置规则”(如“对哪个表的哪些字段需要加解密”、“方法的出入参需加解密的字段”等等),然后拦截sql的请求参数及执行的返回结果,对其进行相应的数据加解密操作。
本文的核心实现思路都是围绕spring aop进行实现的,足以说明aop思想的强大之处,大家平时学习工作中一定要勤学、多用、多练!希望本文可以帮助到有需要的朋友们!