【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年高考作文考人工智能,人工智能写作文能否得高分
前言 众所周知,今年全国一卷考的是人工智能,那么,我们来测试一下,国内几家厉害的人工智能他们的作答情况,以及能取得多少高分呢。由于篇幅有限,我这里只测试一个高考真题,我们这里用百度的文心…...
Linux链表操作全解析
Linux C语言链表深度解析与实战技巧 一、链表基础概念与内核链表优势1.1 为什么使用链表?1.2 Linux 内核链表与用户态链表的区别 二、内核链表结构与宏解析常用宏/函数 三、内核链表的优点四、用户态链表示例五、双向循环链表在内核中的实现优势5.1 插入效率5.2 安全…...
云启出海,智联未来|阿里云网络「企业出海」系列客户沙龙上海站圆满落地
借阿里云中企出海大会的东风,以**「云启出海,智联未来|打造安全可靠的出海云网络引擎」为主题的阿里云企业出海客户沙龙云网络&安全专场于5.28日下午在上海顺利举办,现场吸引了来自携程、小红书、米哈游、哔哩哔哩、波克城市、…...
Redis相关知识总结(缓存雪崩,缓存穿透,缓存击穿,Redis实现分布式锁,如何保持数据库和缓存一致)
文章目录 1.什么是Redis?2.为什么要使用redis作为mysql的缓存?3.什么是缓存雪崩、缓存穿透、缓存击穿?3.1缓存雪崩3.1.1 大量缓存同时过期3.1.2 Redis宕机 3.2 缓存击穿3.3 缓存穿透3.4 总结 4. 数据库和缓存如何保持一致性5. Redis实现分布式…...
可靠性+灵活性:电力载波技术在楼宇自控中的核心价值
可靠性灵活性:电力载波技术在楼宇自控中的核心价值 在智能楼宇的自动化控制中,电力载波技术(PLC)凭借其独特的优势,正成为构建高效、稳定、灵活系统的核心解决方案。它利用现有电力线路传输数据,无需额外布…...
鸿蒙中用HarmonyOS SDK应用服务 HarmonyOS5开发一个医院查看报告小程序
一、开发环境准备 工具安装: 下载安装DevEco Studio 4.0(支持HarmonyOS 5)配置HarmonyOS SDK 5.0确保Node.js版本≥14 项目初始化: ohpm init harmony/hospital-report-app 二、核心功能模块实现 1. 报告列表…...
Neo4j 集群管理:原理、技术与最佳实践深度解析
Neo4j 的集群技术是其企业级高可用性、可扩展性和容错能力的核心。通过深入分析官方文档,本文将系统阐述其集群管理的核心原理、关键技术、实用技巧和行业最佳实践。 Neo4j 的 Causal Clustering 架构提供了一个强大而灵活的基石,用于构建高可用、可扩展且一致的图数据库服务…...
【C++从零实现Json-Rpc框架】第六弹 —— 服务端模块划分
一、项目背景回顾 前五弹完成了Json-Rpc协议解析、请求处理、客户端调用等基础模块搭建。 本弹重点聚焦于服务端的模块划分与架构设计,提升代码结构的可维护性与扩展性。 二、服务端模块设计目标 高内聚低耦合:各模块职责清晰,便于独立开发…...
mysql已经安装,但是通过rpm -q 没有找mysql相关的已安装包
文章目录 现象:mysql已经安装,但是通过rpm -q 没有找mysql相关的已安装包遇到 rpm 命令找不到已经安装的 MySQL 包时,可能是因为以下几个原因:1.MySQL 不是通过 RPM 包安装的2.RPM 数据库损坏3.使用了不同的包名或路径4.使用其他包…...
CMake控制VS2022项目文件分组
我们可以通过 CMake 控制源文件的组织结构,使它们在 VS 解决方案资源管理器中以“组”(Filter)的形式进行分类展示。 🎯 目标 通过 CMake 脚本将 .cpp、.h 等源文件分组显示在 Visual Studio 2022 的解决方案资源管理器中。 ✅ 支持的方法汇总(共4种) 方法描述是否推荐…...
ABAP设计模式之---“简单设计原则(Simple Design)”
“Simple Design”(简单设计)是软件开发中的一个重要理念,倡导以最简单的方式实现软件功能,以确保代码清晰易懂、易维护,并在项目需求变化时能够快速适应。 其核心目标是避免复杂和过度设计,遵循“让事情保…...
