基于mybatis的数据库脱敏

2023-11-05

  • 背景
  • 思路
  • 实现
  • 思考

背景

最近接到需求需要对数据库中的电话、身份证号等敏感信息进行脱敏加密处理,再加上之前面试时也被问到相关问题,所有在此记录。脱敏对象是数据库的字段,所以在数据库存取的出入口进行加解密操作是最合适的,项目中使用mybatis作为ORM框架,所以使用基于mybatis的数据库脱敏。

思路

对数据库中的数据进行脱敏处理,核心思想就是在入库时对敏感字段进行加密,在出库时对敏感字段解密。看清了这个问题,我们的关注点就有两个。

  1. 何时?入库和出库
  2. 何地?入参和查询结果

mybatis框架中的plugin,能够对上面两个关注点进行很好的控制,再结合自定义注解,对需要脱敏的字段进行标注,就能够满足我们的需求。

实现

理论知识储备

  1. 定义自定义注解,用于标识敏感字段

    /**
     * 标识字段入库信息需要加密
     * @see com.vcg.veer.sign.utils.DesUtils
     * @author zhouyao
     * @date 2021/10/27 9:22 上午
     **/
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.FIELD)
    public @interface Encrypt {
    }
    
    
  2. mybatis插件逻辑(对项目中使用的pagehelper和mybatis-processor插件兼容)

    
    /**
     * 敏感字段入库、出库处理
     *
     * @author zhouyao
     * @date 2021/10/27 9:25 上午
     **/
    @Intercepts(
            {
                    @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
                    @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
            }
    )
    public class EncryptInterceptor implements Interceptor {
    
        private final String EXAMPLE_SUFFIX = "Example";
    
    
        @Override
        public Object intercept(Invocation invocation) throws Throwable {
            Object[] args = invocation.getArgs();
            MappedStatement ms = (MappedStatement) args[0];
            Object parameter = args[1];
            Class<?> argClass = parameter.getClass();
            String argClassName = argClass.getName();
            //兼容mybatis-processor
            if (needHandleExample(argClassName)){
                handleExample(args);
            }else{
                //自定义的mapper文件增删查改参数处理
                handleCustomizeMapperParams(args);
            }
    
            //update 方法
            if (args.length == 2 ){
                return invocation.proceed();
            }
            //兼容pagehelper
            if(args.length == 4){
                RowBounds rowBounds = (RowBounds) args[2];
                ResultHandler resultHandler = (ResultHandler) args[3];
                Executor executor = (Executor) invocation.getTarget();
                CacheKey cacheKey;
                BoundSql boundSql;
                //4 个参数时
                boundSql = ms.getBoundSql(parameter);
                cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
                List<Object> queryResult = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
                //处理需要解密的字段
                decryptFieldIfNeeded(queryResult);
                return queryResult;
            }
    
            return invocation.proceed();
        }
    
        /**
         * 对数据进行解密
         * @param queryResult
         */
        private void decryptFieldIfNeeded(List<Object> queryResult) throws IllegalAccessException {
            if (CollectionUtils.isEmpty(queryResult)) {
                return;
            }
            Object o1 = queryResult.get(0);
            Class<?> resultClass = o1.getClass();
            Field[] resultClassDeclaredFields = resultClass.getDeclaredFields();
            List<Field> needDecryptFieldList = new ArrayList<>();
            for (Field resultClassDeclaredField : resultClassDeclaredFields) {
                Encrypt encrypt = resultClassDeclaredField.getDeclaredAnnotation(Encrypt.class);
                if (encrypt == null){
                    continue;
                }
                Class<?> type = resultClassDeclaredField.getType();
                if (!String.class.isAssignableFrom(type)){
                    throw new IllegalStateException("@Encrypt should annotated on String field");
                }
                needDecryptFieldList.add(resultClassDeclaredField);
            }
            if (CollectionUtils.isEmpty(needDecryptFieldList)){
                return;
            }
            for (Field field : needDecryptFieldList) {
                field.setAccessible(true);
                for (Object o : queryResult) {
                    String fieldValue = (String) field.get(o);
                    if (!StringUtils.hasText(fieldValue)){
                        continue;
                    }
                    field.set(o,DesUtils.decrypt(fieldValue));
                }
            }
        }
    
        /**
         * 处理自定义mapper参数
         * @param args
         */
        private void handleCustomizeMapperParams(Object[] args) throws Exception {
            Object param = args[1];
            encryptObjectField(param);
        }
    
        private void encryptObjectField(Object param) throws Exception {
            Class<?> paramClass = param.getClass();
            //mybatis @param注解会处理为多参数
            if (Map.class.isAssignableFrom(paramClass)){
                Map mapParam = (Map) param;
                Set<Object> params = new HashSet<>();
                params.addAll(mapParam.values());
                for (Object o : params) {
                    encryptObjectField(o);
                }
                return;
            }
            Field[] paramClassDeclaredFields = paramClass.getDeclaredFields();
            // 遍历参数的所有字段查找需要加密的字段
            for (Field paramClassDeclaredField : paramClassDeclaredFields) {
                Encrypt encrypt = paramClassDeclaredField.getDeclaredAnnotation(Encrypt.class);
                if (encrypt != null){
                    //加密
                    encryptField(param,paramClassDeclaredField);
                }
            }
        }
    
        /**
         * 给指定字段加密
         * @param targetObj
         * @param paramClassDeclaredField
         */
        private void encryptField(Object targetObj, Field paramClassDeclaredField) throws Exception {
            paramClassDeclaredField.setAccessible(true);
            Class<?> type = paramClassDeclaredField.getType();
            Object fieldValue = paramClassDeclaredField.get(targetObj);
            if (fieldValue == null){
                return;
            }
    
            if (Collection.class.isAssignableFrom(type)) {
                try {
                    Collection<String> collection = (Collection<String>) fieldValue;
                    List<String> tempList = new ArrayList<>();
                    Iterator<String> iterator = collection.iterator();
                    while (iterator.hasNext()) {
                        String next = iterator.next();
                        tempList.add(DesUtils.encrypt(next));
                        iterator.remove();
                    }
                    collection.addAll(tempList);
                }catch (Exception ex){
                    //加密字段参数只支持String类型
                    throw new IllegalArgumentException("Encrypted fields only support String type");
                }
            }
            else if(String.class.isAssignableFrom(type)){
                //基础数据类型直接设值
                paramClassDeclaredField.set(targetObj, DesUtils.encrypt(fieldValue.toString()));
            }
            else if (isBasicType(type)) {
                //加密字段参数只支持String类型
                throw new IllegalArgumentException("Encrypted fields only support String type");
            } else {
                //递归调用
                encryptObjectField(fieldValue);
            }
        }
    
        private boolean isBasicType(Class<?> clz) {
            try {
                return ((Class) clz.getField("TYPE").get(null)).isPrimitive();
            } catch (Exception e) {
                return false;
            }
        }
    
        //兼容processor
        private void handleExample(Object[] args) throws Exception {
            Object arg = args[1];
            Class<?> argClass = arg.getClass();
            String argClassName = argClass.getName();
            //兼容 mybatis-processor
            if (argClassName.endsWith(EXAMPLE_SUFFIX)) {
                //实体类的类名
                String modelClassName = argClassName.substring(0, argClassName.length() - 7);
                Class<?> modelClass;
                try {
                    modelClass = Class.forName(modelClassName);
                }catch(ClassNotFoundException ex){
                    return;
                }
    
                Method getCriteria = argClass.getDeclaredMethod("getCriteria");
                getCriteria.setAccessible(true);
                Object criteria = getCriteria.invoke(arg);
                Class<?> criteriaClass = criteria.getClass();
                Method getAllCriteria = criteriaClass.getDeclaredMethod("getAllCriteria");
                Set<Object> criterions = (Set<Object>) getAllCriteria.invoke(criteria);
                for (Object criterionObj : criterions) {
                    Class<?> criterionClass = criterionObj.getClass();
                    Method getCondition = criterionClass.getDeclaredMethod("getCondition");
                    String condition = (String) getCondition.invoke(criterionObj);
                    //列名
                    String[] conditionParts = condition.split(" ");
                    if (conditionParts.length != 2){
                        continue;
                    }
                    String columnName = conditionParts[0];
                    //操作 >=< like
                    String operateType = conditionParts[1];
                    Field[] modelClassDeclaredFields = modelClass.getDeclaredFields();
                    for (Field modelClassDeclaredField : modelClassDeclaredFields) {
                        Column annotation = modelClassDeclaredField.getAnnotation(Column.class);
                        if (annotation == null){
                            continue;
                        }
                        if (columnName.equalsIgnoreCase(annotation.name())){
                            Encrypt encrypt = modelClassDeclaredField.getDeclaredAnnotation(Encrypt.class);
                            if (encrypt != null) {
                                //加密字段只能用等于比较
                                if (!"=".equalsIgnoreCase(operateType)) {
                                    throw new IllegalArgumentException("encrypt field only can be operate by '='");
                                }
                                Field value = criterionClass.getDeclaredField("value");
                                value.setAccessible(true);
    
                                List<Integer> list = new ArrayList<>();
                                list.add(1);
                                //重新设置参数
                                value.set(criterionObj,list);
    
                                break;
                            }
                            break;
                        }
    
                    }
                }
            }
        }
    
        /**
         * 判断是否需要处理Example类型的查询
         * @param argClassName
         * @return
         */
        private boolean needHandleExample(String argClassName) {
            return argClassName.endsWith(EXAMPLE_SUFFIX);
        }
    
        private Object decryptIfNeeded(Invocation invocation) throws InvocationTargetException, IllegalAccessException {
            return invocation.proceed();
        }
    
        @Override
        public Object plugin(Object target) {
            return Plugin.wrap(target, this);
        }
    
        @Override
        public void setProperties(Properties properties) {
            Interceptor.super.setProperties(properties);
        }
    }
    
    
  3. 插件的使用

    在项目启动时注册插件(注意,根据mybatis插件的执行原理,此插件需要在最后注册,才能保证最先解析参数)

    SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
    		bean.setDataSource(dataSource);
    		//加解密插件
    		EncryptInterceptor encryptInterceptor = new EncryptInterceptor();
    
    		//分页插件
    		PageInterceptor pageInterceptor = new PageInterceptor();
    		Properties properties = new Properties();
    		properties.setProperty("reasonable", "true");
    		properties.setProperty("supportMethodsArguments", "true");
    		properties.setProperty("returnPageInfo", "check");
    		properties.setProperty("params", "count=countSql");
    		pageInterceptor.setProperties(properties);
    
    		//添加插件
    		bean.setPlugins(pageInterceptor,encryptInterceptor);
    

    对需要加密处理的字段标注@Encrypt注解(入参和结果DTO对象字段都需要标注)

        @Encrypt
        private String mobile;
    

