背景:在erp开发中,有些用户比较敏感数据库里的数据比较敏感,系统给用户部署后,公司也不想让任何人看到数据,所以就有了数据库字段加密方案。
技术 spring boot + mybatisplus 3.3.1
mybatisplus 实际提供了 字段加密方案
第一 他要钱
第二 他是在实体类上加注解 满足不了我们的需求
我们的整体需求是
用户可以自定义配置字段
在系统设置里 有个 字段加密 菜单
- 点击某个表单 然后显示这个表单所需要的字段
2.勾选 某个字段 这个字段 就会激活 再提交 更新表单的时候 这个字段就会加密处理
比如 系统管理 字段加密 他点击了请假表单 然后勾选 请假事由 字段加密
然后 公司的员工再次提交表单的时候 请假事由 就会被加密 。
这个需求 用户再页面上操作 我们是不需要改代码的 。
如果利用实体类加注解方案 肯定满足不了 因为 每个用户加密的字段不一样,
鬼知道 加密注解要加在哪个实体类上
我们的系统 表单 和表单的字段 都定义在数据库里 所以 可以自由选择 表单和字段
这个根据各自系统自行修改
下面直接分享 加密的代码和思路 。原理我就不再说 ,用到的知识 自行百度即可
第一步 : 首先把用户勾选需要加密的 字段 缓存到redis 减少数据库查询
// 这段代码 就不分享了 自由编写
根据表名 获取 需要加密的字段
List<String> stringList= BaseDataUtil.getFieldPassword(insertSql.getTableName());
第二步:
引入加密依赖
<!--对数据库字段进行加密、脱敏-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.3</version>
</dependency>
第三步:
配置文件 配置
第四步 编写加密方案 利用的是 框加下的这个类 BaseTypeHandler
对于这个类的介绍 自行百度
自定义一个类 然后继承 这个类 BaseTypeHandler
重写 父类方法
package com.erp.init.handlers;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.symmetric.AES;
import com.erp.init.utils.BaseDataUtil;
import org.apache.ibatis.logging.jdbc.PreparedStatementLogger;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import java.lang.reflect.Proxy;
import java.nio.charset.StandardCharsets;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* User: Json
* <p>
* Date: 2023/11/14
**/
public class EncryptHandler extends BaseTypeHandler<String> {
/**
* 定义好后不要修改
*/
private static final byte[] KEYS = "shc9876543232camp".getBytes(StandardCharsets.UTF_8);
//前缀 为了 老数据 和新数据的 处理 比如 老数据没加密
//新数据加密了 可以根据这个前缀判断 老数据 就不要解密了
private static final String dataWithPrefix = "sada";
/**
* 设置参数
*/
@Override
public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
if (StringUtils.isEmpty(parameter)) {
ps.setString(i, null);
return;
}
// 获取动态代理类的 InvocationHandler
PreparedStatementLogger handler =(PreparedStatementLogger) Proxy.getInvocationHandler(ps);
PreparedStatement preparedStatement= handler.getPreparedStatement();
MetaObject stmtMetaObj = SystemMetaObject.forObject(preparedStatement);
String sql = stmtMetaObj.getValue("sql").toString();
//System.out.println("sql:"+sql);
SqlResult updateSql= updateSql(sql);
if(!ObjectUtils.isEmpty(updateSql)){
// System.out.println("更新数据:"+updateSql);
// System.out.println(i+"===>"+updateSql.getColumnNames().get(i-1));
List<String> stringList= BaseDataUtil.getFieldPassword(updateSql.getTableName());
if (!CollectionUtils.isEmpty(stringList) &&
!CollectionUtils.isEmpty(updateSql.getColumnNames()) &&
stringList.contains(updateSql.getColumnNames().get(i-1))) {
AES aes = SecureUtil.aes(KEYS);
String encrypt = aes.encryptHex(parameter);
ps.setString(i,dataWithPrefix+ encrypt);
} else {
ps.setString(i, parameter);
}
}
SqlResult insertSql=insetSQl(sql);
if(!ObjectUtils.isEmpty(insertSql)){
// System.out.println("新增数据:"+insertSql);
// System.out.println(i+"===>"+insertSql.getColumnNames().get(i-1));
List<String> stringList= BaseDataUtil.getFieldPassword(insertSql.getTableName());
if (!CollectionUtils.isEmpty(stringList) &&
!CollectionUtils.isEmpty(insertSql.getColumnNames()) &&
stringList.contains(insertSql.getColumnNames().get(i-1))) {
AES aes = SecureUtil.aes(KEYS);
String encrypt = aes.encryptHex(parameter);
ps.setString(i, dataWithPrefix+ encrypt);
} else {
ps.setString(i, parameter);
}
}
if(ObjectUtils.isEmpty(insertSql) && ObjectUtils.isEmpty(updateSql) ){
ps.setString(i, parameter);
}
}
/**
* 获取值
*/
@Override
public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
return decrypt(rs.getString(columnName),columnName,rs.getMetaData().getTableName(1));
}
/**
* 获取值
*/
@Override
public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return null;
// return decrypt(rs.getString(columnIndex));
}
/**
* 获取值
*/
@Override
public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
return null;
//return decrypt(cs.getString(columnIndex));
}
public String decrypt(String value,String columnName,String tableName) {
// return value;
if (null == value) {
return null;
}
List<String> stringList= BaseDataUtil.getFieldPassword(tableName);
if(CollectionUtils.isEmpty(stringList)){
return value;
}
if (value.startsWith(dataWithPrefix) && stringList.contains(columnName)) {
// 是新数据,去掉前缀
String decryptedData = value.substring(dataWithPrefix.length());
return SecureUtil.aes(KEYS).decryptStr(decryptedData);
} else {
return value;
}
}
public SqlResult updateSql(String sql) {
// 假设你已经有了sql字符串
// String sql = "UPDATE your_table SET column1 = value1, column2 = value2 WHERE condition";
// 匹配UPDATE语句
Pattern updatePattern = Pattern.compile("UPDATE\\s+([^\\s]+)\\s+SET\\s+([^\\s]+\\s*=\\s*[^,]+(,\\s*[^\\s]+\\s*=\\s*[^,]+)*)\\s+WHERE\\s+(.+)");
Matcher updateMatcher = updatePattern.matcher(sql);
// 如果是UPDATE语句
if (updateMatcher.matches()) {
String tableName = updateMatcher.group(1); // 获取表名
String setClause = updateMatcher.group(2); // 获取SET子句
// 提取更新的字段名
List<String> columnNames = extractColumnNames(setClause);
// 返回表名和字段名的信息
return new SqlResult(tableName, columnNames);
}
return null;
}
private static List<String> extractColumnNames(String setClause) {
List<String> columnNames = new ArrayList<>();
String[] columns = setClause.split("\\s*,\\s*");
for (String column : columns) {
String[] parts = column.split("\\s*=\\s*");
String columnName = parts[0].trim();
columnNames.add(columnName);
}
return columnNames;
}
private static List<String> extractColumnNamesInsert(String columnsClause) {
String[] columns = columnsClause.split("\\s*,\\s*");
List<String> columnNames = new ArrayList<>();
for (String column : columns) {
columnNames.add(column.trim());
}
return columnNames;
}
public SqlResult insetSQl(String sql) {
// 假设你已经有了sql字符串
// String sql = "INSERT INTO your_table (column1, column2) VALUES (value1, value2)";
// 匹配INSERT语句
Pattern insertPattern = Pattern.compile("INSERT\\s+INTO\\s+([^\\s]+)\\s*\\(([^)]+)\\)\\s*VALUES\\s*\\(([^)]+)\\)");
Matcher insertMatcher = insertPattern.matcher(sql);
// 如果是INSERT语句
if (insertMatcher.matches()) {
String tableName = insertMatcher.group(1); // 获取表名
String columnsClause = insertMatcher.group(2); // 获取列名的部分
// String valuesClause = insertMatcher.group(3); // 获取值的部分
// 提取新增的字段名和对应的值
List<String> columnNames = extractColumnNamesInsert(columnsClause);
// 返回表名、字段名和对应值的信息
return new SqlResult(tableName, columnNames);
}
return null;
}
}
第五步:
把这个类注册到配置文件中
package com.erp.init.mybatisplus;
import com.baomidou.mybatisplus.autoconfigure.ConfigurationCustomizer;
import com.erp.init.handlers.EncryptHandler;
import org.apache.ibatis.type.JdbcType;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* User: Json
* <p>
* Date: 2023/11/15
**/
@Configuration
public class MyBatisConfig {
@Bean
public ConfigurationCustomizer configurationCustomizer() {
return configuration -> {
//String.class, JdbcType.VARCHAR 这个是 只对 字符串进行处理
// int float 的加密 根据字符串再扩展一个类就行了
configuration.getTypeHandlerRegistry().register(String.class, JdbcType.VARCHAR, new EncryptHandler());
// 注册其他类型处理器
};
}
}
这要编写后 每次 mybatisplus 调用 新增 和 更新 批量更新 批量操作 都会触发这里的代码
实现字段加解密