一种使用mybatis进行脱敏的思路

488 阅读4分钟

通过mybatis数据脱敏的大致流程

现在对数据安全是越来越重视了,最近也做了几个存有大量用户个人信息的项目,考虑到安全性,相关的敏感字段是不能以明文的方式保存到数据库的,经过一番资料的查找(上网划水)确定了如下的全局加密解密的处理。

  1. 通过自定义的mybatis插件可以对sql查询参数进行拦截修改
  2. 通过自定义的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>

其他的数据脱敏工具推荐:ShardingSphere,支持读写分离、数据分片、分布式事务、数据脱敏等诸多功能