【JAVA技术】mybatis 数据库敏感字段加解密方案
引言:自从有公司项目前2年做了三级等保,每年一度例行公事,昨天继续配合做等保测试。这2天比较忙,这里整理之前写的一篇等保技术文章。
正文:
现在公司项目基本用mybatis实现,但由于项目跨度年份比较久, 技术实现分为2种,早期项目是用mybatis-generator实现, 新项目是用mybatis-plus实现, 这里讲一下分别用不同的方式实现数据库敏感字段加解密。
敏感字段加解密,比如手机号、姓名、身份证、住址、邮箱,原理比较简单,在数据插入前对数据进行加密,数据查询出来的时候再解密。
1、先说加密算法, 已有的老项目,可以用AES,毕竟修复数据可以通过mysql的sql语法直接搞定。直接上sql。新项目,推荐用国密SM4
select hex(AES_ENCRYPT('hello world! 张三-123', '1234567890123456'));select AES_DECRYPT(UNHEX('42A8DAF413119F35A746CD231A955E7501C1E3E3D785CD892795EAE387B379BF'),'1234567890123456') ;
2、mybatis-generator项目,实现mybatis的typeHandler
/*** 注意不能把 BaseTypeHandler 改为 BaseTypeHandler<String> ,如果改为 BaseTypeHandler<String> 的话,* 所有 String 类型的字段都会给加密了。改为 BaseTypeHandler 在需要加密的地方加上 typeHandler 就好了*/
public class EncryptTypeHandler extends BaseTypeHandler {@SneakyThrows@Overridepublic void setNonNullParameter(PreparedStatement preparedStatement, int i, Object o, JdbcType jdbcType) {preparedStatement.setString(i, BizUtil.encryptData_ECB((String) o));}/*** 用于在Mybatis获取数据结果集时如何把数据库类型转换为对应的Java类型** @param rs 当前的结果集* @param columnName 当前的字段名称* @return 转换后的Java对象* @throws SQLException*/@SneakyThrows@Overridepublic String getNullableResult(ResultSet rs, String columnName) {String r = rs.getString(columnName);return r == null ? null : BizUtil.decryptData_ECB(r);}/*** 用于在Mybatis通过字段位置获取字段数据时把数据库类型转换为对应的Java类型** @param rs 当前的结果集* @param columnIndex 当前字段的位置* @return 转换后的Java对象* @throws SQLException*/@SneakyThrows@Overridepublic String getNullableResult(ResultSet rs, int columnIndex) {String r = rs.getString(columnIndex);return r == null ? null : BizUtil.decryptData_ECB(r);}/*** 用于Mybatis在调用存储过程后把数据库类型的数据转换为对应的Java类型** @param cs 当前的CallableStatement执行后的CallableStatement* @param columnIndex 当前输出参数的位置* @return* @throws SQLException*/@SneakyThrows@Overridepublic String getNullableResult(CallableStatement cs, int columnIndex) {String r = cs.getString(columnIndex);// 兼容待修复的数据return r == null ? null : BizUtil.decryptData_ECB(r);}}
mapper文件对应加密字段属性实现
<result column="user_mobile" jdbcType="VARCHAR"property="userMobile"typeHandler="com.company.group.project.util.mybatis.EncryptTypeHandler"/>
插入、更新对应属性加上
#{userMobile,jdbcType=VARCHAR, typeHandler=com.company.group.project.util.mybatis.EncryptTypeHandler},
老项目埋坑点:
a、手写的mapper 人肉处理,通过建resultMap设置属性
b、敏感字段作为参数搜索,必须加密后传递查询。 正常来说,在baseService做 selectByExample封装, 不少同学之前在extendService甚至更高层级操作了mapper, 改的苦笑不得
3、mybatis-plus项目, 之前mybatis版本之前用的低版本3.1,为了用上mybatis-plus 3.4的JsqlParserSupport等,把springboot1.5.x 升级到了springboot2.x, 这个过程走了一些弯路,所幸成功了。
mybatis-plus主要通过对象属性注解反射实现的,参照了网上的一些代码,代码改动量相对比较少。 敏感字段作为参数搜索,必须加密传递查询。
加密插件:
public class EncryptInterceptor extends JsqlParserSupport implements InnerInterceptor {/*** 变量占位符正则*/private static final Pattern PARAM_PAIRS_RE = Pattern.compile("#\\{ew\\.paramNameValuePairs\\.(" + Constants.WRAPPER_PARAM + "\\d+)\\}");@Overridepublic void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) {InnerInterceptor.super.beforePrepare(sh, connection, transactionTimeout);//System.out.println("================================================================================beforePrepare");}/*** 如果查询条件是加密数据列,那么要将查询条件进行数据加密。* 例如,手机号加密存储后,按手机号查询时,先把要查询的手机号进行加密,再和数据库存储的加密数据进行匹配*/@Overridepublic void beforeQuery(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {//System.out.println("================================================================================beforeQuery");if (Objects.isNull(parameterObject)) {return;}if (!(parameterObject instanceof Map)) {return;}Map paramMap = (Map) parameterObject;// 参数去重,否则多次加密会导致查询失败Set set = (Set) paramMap.values().stream().collect(Collectors.toSet());for (Object param : set) {/*** 仅支持类型是自定义Entity的参数,不支持mapper的参数是QueryWrapper、String等,例如:** 支持:findList(@Param(value = "query") UserEntity query);* 支持:findPage(@Param(value = "query") UserEntity query, Page<UserEntity> page);** 不支持:findList(@Param(value = "mobile") String mobile);* 不支持:findList(QueryWrapper wrapper);*/if (param instanceof AbstractWrapper || param instanceof String) {// Wrapper、String类型查询参数,无法获取参数变量上的注解,无法确认是否需要加密,因此不做判断continue;}if (needToDecrypt(param.getClass())) {encryptEntity(param);}}}@Overridepublic void beforeUpdate(Executor executor, MappedStatement mappedStatement, Object parameterObject) throws SQLException {if (Objects.isNull(parameterObject)) {return;}// 通过MybatisPlus自带API(save、insert等)新增数据库时if (!(parameterObject instanceof Map)) {if (needToDecrypt(parameterObject.getClass())) {encryptEntity(parameterObject);}return;}Map paramMap = (Map) parameterObject;Object param;// 通过MybatisPlus自带API(update、updateById等)修改数据库时if (paramMap.containsKey(Constants.ENTITY) && null != (param = paramMap.get(Constants.ENTITY))) {if (needToDecrypt(param.getClass())) {encryptEntity(param);}return;}// 通过在mapper.xml中自定义API修改数据库时if (paramMap.containsKey("entity") && null != (param = paramMap.get("entity"))) {if (needToDecrypt(param.getClass())) {encryptEntity(param);}return;}// 通过UpdateWrapper、LambdaUpdateWrapper修改数据库时if (paramMap.containsKey(Constants.WRAPPER) && null != (param = paramMap.get(Constants.WRAPPER))) {if (param instanceof Update && param instanceof AbstractWrapper) {Class<?> entityClass = mappedStatement.getParameterMap().getType();if (needToDecrypt(entityClass)) {encryptWrapper(entityClass, param);}}return;}}/*** 校验该实例的类是否被@EncryptedTable所注解*/private boolean needToDecrypt(Class<?> objectClass) {EncryptedTable sensitiveData = AnnotationUtils.findAnnotation(objectClass, EncryptedTable.class);return Objects.nonNull(sensitiveData);}/*** 通过API(save、updateById等)修改数据库时** @param parameter*/private void encryptEntity(Object parameter) {//取出parameterType的类Class<?> resultClass = parameter.getClass();Field[] declaredFields = resultClass.getDeclaredFields();for (Field field : declaredFields) {//取出所有被EncryptedColumn注解的字段EncryptedColumn sensitiveField = field.getAnnotation(EncryptedColumn.class);if (!Objects.isNull(sensitiveField)) {field.setAccessible(true);Object object = null;try {object = field.get(parameter);} catch (IllegalAccessException e) {continue;}//只支持String的解密if (object instanceof String) {String value = (String) object;//对注解的字段进行逐一加密try {field.set(parameter, AESUtil.encrypt(value));} catch (IllegalAccessException e) {continue;}}}}}/*** 通过UpdateWrapper、LambdaUpdateWrapper修改数据库时** @param entityClass* @param ewParam*/private void encryptWrapper(Class<?> entityClass, Object ewParam) {AbstractWrapper updateWrapper = (AbstractWrapper) ewParam;String sqlSet = updateWrapper.getSqlSet();if (StringUtils.isBlank(sqlSet)) {return;}String[] elArr = sqlSet.split(",");Map<String, String> propMap = new HashMap<>(elArr.length);Arrays.stream(elArr).forEach(el -> {String[] elPart = el.split("=");propMap.put(elPart[0], elPart[1]);});//取出parameterType的类Field[] declaredFields = entityClass.getDeclaredFields();for (Field field : declaredFields) {//取出所有被EncryptedColumn注解的字段EncryptedColumn sensitiveField = field.getAnnotation(EncryptedColumn.class);if (Objects.isNull(sensitiveField)) {continue;}String el = propMap.get(field.getName());try {Matcher matcher = PARAM_PAIRS_RE.matcher(el);if (matcher.matches()) {String valueKey = matcher.group(1);Object value = updateWrapper.getParamNameValuePairs().get(valueKey);updateWrapper.getParamNameValuePairs().put(valueKey, AESUtil.encrypt(value.toString()));}}catch (Exception e){logger.error("{}", e);}}Method[] declaredMethods = entityClass.getDeclaredMethods();for (Method method : declaredMethods) {//取出所有被EncryptedColumn注解的字段EncryptedColumn sensitiveField = method.getAnnotation(EncryptedColumn.class);if (Objects.isNull(sensitiveField)) {continue;}String el = propMap.get(method.getName());try {Matcher matcher = PARAM_PAIRS_RE.matcher(el);if (matcher.matches()) {String valueKey = matcher.group(1);Object value = updateWrapper.getParamNameValuePairs().get(valueKey);updateWrapper.getParamNameValuePairs().put(valueKey, AESUtil.encrypt(value.toString()));}}catch (Exception e){logger.error("{}",e);}}}
}
解密插件:
@Intercepts({@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})
})
@Component
public class DecryptInterceptor implements Interceptor {@Overridepublic Object intercept(Invocation invocation) throws Throwable {Object resultObject = invocation.proceed();if (Objects.isNull(resultObject)) {return null;}if (resultObject instanceof ArrayList) {//基于selectListArrayList resultList = (ArrayList) resultObject;if (!resultList.isEmpty() && needToDecrypt(resultList.get(0))) {for (Object result : resultList) {//逐一解密decrypt(result);}}} else if (needToDecrypt(resultObject)) {//基于selectOnedecrypt(resultObject);}return resultObject;}/*** 校验该实例的类是否被@EncryptedTable所注解*/private boolean needToDecrypt(Object object) {Class<?> objectClass = object.getClass();EncryptedTable sensitiveData = AnnotationUtils.findAnnotation(objectClass, EncryptedTable.class);return Objects.nonNull(sensitiveData);}@Overridepublic Object plugin(Object o) {return Plugin.wrap(o, this);}private <T> T decrypt(T result) throws Exception {//取出resultType的类Class<?> resultClass = result.getClass();Field[] declaredFields = resultClass.getDeclaredFields();for (Field field : declaredFields) {//取出所有被EncryptedColumn注解的字段EncryptedColumn sensitiveField = field.getAnnotation(EncryptedColumn.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, AESUtil.decrypt(value));}}}return result;}
}
mybatis配置
@Configuration(proxyBeanMethods = false)
public class MybatisPlusConfig {/*** 单页分页条数限制(默认无限制,参见 插件#handlerLimit 方法)*/private static final Long MAX_LIMIT = 1000L;@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor() {MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();// 加解密拦截器interceptor.addInnerInterceptor(new EncryptInterceptor());// 分页拦截PaginationInnerInterceptor paginationInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);paginationInterceptor.setMaxLimit(MAX_LIMIT);interceptor.addInnerInterceptor(paginationInterceptor);// 乐观锁拦截interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());return interceptor;}}
model对象demo
@EncryptedTable
@TableEventListen({SqlCommandType.INSERT, SqlCommandType.UPDATE})
@Data
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
@TableName("db_demo")
public class Demo extends BaseEntity<Demo> {private static final long serialVersionUID = 1L;@EncryptedColumnprivate String mobile;@Overrideprotected Serializable pkVal() {return null;}}
遇到的坑:
1、mybatis-plus的 service 低版本和高版本默认值不一样getOne(queryWrapper, false); 高版本默认是抛异常
2、针对对象反射,比如更新后查询这种,最好是更新 和查询分别用一个对象。
原文链接:【JAVA技术】mybatis 数据库敏感字段加解密方案
相关文章:
【JAVA技术】mybatis 数据库敏感字段加解密方案
引言:自从有公司项目前2年做了三级等保,每年一度例行公事,昨天继续配合做等保测试。这2天比较忙,这里整理之前写的一篇等保技术文章。 正文: 现在公司项目基本用mybatis实现,但由于项目跨度年份比较久&…...
Collections工具类及其案例
package exercise;public class Demo1 {public static void main(String[] args) {//可变参数//方法形参的个数是可以发生变化的//格式:属性类型...名字//int...argsint sum getSum(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);System.out.println(sum);}//底层:可…...
Duck Bro的第512天创作纪念日
Tips:发布的文章将会展示至 里程碑专区 ,也可以在 专区 内查看其他创作者的纪念日文章 我的创作纪念日第512天 文章目录 我的创作纪念日第512天一、与CSDN平台的相遇1. 为什么在CSDN这个平台进行创作?2. 创作这些文章是为了赚钱吗?…...
【机器学习】GPT-4中的机器学习如何塑造人类与AI的新对话
🚀时空传送门 🔍引言📕GPT-4概述🌹机器学习在GPT-4中的应用🚆文本生成与摘要🎈文献综述与知识图谱构建🚲情感分析与文本分类🚀搜索引擎优化💴智能客服与虚拟助手…...
晨控CK-UR12-E01与欧姆龙NX/NJ系列EtherNet/IP通讯手册
晨控CK-UR12-E01与欧姆龙NX/NJ系列EtherNet/IP通讯手册 晨控CK-UR12-E01 是天线一体式超高频读写器头,工作频率默认为902MHz~928MHz,符合EPC Global Class l Gen 2/IS0-18000-6C 标准,最大输出功率 33dBm。读卡器同时…...
模板显式、隐式实例化和(偏)特化、具体化的详细分析
最近看了<The C Programing Language>看到了模板的特化,突然想起来<C Primer>上说的显式具体化、隐式具体化、特化、偏特化、具体化等概念弄得头晕脑胀,我在网上了找了好多帖子,才把概念给理清楚。 看着这么多叫法,其…...
软件设计师笔记-计算机系统基础知识
CPU的功能 CPU(中央处理器)是计算机的核心部件,负责执行计算机的指令和处理数据。它的功能主要可以分为程序控制、操作控制、时间控制和数据处理四个方面: 程序控制:CPU的首要任务是执行存储在内存中的程序。程序控制功能确保CPU能够按照程序的指令序列,一条一条地执行。…...
flink 作业动态维护更新,不重启flink,不提交作业
Flink任务实时获取并更新规则_flink任务流实时变更-CSDN博客 一种动态更新flink任务配置的方法_flink 数据源 动态更新-CSDN博客 Flink CEP在实时风控场景的落地与优化 最佳实践 - 在SQL任务中使用Flink CEP - 《实时计算用户手册-v4.5.0》 Flink SQL CEP详解-CSDN博客 如…...
为何数据仓库需要“分层次”?
在数据驱动的商业世界中,数据仓库是企业决策的心脏。然而,一个高效、可扩展且易于管理的数据仓库,需要精心设计和构建。分层是构建数据仓库的关键策略之一。本文将探讨数据仓库分层的重要性以及它如何帮助企业更好地管理数据。 数据仓库分层…...
小熊家务帮day15-day18 预约下单模块(预约下单,熔断降级,支付功能,退款功能)
目录 1 预约下单1.1 需求分析1.1.1 业务流程1.1.2 订单状态 1.2 系统设计1.2.1 订单表设计1.2.2 表结构的设置 1.3 开发远程调用接口1.3.0 复习下远程调用的开发1.3.1 查询地址簿远程接口jzo2o-api工程定义接口Customer服务实现接口 1.3.2 查询服务&服务项远程接口jzo2o-ap…...
[word] word悬挂缩进怎么设置? #经验分享#职场发展#经验分享
word悬挂缩进怎么设置? 在编辑Word的时候上方会有个Word标尺,相信很多伙伴都没使用过。其实它隐藏着很多好用的功能,今天就给大家分享下利用这个word标尺的悬挂缩进怎么设置,一起来看看吧! 1、悬挂缩进 选中全文&…...
6-Maven的使用
6-Maven的使用 常用maven命令 //常用maven命令 mvn -v //查看版本 mvn archetype:create //创建 Maven 项目 mvn compile //编译源代码 mvn test-compile //编译测试代码 mvn test //运行应用程序中的单元测试 mvn site //生成项目相关信息的网站 mvn package //依据项目生成 …...
WPF真入门教程32--WPF数字大屏项目实干
1、项目背景 WPF (Windows Presentation Foundation) 是微软的一个框架,用于构建桌面客户端应用程序,它支持富互联网应用程序(RIA)的开发。在数字大屏应用中,WPF可以用来构建复杂的用户界面,展示庞大的数据…...
数据可视化Python实现超详解【数据分析】
各位大佬好 ,这里是阿川的博客,祝您变得更强 个人主页:在线OJ的阿川 大佬的支持和鼓励,将是我成长路上最大的动力 阿川水平有限,如有错误,欢迎大佬指正 Python 初阶 Python–语言基础与由来介绍 Python–…...
Maxkb玩转大语言模型
Maxkb玩转大语言模型 随着国外大语言模型llama3的发布,搭建本地个人免费“人工智能”变得越来越简单,今天博主分享使用Max搭建本地的个人聊天式对话及个人本地知识域的搭建。 1.安装Maxkb开源应用 github docker快速安装 docker run -d --namemaxkb -p 8…...
React Hooks 封装可粘贴图片的输入框组件(wangeditor)
需求是需要一个文本框 但是可以支持右键或者ctrlv粘贴图片,原生js很麻烦,那不如用插件来实现吧~我这里用的wangeditor插件,初次写初次用,可能不太好,但目前是可以达到实现需求的一个效果啦!后面再改进吧~ …...
Wireshark TS | 应用传输丢包问题
问题背景 仍然是来自于朋友分享的一个案例,实际案例不难,原因也就是互联网线路丢包产生的重传问题。但从一开始只看到数据包截图的判断结果,和最后拿到实际数据包的分析结果,却不是一个结论,方向有点跑偏,…...
架构设计-web项目中跨域问题涉及到的后端和前端配置
WEB软件项目中经常会遇到跨域问题,解决方案早已是业内的共识,简要记录主流的处理方式: 跨域感知session需要解决两个问题: 1. 跨域问题 2. 跨域cookie传输问题 跨域问题 解决跨域问题有很多种方式,如使用springboot…...
==Redis淘汰策略(内存满了触发)==
好的,面试官。这个问题我需要从三个方面来回答。第一个方面: 当 Redis 使用的内存达到 maxmemory 参数配置的阈值的时候,Redis 就会根据配置的内存淘汰策略。 把访问频率不高的 key 从内存中移除。maxmemory 默认情况是当前服务器的最大内存…...
2024年高考作文考人工智能,人工智能写作文能否得高分
前言 众所周知,今年全国一卷考的是人工智能,那么,我们来测试一下,国内几家厉害的人工智能他们的作答情况,以及能取得多少高分呢。由于篇幅有限,我这里只测试一个高考真题,我们这里用百度的文心…...
leetcodeSQL解题:3564. 季节性销售分析
leetcodeSQL解题:3564. 季节性销售分析 题目: 表:sales ---------------------- | Column Name | Type | ---------------------- | sale_id | int | | product_id | int | | sale_date | date | | quantity | int | | price | decimal | -…...
Java入门学习详细版(一)
大家好,Java 学习是一个系统学习的过程,核心原则就是“理论 实践 坚持”,并且需循序渐进,不可过于着急,本篇文章推出的这份详细入门学习资料将带大家从零基础开始,逐步掌握 Java 的核心概念和编程技能。 …...
Map相关知识
数据结构 二叉树 二叉树,顾名思义,每个节点最多有两个“叉”,也就是两个子节点,分别是左子 节点和右子节点。不过,二叉树并不要求每个节点都有两个子节点,有的节点只 有左子节点,有的节点只有…...
Unity | AmplifyShaderEditor插件基础(第七集:平面波动shader)
目录 一、👋🏻前言 二、😈sinx波动的基本原理 三、😈波动起来 1.sinx节点介绍 2.vertexPosition 3.集成Vector3 a.节点Append b.连起来 4.波动起来 a.波动的原理 b.时间节点 c.sinx的处理 四、🌊波动优化…...
在Ubuntu24上采用Wine打开SourceInsight
1. 安装wine sudo apt install wine 2. 安装32位库支持,SourceInsight是32位程序 sudo dpkg --add-architecture i386 sudo apt update sudo apt install wine32:i386 3. 验证安装 wine --version 4. 安装必要的字体和库(解决显示问题) sudo apt install fonts-wqy…...
安全突围:重塑内生安全体系:齐向东在2025年BCS大会的演讲
文章目录 前言第一部分:体系力量是突围之钥第一重困境是体系思想落地不畅。第二重困境是大小体系融合瓶颈。第三重困境是“小体系”运营梗阻。 第二部分:体系矛盾是突围之障一是数据孤岛的障碍。二是投入不足的障碍。三是新旧兼容难的障碍。 第三部分&am…...
Cilium动手实验室: 精通之旅---13.Cilium LoadBalancer IPAM and L2 Service Announcement
Cilium动手实验室: 精通之旅---13.Cilium LoadBalancer IPAM and L2 Service Announcement 1. LAB环境2. L2公告策略2.1 部署Death Star2.2 访问服务2.3 部署L2公告策略2.4 服务宣告 3. 可视化 ARP 流量3.1 部署新服务3.2 准备可视化3.3 再次请求 4. 自动IPAM4.1 IPAM Pool4.2 …...
学习一下用鸿蒙DevEco Studio HarmonyOS5实现百度地图
在鸿蒙(HarmonyOS5)中集成百度地图,可以通过以下步骤和技术方案实现。结合鸿蒙的分布式能力和百度地图的API,可以构建跨设备的定位、导航和地图展示功能。 1. 鸿蒙环境准备 开发工具:下载安装 De…...
HybridVLA——让单一LLM同时具备扩散和自回归动作预测能力:训练时既扩散也回归,但推理时则扩散
前言 如上一篇文章《dexcap升级版之DexWild》中的前言部分所说,在叠衣服的过程中,我会带着团队对比各种模型、方法、策略,毕竟针对各个场景始终寻找更优的解决方案,是我个人和我司「七月在线」的职责之一 且个人认为,…...
云原生周刊:k0s 成为 CNCF 沙箱项目
开源项目推荐 HAMi HAMi(原名 k8s‑vGPU‑scheduler)是一款 CNCF Sandbox 级别的开源 K8s 中间件,通过虚拟化 GPU/NPU 等异构设备并支持内存、计算核心时间片隔离及共享调度,为容器提供统一接口,实现细粒度资源配额…...