思考

通过mybatis的插件对数据库的增删改查实现脱敏处理还是比较简单的。重点就在于:

  1. 拦截Executor对象的query和update方法,获取查询/更新参数和查询结果集
  2. 通过反射对参数中标注自定义注解的字段进行加/解密处理

在开发过程中也遇到了由于使用了pagehelper插件,导致自定义拦截器不生效的问题,最后查阅pagehelper的文档解决了(需要根据pagehelper定义的拦截器编写规范来开发)。

完整的代码参考:

https://github.com/zhouyao423/mybatis-encrypt

参考文档:

pagehelper interceptor高级用法

https://blog.csdn.net/weixin_39494923/article/details/91534658/

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

基于mybatis的数据库脱敏 的相关文章

随机推荐

  • js 将字符串转化为number的简单方式

    str 前边加上 正负号即可 console log 11 typeof 123 number console log 11 typeof 123 number 2 就是使用运算符减 乘 除的其中一种 在不改变原值的情况下 和number类
  • 魔兽世界开服一条龙服务端架设服务器搭建

    魔兽世界开服一条龙服务端架设服务器搭建 作为魔兽世界资深玩家 你是否有想过自己开服当服主 相信能看到此篇文章的你心里是有想法的 首先你需要知道开魔兽需要准备哪些东西 我是艾西今天带您了解下魔兽世界开服需要的东西 想要搭建一个魔兽商业服 需要
  • 数字之和

    链接 https www nowcoder com questionTerminal ae759916631f4711a90c4d4d9657f4b0 toCommentId 382873 来源 牛客网 对于给定的正整数 n 计算其十进制形
  • PyQt5 组件之QSlider

    QSlider简介 QSlider控件提供一个垂直或者水平的滑动条 滑动条是一个用于控制有界值典型的控件 它允许用户沿水平或者垂直方向在某一范围内移动滑块 并将滑块所在的位置转换为一个合法范围内的整数值 有时候这中方式比输入数字或者使用Sp
  • 双系统如何在Centos7中添加GRUB2启动项

    GRUB2是Centos7的默认引导加载程序 它可以轻松地加载所有的Linux发行版和其他操作系统 它提供一个菜单显示所有的操作系统 GRUB2具有高级特性以及一些基本特性 使其在所有Linux上都有效 文内含长段代码可复制可往左滑 希望对
  • Docker保存镜像到本地并载入本地镜像文件

    目录 一 适用情况 二 镜像保存到本机 1 查看已有的镜像文件 2 将镜像保存为本地文件 保存指令一 保存指令二 测试根据镜像ID保存镜像 三 载入本地镜像 载入指令一 载入指令二 载入通过镜像ID保存的本地镜像 四 批量保存和载入镜像脚本
  • 全网最详细中英文ChatGPT-GPT-4示例文档-步骤指示智能生成从0到1快速入门——官网推荐的48种最佳应用场景(附python/node.js/curl命令源代码,小白也能学)

    从0到1快速入门步骤指示智能生成应用场景 Introduce 简介 setting 设置 Prompt 提示 Sample response 回复样本 API request 接口请求 python接口请求示例 node js接口请求示例
  • 内网Https 自签Https证书 配合Tomcat 实现内网Https详细图文

    转载地址 https www cnblogs com ChromeT p 11122480 html 目录 开始制作CA根证书 创建私钥 创建证书请求 生成CA自签证书 制作服务器证书 向 本地keystore 密钥库中导入证书 导入服务器
  • 结构方程模型_结构方程模型(SEM)到底有多厉害?

    什么是结构方程模型 结构方程模型是高级定量方法中最常见的统计方法之一 在社会科学以及经济 市场 管理等研究领域 有时需处理多个原因 多个结果的关系 或者会碰到不可直接观测的变量 即潜变量 这些都是传统的统计方法不能很好解决的问题 结构方程模
  • qt传指针

    1 传指针 需要获取并且初始化 2 接口函数应该指明其父窗口
  • 【MedusaSTears】记一次踏坑:EL表达式不支持正则_EL表达式使用函数一览表_手机/座机号码脱敏正则规则

    吃水不忘挖井人系列 EL表达式中使用函数 以下这俩正则应该都可 应对诸如 010 12345678 这种座机 以及 13312345678 这种手机号
  • 基于类属属性的多标记学习——含python代码

    本文的基本内容翻译自Lift Multi Label Learning with Label Specific Features 1 含部分本人的理解 最后附带了我复现的python代码的github链接 类属属性 所谓多标记学习是相对于单
  • 修改Nuget包默认存放路径

    默认情况下所有的Nuget包都会下载到C盘 目前我这边有几十个G的大小 这导致我C盘的c 教程容量越来越小 我们可以在Nuget config中修改package存放路径 Nuget config 在C Users UserName App
  • 解决 R 语言中遇到的奇异拟合错误(singular fit encountered)

    解决 R 语言中遇到的奇异拟合错误 singular fit encountered 在 R 语言中 当执行线性回归或拟合模型时 有时会遇到奇异拟合错误 singular fit encountered 的问题 这个错误表示拟合过程中出现了
  • 关于知识的理解与忘却

    关于知识的理解与忘却 在做项目的过程中 我会遇到一些难题 会通过baidu与google来解决 解决之后 我很少对这些知识进行总结 但是 将来又遇到这方面的知识时 我发现却很难再找到记忆中的那些文章 当初对于自己理解这些知识有很大影响的文章
  • 使用OpenCV,Python进行图像哈希(差分哈希 dHash)处理

    使用OpenCV Phthon进行图像哈希处理的一个重要应用是去除重复的图像 当你有多个相册的图片 进行合并时 so boring 有一些图片是重复的 肉眼来看太难删除了 图像哈希可以帮助你完美的解决这个问题 找到完全相同的图片 只保留一张
  • python最详细的安装与完全卸载

    一 python的安装 1 python下载 在网上 输入网址https www python org downloads 下载Windows最新的python程序 我电脑为64位 注意 不要下载32位的且尽可能不要下载3 6 x以上的版本
  • 虚幻4——蓝图在脚本之间的交互

    从虚幻3到虚幻4 之前都已经习惯了在脚本中写节点 然后在编辑器中供关卡师使用 所以对于这种开发模式 也不会有什么不适应的感觉 可视化编辑 也是以后游戏开发的重要的一环吧 但是国内游戏开发还没有这一种概念 对于关卡师 尤其懂游戏逻辑的 这是多
  • 算法 - 冒泡排序(Bubble_ Sort)

    在总结冒泡排序前 先从头开始 对冒泡排序 BubbleSort 进行一个系统性了解 冒泡排序法 又称交换排序法 是从观察水中的气泡变化构思而成的 原理是从第一个元素开始 比较相邻元素的大小 如果大小顺序不对 则进行两个数据的调换 就好像气泡
  • 基于mybatis的数据库脱敏

    背景 思路 实现 思考 背景 最近接到需求需要对数据库中的电话 身份证号等敏感信息进行脱敏加密处理 再加上之前面试时也被问到相关问题 所有在此记录 脱敏对象是数据库的字段 所以在数据库存取的出入口进行加解密操作是最合适的 项目中使用myba