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

背景

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

思路

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

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

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

实现

理论知识储备

  • mybatis interceptor执行流程与原理(MyBatis 插件之拦截器(Interceptor)_M义薄云天的博客-CSDN博客_mybatis 拦截器)
  • 自定义注解
  • 反射
  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";@Overridepublic 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-processorif (needHandleExample(argClassName)){handleExample(args);}else{//自定义的mapper文件增删查改参数处理handleCustomizeMapperParams(args);}//update 方法if (args.length == 2 ){return invocation.proceed();}//兼容pagehelperif(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;}}//兼容processorprivate void handleExample(Object[] args) throws Exception {Object arg = args[1];Class<?> argClass = arg.getClass();String argClassName = argClass.getName();//兼容 mybatis-processorif (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];//操作 >=< likeString 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();}@Overridepublic Object plugin(Object target) {return Plugin.wrap(target, this);}@Overridepublic 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对象字段都需要标注)

        @Encryptprivate 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/

基于mybatis的数据库脱敏相关推荐

  1. java毕业设计基于的测试项目管理平台Mybatis+系统+数据库+调试部署

    java毕业设计基于的测试项目管理平台Mybatis+系统+数据库+调试部署 java毕业设计基于的测试项目管理平台Mybatis+系统+数据库+调试部署 本源码技术栈: 项目架构:B/S架构 开发语 ...

  2. java毕业设计基于的电商平台的设计与实现Mybatis+系统+数据库+调试部署

    java毕业设计基于的电商平台的设计与实现Mybatis+系统+数据库+调试部署 java毕业设计基于的电商平台的设计与实现Mybatis+系统+数据库+调试部署 本源码技术栈: 项目架构:B/S架构 ...

  3. java毕业设计基于web的学校工资管理系统Mybatis+系统+数据库+调试部署

    java毕业设计基于web的学校工资管理系统Mybatis+系统+数据库+调试部署 java毕业设计基于web的学校工资管理系统Mybatis+系统+数据库+调试部署 本源码技术栈: 项目架构:B/S ...

  4. java毕业设计基于的高校学生综合素质评价系统Mybatis+系统+数据库+调试部署

    java毕业设计基于的高校学生综合素质评价系统Mybatis+系统+数据库+调试部署 java毕业设计基于的高校学生综合素质评价系统Mybatis+系统+数据库+调试部署 本源码技术栈: 项目架构:B ...

  5. java毕业设计基于精细化考核的离散数学课程教学目标达成系统Mybatis+系统+数据库+调试部署

    java毕业设计基于精细化考核的离散数学课程教学目标达成系统Mybatis+系统+数据库+调试部署 java毕业设计基于精细化考核的离散数学课程教学目标达成系统Mybatis+系统+数据库+调试部署 ...

  6. JAVA基于的智慧小区计算机毕业设计Mybatis+系统+数据库+调试部署

    JAVA基于的智慧小区计算机毕业设计Mybatis+系统+数据库+调试部署 JAVA基于的智慧小区计算机毕业设计Mybatis+系统+数据库+调试部署 本源码技术栈: 项目架构:B/S架构 开发语言: ...

  7. java毕业设计基于的智慧小区Mybatis+系统+数据库+调试部署

    java毕业设计基于的智慧小区Mybatis+系统+数据库+调试部署 java毕业设计基于的智慧小区Mybatis+系统+数据库+调试部署 本源码技术栈: 项目架构:B/S架构 开发语言:Java语言 ...

  8. java毕业设计基于网络平台个人博客系统Mybatis+系统+数据库+调试部署

    java毕业设计基于网络平台个人博客系统Mybatis+系统+数据库+调试部署 java毕业设计基于网络平台个人博客系统Mybatis+系统+数据库+调试部署 本源码技术栈: 项目架构:B/S架构 开 ...

  9. java毕业设计基于动漫电影网站Mybatis+系统+数据库+调试部署

    java毕业设计基于动漫电影网站Mybatis+系统+数据库+调试部署 java毕业设计基于动漫电影网站Mybatis+系统+数据库+调试部署 本源码技术栈: 项目架构:B/S架构 开发语言:Java ...

  10. list mybatis 接收 类型_基于mybatis拦截器实现的一款简易影子表自动切换插件

    近期因工作需要,小编基于mybatis拦截器开发了一款简易影子表自动切换插件,可以根据配置实现动态修改表名,即将对原source table表的操作自动切换到对target table表的操作.该插件 ...

最新文章

  1. 新人赛《金融风控贷款违约》避坑指南!
  2. ENISA报告:ICS-SCADA防护建议
  3. php实现两个大整数求和,PHP计算两个特别大的整数实例代码
  4. “呵呵呵”之后 B站又申请了“一键三连”商标
  5. mysql检索整数_MyBatis从MySql DB中检索整数为Enum
  6. php微信公众号测试号token配置失败,微信公众号Token配置失败解决办法
  7. Matlab读取TXT文本文件通用程序
  8. dcdc模块降额设计_大功率IGBT模块及驱动技术
  9. Linux 配置双网卡,同时访问内外网
  10. ubuntu20.04不能切换输入法
  11. 把Android API文档的颜色改成不易疲劳的绿豆沙颜色
  12. python爬虫企业工商信息_Python 爬虫进阶必备 | 企业信用公示系统公告加密解析...
  13. 什么样人适合学平面设计?零门槛入门工具收藏
  14. 京东全民营业拿金币之辅助脚本网页版
  15. 中文linux(Ubuntu)下让date命令显示英语日期
  16. 【JavaScript 】for 循环
  17. 网页中在线玩圆桌骑士
  18. 问题 D: DD_BOND看到的hcy
  19. 亚马逊、eBay、速卖通、lazada、Shopee等跨境平台,如何快速打造爆款
  20. QCC3031 32M Flash精简 (QCC30xx系列应该通用)

热门文章

  1. js增量更新算法研究
  2. pytorch函数测试
  3. linux中 qt安装教程视频,Linux 下QT安装教程
  4. ADS2015 for linux 安装教程
  5. JS回调函数、真实举例
  6. 地图上分成一块一块区域 高德地图_高德地图行政区域划分问题有点搞不懂
  7. Zmap详细用户手册及DDOS的可行性
  8. Python数据分析入门(1)——数据分析基础步骤知识
  9. docker添加新的环境变量_DockerFile 设置环境变量
  10. [4G5G专题-45]:物理层-基带子载波数字调制解调(星座图, 相位调制PSK, 正交幅度相位调制QAM)