概要
基于SpringBoot和Mybatis(兼容Mybatis-Plus)的数据安全处理插件,在数据持久层对数据进行加/解密和脱敏处理。
主要功能:
1、通过注解在保存数据的时候将指定字段进行加密处理,查询时自动解密
2、通过注解在查询数据的时候对指定字段进行脱敏处理
技术架构
SpringBoot/Mybatis-plus/MySQL/HuTool/Bouncy Castle/Lombok
项目概述
提示:除了黄色标记的目录是为了测试之外,所有类上都有类的作用简介
技术细节
MybatisEncryptInterceptor
MyBatis数据更新拦截器,主要负责拦截MyBatis的新增和修改操作,并且调用安全处理类对带有加密注解的字段进行加密处理。
@Intercepts({
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
})
public class MybatisEncryptInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
//获取mappedStatement对象
MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
// 获取Mybatis插入或更新时传入的参数对象
Object paramEntity = invocation.getArgs()[1];
//调用安全处理类对数据加密处理
SecurityHandler.handlerSecurity(mappedStatement.getId(), paramEntity, false);
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
if (target instanceof Executor) {
return Plugin.wrap(target, this);
} else {
return target;
}
}
@Override
public void setProperties(Properties properties) {
}
}
MybatisDecryptInterceptor
MyBatis数据查询拦截器,主要负责拦截MyBatis的查询操作,在查询完成之后,调用安全处理类对查询返回的数据进行解密和脱敏处理。
@Intercepts({
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class})
})
public class MybatisDecryptInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
//执行查询
ArrayList<Object> result = (ArrayList) invocation.proceed();
//查询结果解密
if (result != null) {
for (Object paramEntity : result) {
//调用处理类解密
SecurityHandler.handlerSecurity(null, paramEntity, true);
}
}
return result;
}
@Override
public Object plugin(Object target) {
if (target instanceof Executor) {
return Plugin.wrap(target, this);
} else {
return target;
}
}
@Override
public void setProperties(Properties properties) {
}
}
MybatisInterceptorConfig
MyBatis拦截器配置类,把自定义的拦截器添加到MyBatis拦截器中生效。
@Component
public class MybatisInterceptorConfig {
@Resource
private SqlSessionFactory sqlSessionFactory;
@PostConstruct
public void addInterceptor() {
Configuration sqlSessionFactoryConfiguration = sqlSessionFactory.getConfiguration();
sqlSessionFactoryConfiguration.addInterceptor(new MybatisEncryptInterceptor());
sqlSessionFactoryConfiguration.addInterceptor(new MybatisDecryptInterceptor());
}
}
SecurityHandler
安全处理类,主要负责调用加密处理类和脱敏处理类
public class SecurityHandler {
/**
* 安全处理
*
* @param paramEntity
* @param doDecrypt
*/
public static void handlerSecurity(String methodPath, Object paramEntity, boolean doDecrypt) {
//加/解密
try {
SecretHandler.doEncrypt(methodPath, paramEntity, doDecrypt);
} catch (Exception e) {
throw new RuntimeException("数据加/解密处理时出现异常,异常信息【" + e.getMessage() + "】");
}
//如果是解密,则继续进行脱敏操作
if (doDecrypt) {
//脱敏
try {
DesensitizedHandler.doDesensitized(paramEntity);
} catch (Exception e) {
throw new RuntimeException("数据脱敏处理时出现异常,异常信息【" + e.getMessage() + "】");
}
}
}
}
SecretHandler
加/解密处理类,主要负责对配置加密注解进行校验和判断,找到需要加/解密处理的字段,调用加密工具对数据进行加密,每个代码上面都有相应的功能注释。
public class SecretHandler {
/**
* 对数据加/解密
*
* @param paramEntity 参数实体
* @param doDecrypt 是否是解密操作
* @throws IllegalAccessException
*/
public static void doEncrypt(String methodPath, Object paramEntity, boolean doDecrypt) throws IllegalAccessException, ClassNotFoundException {
//获取参数类的class
Class<?> paramEntityClass = paramEntity.getClass();
//如果当前是加密并且方法入参不是实体类,那么看是方法上是否有加密参数注解
if (!doDecrypt && paramEntityClass.equals(MapperMethod.ParamMap.class)) {
//获取类和方法名段断点索引
int lastIndexOf = methodPath.lastIndexOf(".");
//获取执行的类路径
Class<?> methodClass = Class.forName(methodPath.substring(0, lastIndexOf));
//执行的获取方法名
String methodName = methodPath.substring(lastIndexOf + 1);
//获取所有执行的方法,找到当前执行的方法
Method[] declaredMethods = methodClass.getDeclaredMethods();
//定义当前执行的加密方法
Method currentEncryptMethod = null;
//循环所有方法,找到当前执行的加密方法
for (Method declaredMethod : declaredMethods) {
//如果当前执行的方法带有加密注解,则获取执行的参数
if (methodName.equals(declaredMethod.getName()) && declaredMethod.isAnnotationPresent(EncryptMethod.class)) {
//找到了当前需要加密的方法,跳出循环
currentEncryptMethod = declaredMethod;
break;
}
}
if (currentEncryptMethod != null) {
//获取所有参数
Parameter[] parameters = currentEncryptMethod.getParameters();
//遍历该方法的所有参数
for (Parameter parameter : parameters) {
//对带有加密参数注解的参数进行加密处理
if (parameter.isAnnotationPresent(EncryptParam.class)) {
//定义加密查询的变量名
String paramName;
//定义原始参数值
Object paramValue;
//获取参数类型
Class<?> parameterType = parameter.getType();
//通过注解获取参数值
EncryptParam encryptParam = parameter.getAnnotation(EncryptParam.class);
//获取加密的参数名,要和mybatis的xml使用的sql一致
String encryptParamName = encryptParam.value();
//如果没有设置的话,尝试取默认参数名(args0、args1、args2...)
if (StrUtil.isBlank(encryptParamName)) {
//获取加密查询的变量名
paramName = parameter.getName();
//获取原始值
paramValue = ((MapperMethod.ParamMap) paramEntity).get(parameter.getName());
} else {
//如果传入的参数实体包含注解设置的名称,则可以获取传入的值
if (((MapperMethod.ParamMap) paramEntity).containsKey(encryptParamName)) {
//获取加密查询的变量名
paramName = encryptParamName;
//获取原始值
paramValue = ((MapperMethod.ParamMap) paramEntity).get(encryptParamName);
} else {
//如果不包含就抛异常
throw new RuntimeException("EncryptParam注解设置的参数名【" + encryptParamName + "】不在mybatis查询的参数当中,请检查!");
}
}
//原始值为空直接跳过
if (paramValue == null) {
continue;
}
//如果是字符串,则直接对字符串的值进行加密
if (String.class.equals(parameterType)) {
//原始值判空
if (StrUtil.isNotBlank(paramValue.toString())) {
//对值加密
((MapperMethod.ParamMap) paramEntity).put(paramName, SecretUtil.encrypt(paramValue.toString()));
}
} else if (List.class.equals(parameterType)) {
//如果是集合,则遍历集合,看实体类的参数是否有加密注解
for (Object object : (List) paramValue) {
if (object != null) {
//对实体类加密处理
encryptByEntity(object, false);
}
}
}
}
}
}
} else {
//如果查询参数是实体,则进行对实体进行判定并加密
encryptByEntity(paramEntity, doDecrypt);
}
}
/**
* 实体类加密处理
*
* @param paramEntity 加密实体类
* @param doDecrypt 是否是解密
* @throws IllegalAccessException
*/
private static void encryptByEntity(Object paramEntity, boolean doDecrypt) throws IllegalAccessException {
//获取实体类的class对象
Class<?> paramEntityClass = paramEntity.getClass();
//如果是实体类,判断该实体类是否添加EncryptEntity注解,如果没有EncryptEntity注解,则不需要加密操作
boolean needEncryptEntity = paramEntityClass.isAnnotationPresent(EncryptEntity.class);
//不需要加密,直接返回
if (!needEncryptEntity) {
return;
}
//是否自动标记字段
boolean autoMark = paramEntityClass.getAnnotation(EncryptEntity.class).autoMark();
//获取实体类的所有字段值
Field[] declaredFields = paramEntityClass.getDeclaredFields();
//遍历字段加密,如果自动标记为true则除了忽略的所有字段全部需要加密
if (autoMark) {
for (Field declaredField : declaredFields) {
//判断字段类型是否是字符串,不是字符串直接跳过
Class<?> fieldType = declaredField.getType();
if (!String.class.equals(fieldType)) {
continue;
}
//判断字段是否有忽略加密注解
boolean ignoreEncrypt = declaredField.isAnnotationPresent(IgnoreEncryptField.class);
if (ignoreEncrypt) {
continue;
}
encryptField(paramEntity, doDecrypt, declaredField);
}
} else {
//否则只对带有EncryptField字段的字段加密
for (Field declaredField : declaredFields) {
//判断字段类型是否是字符串,不是字符串直接跳过
Class<?> fieldType = declaredField.getType();
if (!String.class.equals(fieldType)) {
continue;
}
//判断是否有加密注解
boolean needEncryptField = declaredField.isAnnotationPresent(EncryptField.class);
if (!needEncryptField) {
continue;
}
encryptField(paramEntity, doDecrypt, declaredField);
}
}
}
/**
* 加密/解密字段
*
* @param paramEntity 参数实体
* @param doDecrypt 是否是解密操作
* @param declaredField 待处理字段
* @throws IllegalAccessException
*/
private static void encryptField(Object paramEntity, boolean doDecrypt, Field declaredField) throws IllegalAccessException {
//如果自动标记字段则除了忽略的所有字段全部需要加密
declaredField.setAccessible(true);
//获取原始值
Object paramValue = declaredField.get(paramEntity);
//加解密操作
if (paramValue != null && StrUtil.isNotBlank(paramValue.toString())) {
if (doDecrypt) {
//设置解密值
try {
declaredField.set(paramEntity, SecretUtil.decrypt(paramValue.toString()));
} catch (Exception e) {
throw new RuntimeException("解密失败,异常信息为【" + e.getMessage() + "】,请联系管理员");
}
} else {
//设置加密值
declaredField.set(paramEntity, SecretUtil.encrypt(paramValue.toString()));
}
declaredField.setAccessible(false);
}
}
}
DesensitizedHandler
脱敏处理类,主要负责对配置的脱敏注解进行校验和判断,找到需要脱敏处理的字段,调用脱敏的工具对数据脱敏,同样每个代码上面都有相应的功能注释。
public class DesensitizedHandler {
/**
* 脱敏处理
*
* @param paramEntity 参数实体
* @throws IllegalAccessException
*/
public static void doDesensitized(Object paramEntity) throws IllegalAccessException {
//获取参数类的class
Class<?> paramEntityClass = paramEntity.getClass();
//获取实体类的所有字段值
Field[] declaredFields = paramEntityClass.getDeclaredFields();
//遍历字段脱敏
for (Field declaredField : declaredFields) {
//判断字段类型是否是字符串,不是字符串直接跳过
Class<?> fieldType = declaredField.getType();
if (!String.class.equals(fieldType)) {
continue;
}
//判断字段是否有脱敏注解
boolean needDesensitized = declaredField.isAnnotationPresent(DesensitizedField.class);
//不需要脱敏直接跳过
if (!needDesensitized) {
continue;
}
declaredField.setAccessible(true);
Object paramValue = declaredField.get(paramEntity);
//脱敏
if (paramValue != null && StrUtil.isNotBlank(paramValue.toString())) {
String paramValueStr = paramValue.toString();
//获取脱敏注解
DesensitizedField desensitizedField = declaredField.getAnnotation(DesensitizedField.class);
//获取脱敏配置的模式
DesensitizedMode desensitizedMode = desensitizedField.mode();
//如果是默认模式,根据front和tail的值脱敏
if (DesensitizedMode.defaultMode == desensitizedMode) {
//否则根据配置的脱敏模式脱敏
int front = desensitizedField.front();
int tail = desensitizedField.tail();
//如果头尾位置都大于0,根据脱敏的头尾值脱敏,否则不进行脱敏处理
if (front >= 0 && tail >= 0) {
declaredField.set(paramEntity, DesensitizeUtil.desensitized(paramValueStr, front, tail));
}
} else {
//如果不是默认模式,则根据配置的模式脱敏
declaredField.set(paramEntity, DesensitizeUtil.desensitizedByMode(desensitizedMode, paramValueStr));
}
declaredField.setAccessible(false);
}
}
}
}
小结
上面只贴了MyBatis过滤器的配置以及加密脱敏判断的处理类代码,核心注解类以及加密和脱敏的工具类的代码就不贴了,下面是源码地址,里面有一份插件的使用文档,如果有不清楚怎么使用的读者,或者是项目有错误以及不足之处欢迎一起讨论。
为了方便读者测试,小编保留了简单的测试接口,下载源码后,先执行里面的数据库脚本(table.sql),然后就可以调用项目里开发好的接口进行测试。
另外,不足之处还请见谅,谢谢 ~
链接: 源码地址