通过mybatis数据脱敏的大致流程
现在对数据安全是越来越重视了,最近也做了几个存有大量用户个人信息的项目,考虑到安全性,相关的敏感字段是不能以明文的方式保存到数据库的,经过一番资料的查找(上网划水)确定了如下的全局加密解密的处理。
- 通过自定义的mybatis插件可以对sql查询参数进行拦截修改
- 通过自定义的mybatis 插件可以对查询结果进行拦截修改
那么大致的思路就有了,1、自定义一个注解用于表示那些是需要脱敏的字段;2、拦截并加密 sql 参数;3、对结果进行解密
graph LR
a1(用注解标识脱敏字段)
a2(mybatis拦截参数加密数据)
a3(数据库)
a6(应用)
a5(mybatis拦截结果集解密数据)
a1 --> a2
a2 --> a3
a3 --> a5
a5 --> a6
加密算法及工具准备
在正式开始前还需要确定加密算法,如果是实验性的加密的话,建议使用AES一般数据库都原生支持,有什么问题可以直接在sql中就完成加解密,如果想要灵活的话,加密解密类都继承同一个抽象类,到时候只用换具体实现即可。
/**
* 对象字段加密解密类,目前我是提供了两种加密算法选择
*/
public class FiledEncryptDecrypt {
/**
* 数据字段加密密钥-密钥长度需要为16
* 对不同的项目建议设置不同的密钥
*/
public static final String DATABASE_SECERT = "123456789abcd";
/**
* 加密
*
* @param declaredFields paramsObject所声明的字段
* @param paramsObject mapper中paramsType的实例
* @return T
* @throws IllegalAccessException 字段不可访问异常
*/
public static <T> T encrypt(Field[] declaredFields, T paramsObject) throws IllegalAccessException {
for (Field field : declaredFields) {
//取出所有被EncryptDecryptField注解的字段
EncryptDecryptField sensitiveField = field.getAnnotation(EncryptDecryptField.class);
if (!Objects.isNull(sensitiveField)) {
field.setAccessible(true);
Object object = field.get(paramsObject);
//暂时只实现String类型的加密
if (object instanceof String) {
String value = (String) object;
//加密 AES加密工具
field.set(paramsObject, encrypt(value));
}
}
}
return paramsObject;
}
/**
* 解密
*
* @param result resultType的实例
* @return T
* @throws IllegalAccessException 字段不可访问异常
*/
public static <T> T decrypt(T result) throws IllegalAccessException {
//取出resultType的类
Class<?> resultClass = result.getClass();
Field[] declaredFields = resultClass.getDeclaredFields();
for (Field field : declaredFields) {
//取出所有被EncryptDecryptField注解的字段
EncryptDecryptField sensitiveField = field.getAnnotation(EncryptDecryptField.class);
if (!Objects.isNull(sensitiveField)) {
field.setAccessible(true);
Object object = field.get(result);
//只支持String的解密
if (object instanceof String) {
String value = (String) object;
//对注解的字段进行逐一解密
field.set(result, decrypt(value));
}
}
}
return result;
}
/**
* 加密方法
* @param value
* @return
*/
private static String encrypt(String value){
return AesUtil.ecbEncryptOfHex(value, DATABASE_SECERT);
}
/**
* 解密
* @param value
* @return
*/
private static String decrypt(String value){
return AesUtil.ecbDecryptOfHex(value, DATABASE_SECERT);
}
}
AesUtil 加密解密具体实现类
public class AesUtil {
/**
* AES-ECB模式解密操作
* @param content
* @param key
* @return
* @throws Exception
*/
public static String ecbDecryptOfHex(String content, String key) {
try {
byte[] result = ecbDecrypt(Encodes.decodeHex(content), key);
return new String(result, DEFAULT_URL_ENCODING);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* AES-ECB模式解密操作
* @param content 16进制的密文
* @param key 需要满足长度为16
* @return
*/
public static String ecbDecryptOfHex(String content, String key) {
try {
byte[] result = ecbDecrypt(Encodes.decodeHex(content), key);
return new String(result, DEFAULT_URL_ENCODING);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
编写mybatis插件
加密解密工具准备完成,下面开始mybatis插件,这里需要两个插件一个对入库的参数进行加密,一个是对查询结果进行解密,注意两个插件注入的位置是不一样的。
/**
* sql参数加密插件
* method = "setParameters" 注册到参数设置环节
*/
@Intercepts({
@Signature(type = ParameterHandler.class, method = "setParameters", args = PreparedStatement.class),
})
public class SetParameterInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
//@Signature 指定了 type= parameterHandler 后,这里的 invocation.getTarget() 便是parameterHandler
//若指定ResultSetHandler ,这里则能强转为ResultSetHandler
ParameterHandler parameterHandler = (ParameterHandler) invocation.getTarget();
// 获取参数对像,即 mapper 中 paramsType 的实例
Field parameterField = parameterHandler.getClass().getDeclaredField("parameterObject");
parameterField.setAccessible(true);
//取出实例
Object parameterObject = parameterField.get(parameterHandler);
if (parameterObject != null) {
Class<?> parameterObjectClass = parameterObject.getClass();
//校验该实例的类是否被@EncryptDecryptData所注解,避免对不需要的对象进行反射操作
EncryptDecryptData encryptDecryptData = AnnotationUtils.findAnnotation(parameterObjectClass, EncryptDecryptData.class);
if (Objects.nonNull(encryptDecryptData)) {
// 复制一份参数对象,避免影响原始对象参数
Object tempObject = parameterObjectClass.newInstance();
BeanUtils.copyProperties(parameterObject, tempObject);
//取出当前当前类所有字段,传入加密方法
Field[] declaredFields = parameterObjectClass.getDeclaredFields();
FiledEncryptDecrypt.encrypt(declaredFields, tempObject);
parameterField.set(parameterHandler, tempObject);
} else if (parameterObject instanceof MapperMethod.ParamMap) {
// 对list和多个入参的情况进行处理
MapperMethod.ParamMap paramMap = (MapperMethod.ParamMap) parameterObject;
Set<Map.Entry> set = paramMap.entrySet();
for (Map.Entry entry : set) {
Object key = entry.getKey();
if (key instanceof String) {
String keyName = (String) key;
if (keyName.startsWith("param")) {
// 以param开头的是别名,实际上是和实际参数名使用同一个引用,为避免参数多次加密
continue;
}
}
Object obj = entry.getValue();
// 判断是否为集合类型
if (obj instanceof Collection) {
Collection collection = (Collection) obj;
for (Object entity : collection) {
Class<?> paramClass = entity.getClass();
//校验该实例的类是否被@EncryptDecryptData所注解
encryptDecryptData = AnnotationUtils.findAnnotation(paramClass, EncryptDecryptData.class);
if (Objects.nonNull(encryptDecryptData)) {
//取出当前当前类所有字段,传入加密方法
Field[] declaredFields = paramClass.getDeclaredFields();
FiledEncryptDecrypt.encrypt(declaredFields, entity);
}
}
} else {
Class<?> paramClass = obj.getClass();
//校验该实例的类是否被@EncryptDecryptData所注解
encryptDecryptData = AnnotationUtils.findAnnotation(paramClass, EncryptDecryptData.class);
if (Objects.nonNull(encryptDecryptData)) {
// 复制一份参数对象,避免修改原值
Object tempObject = paramClass.newInstance();
BeanUtils.copyProperties(obj, tempObject);
//取出当前当前类所有字段,传入加密方法
Field[] declaredFields = parameterObjectClass.getDeclaredFields();
FiledEncryptDecrypt.encrypt(declaredFields, tempObject);
entry.setValue(tempObject);
}
}
}
}
}
return invocation.proceed();
}
@Override
public Object plugin(Object o) {
return Plugin.wrap(o, this);
}
@Override
public void setProperties(Properties properties) {
}
}
/**
* 查询结果解密插件
* method = "handleResultSets" 注册到结果集处理环节
*/
@Intercepts({
@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})
})
public class ReadInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
//取出查询的结果
Object resultObject = invocation.proceed();
if (Objects.isNull(resultObject)) {
return null;
}
//基于selectList
if (resultObject instanceof ArrayList) {
ArrayList resultList = (ArrayList) resultObject;
if (!CollectionUtils.isEmpty(resultList) && needToDecrypt(resultList.get(0))) {
for (Object result : resultList) {
//逐一解密
FiledEncryptDecrypt.decrypt(result);
}
}
//基于selectOne
} else {
if (needToDecrypt(resultObject)) {
FiledEncryptDecrypt.decrypt(resultObject);
}
}
return resultObject;
}
private boolean needToDecrypt(Object object) {
Class<?> objectClass = object.getClass();
EncryptDecryptData sensitiveData = AnnotationUtils.findAnnotation(objectClass, EncryptDecryptData.class);
return Objects.nonNull(sensitiveData);
}
@Override
public Object plugin(Object o) {
return Plugin.wrap(o, this);
}
@Override
public void setProperties(Properties properties) {
}
}
最后在mybatis-config.xml中注册插件
<plugins>
<plugin interceptor="com.xx.ReadInterceptor" />
<plugin interceptor="com.xx.SetParameterInterceptor" />
</plugins>