【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 数据库敏感字段加解密方案

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/23987.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

chls.pro/ssl证书安装失败解决办法

我的手机是一台root&#xff0c;但未获取sustem分区的手机&#xff0c;在安装charles的 ssl证书时&#xff0c;发现在手机浏览器输入chls.pro/ssl&#xff0c;但证书一直无法下载。

数据中心网络架构设计与优化

数据中心是现代企业和组织的核心基础设施&#xff0c;它们用于存储、处理和传输大量的数据和信息。为了满足不断增长的数据需求和提供可靠的服务&#xff0c;设计和优化数据中心网络架构至关重要。 首先&#xff0c;数据中心网络架构设计需要考虑可扩展性。随着业务的增长&…

[Bug]使用Transformers 微调 Whisper出现版本不兼容的bug

错误的现象 ImportError Traceback (most recent call last) <ipython-input-20-6958d7eed552> in () from transformers import Seq2SegTrainingArguments training_args Seq2SeqTrainingArguments( output_dir"./whisper-small-…

【全开源】防伪溯源一体化管理系统源码(FastAdmin+ThinkPHP+Uniapp)

&#x1f50d;防伪溯源一体化管理系统&#xff1a;守护品质&#xff0c;追溯无忧 一款基于FastAdminThinkPHP和Uniapp进行开发的多平台&#xff08;微信小程序、H5网页&#xff09;溯源、防伪、管理一体化独立系统&#xff0c;拥有强大的防伪码和溯源码双码生成功能&#xff0…

从零开始学JAVA

一、编写Hello world程序 public class JavaMain1 {//主程序执行入口&#xff0c;main方法public static void main(String[] args){System.out.println("Hello world!");} } 运行结果 Hello world! java编写主程序常见错误&#xff1a; 1、System ---首字母没有…

使用Aspose技术将Excel转换为PDF

简介&#xff1a;本文将介绍如何使用Aspose技术将Excel文件转换为PDF格式。我们将使用Aspose-Cells-8.5.2.jar包&#xff0c;并演示Java代码以及进行测试。 一、Aspose技术概述 Aspose是一款强大的文档处理库&#xff0c;支持多种编程语言&#xff0c;如Java、C#、Python等。…

ssh 樹梅派RaspberryPi eMMC版本

安裝rpi 2204的desktop版本&#xff0c;在boot根目錄添加一個ssh的文件&#xff0c;不知道爲什麼還是不能ssh 參考以下博客的方法後可以ssh了 https://idminer.gitbook.io/robotis/part-1-turtlebot3/24.-faq/24.1.-ru-he-zai-raspberry-pi-shu-mei-pai-qi-dong-ssh-server …

工程力学 - 课程导论

工程力学是工程学科中的核心基础课程&#xff0c;专注于研究物体在外力作用下的运动规律和受力情况。它涵盖了静力学、运动学、动力学和材料力学等关键领域&#xff0c;为工程结构的安全性、稳定性和功能性提供理论支撑。通过理论分析、实验测定和计算机模拟等方法&#xff0c;…

node.js安装包

安装地址&#xff1a;Node.js — Run JavaScript Everywhere 链接&#xff1a;https://pan.baidu.com/s/1c7gCEt46HA0yt-EZeu5iFA 提取码&#xff1a;yfbq

【GlobalMapper精品教程】084:tif(tfw)、jpg(jgw)、png(pgw)的区别与联系

文章目录 一、tif(tfw)、jpg(jgw)、png(pgw)简介二、tif(tfw)、jpg(jgw)、png(pgw)生成1. ArcGIS中生成2. Globalmapper中生成一、tif(tfw)、jpg(jgw)、png(pgw)简介 tif(tfw)、jpg(jgw)、png(pgw)文件原理相同,以tfw为例来说明。 TFW(TIFF World File)…

MyEclipse新手使用介绍

目录 1.MyEclipse诞生背景 2.作用 3.版本历史 4.优缺点 5.应用场景 6.如何使用 6.1.下载与安装 6.2.MyEclipse 菜单及其菜单项 7.创建和发布一个 Java 程序 7.1.创建 Java 程序 7.2.发布 Java 程序 8.示例 8.1. Hello World 示例 8.2. 简单Spring Boot 应用 8.3…

SpringBoot实现发送邮件功能

目录 一、开启邮件服务 二、导入pom依赖 三、配置yml文件 四、发送邮件 4.1、发送文字邮件 4.2、发送html邮件 4.3、发送附件邮件 4.4、发送图片邮件 一、开启邮件服务 这里拿QQ邮箱举例。 翻到下面进行开启,之后获取授权码。 二、导入pom依赖 <dependency><…

问题:明朝统辖西域的主要措施有( )(2.0分) (多选题) #学习方法#微信#经验分享

问题&#xff1a;明朝统辖西域的主要措施有( )(2.0分) (多选题) A.设立哈密卫 B.册封各地的首领 C.都护统领 D.布局要地 参考答案如图所示

any和unknown有何区别

在TypeScript&#xff08;TS&#xff09;中&#xff0c;any和unknown都是用来表示类型的一种方式&#xff0c;但它们在类型安全性和使用场景上有着显著的区别。以下是关于any和unknown的详细区别&#xff1a; 类型安全性&#xff1a; any类型&#xff1a;它表示任何类型&#…

为什么说国产大模型的野心,都藏在 MaaS 的生态中

2016 年 4 月&#xff0c;在一场由 IDG 主办的行业峰会上。 IDG 创始董事长熊晓鸽&#xff0c;刚刚盘点完 IDG 深耕中国企业服务多年的成果&#xff0c;台下&#xff0c;GE 中国公司 CEO 便发出了不合时宜的灵魂一问&#xff1a; 「中国有很多伟大的公司&#xff0c;也有很多…

深度评价GPT-4o:探索人工智能的新里程碑

在人工智能领域&#xff0c;OpenAI的GPT系列自推出以来就备受瞩目。GPT-4o作为该系列的最新版本&#xff0c;无疑是迄今为止最为强大的一代。它不仅在技术性能上有了质的飞跃&#xff0c;而且在应用的广泛性和深度上都展现出了惊人的潜力。本文将从版本对比、技术能力、使用体验…

用大模型实现PPT可视化几种思路

https://zhuanlan.zhihu.com/p/700685802 背景 前面一篇文章已经介绍了如何根据用户输入&#xff0c;用大模型实现内容检索、分析、脑图可视化的链路。然而往往投研团队需要针对重要新闻做组内分析解读&#xff0c;需要用ppt的方式来展现&#xff1b;那么优美可能让大模型直…

java 通过接口下载excel并转实体类工具类

java 通过接口下载excel并转实体类工具类 依赖 <dependency><groupId>com.alibaba</groupId><artifactId>easyexcel</artifactId><version>2.2.6</version></dependency>工具类 import com.alibaba.excel.EasyExcel; import …

161-配置nginx正向代理

为了允许所有请求通过Nginx的正向代理&#xff0c;您需要确保Nginx配置正确&#xff0c;并且能够处理各种请求。您的配置中需要一些调整以确保Nginx能够正确解析请求并代理它们。 以下是调整后的Nginx配置示例&#xff1a; Nginx 配置示例 server {listen 8888;server_name …

[office] 如何才能用EXCEL打开dat文件- #微信#学习方法

如何才能用EXCEL打开dat文件? 方法&#xff1a; 1、打开EXCEL软件&#xff1b; 2、文件&#xff0c;打开&#xff0c;选择要转化的DAT文件&#xff1b; 3、在弹出的向导文件&#xff08;步骤1&#xff09;中&#xff0c;选择合适的文件类型&#xff08;按预览选择&#xf…