本文将详细介绍基于MyBatis的敏感数据全链路安全处理方案,涵盖插件开发原理、具体实现代码以及数据库函数集成。
1. MyBatis插件开发原理
1.1 插件的作用与拦截机制
MyBatis 插件是基于拦截器模式实现的,允许开发者在 MyBatis 执行 SQL语句的关键节点插入自定义逻辑。MyBatis 支持对四大核心组件进行拦截:
- Executor:执行器,用于拦截增删改查操作
- ParameterHandler:参数处理器,用于拦截参数设置
- ResultSetHandler:结果集处理器,用于拦截结果集处理
- StatementHandler:语句处理器,用于拦截SQL语句构建
插件通过实现 Interceptor 接口并配合@Intercepts和@Signature注解来定义拦截规则。
1.2 插件的运行机制与责任链模式
MyBatis的插件体系采用责任链模式,通过InterceptorChain管理所有拦截器。当执行目标方法时,会依次调用所有注册的拦截器。具体实现机制如下:
bash
// 插件动态代理示例
public Object plugin(Object target) {
if (target instanceof Executor) {
return Plugin.wrap(target, this);
}
return target;
}
动态代理机制确保在目标方法执行前后可以插入自定义逻辑,这是实现数据加解密的理论基础。
2. MyBatis + 注解实现加解密插件
2.1 整体架构设计
本方案采用分层处理策略,将加解密与脱敏分离:
数据访问层:通过MyBatis插件实现自动加解密
API展示层:通过Jackson序列化器实现数据脱敏
2.2 注解定义
首先定义用于标记敏感数据的注解:
bash
// 类级注解,标记需要加解密的实体
@Inherited
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface SensitiveData {
}
// 加解密字段注解
@Inherited
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface SensitiveField {
}
2.2 加密解密方法或函数工具类
以 pgsql 中的加解密函数作为举例,实现加密解密方法调用:
bash
@Component
public class PgCryptoUtil {
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* 加密方法 - 返回字符串格式(如十六进制)
*/
public String encrypt(String plainText, String key) throws Exception {
String sql = "SELECT encode(pgp_sym_encrypt(?, ?), 'hex')";
return jdbcTemplate.queryForObject(sql, String.class, plainText, key);
}
/**
* 解密方法 - 接收字符串格式的加密数据
*/
public String decrypt(String encryptedText, String key) throws Exception {
// 假设encryptedText是十六进制格式的字符串
String sql = "SELECT pgp_sym_decrypt(decode(?, 'hex'), ?)";
return jdbcTemplate.queryForObject(sql, String.class, encryptedText, key);
}
}
2.3 基于拦截器的加解密实现
2.3.1 参数加密拦截器
拦截ParameterHandler.setParameters方法,在数据入库前进行加密。
bash
// 加密拦截器 - 用于处理INSERT/UPDATE操作
@Component
@Intercepts({
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
})
public class EncryptInterceptor implements Interceptor {
@Autowired
private PgCryptoUtil pgCryptoUtil;
private final String ENCRYPTION_KEY = "1234567890abcdef";
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object[] args = invocation.getArgs();
Object parameter = args[1];
// 加密处理
if (parameter != null) {
encryptParameters(parameter);
}
return invocation.proceed();
}
private void encryptParameters(Object parameter) {
if (parameter instanceof List) {
for (Object item : (List) parameter) {
encryptItem(item);
}
} else {
encryptItem(parameter);
}
}
private void encryptItem(Object item) {
if (item != null && item.getClass().isAnnotationPresent(SensitiveData.class)) {
MetaObject metaObject = SystemMetaObject.forObject(item);
Field[] fields = item.getClass().getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(SensitiveField.class) && field.getType() == String.class) {
String fieldName = field.getName();
Object value = metaObject.getValue(fieldName);
if (value instanceof String && !((String) value).isEmpty()) {
try {
// 加密字符串数据
String encryptedValue = pgCryptoUtil.encrypt((String) value, ENCRYPTION_KEY);
metaObject.setValue(fieldName, encryptedValue);
} catch (Exception e) {
throw new RuntimeException("加密字段 " + fieldName + " 时发生错误", e);
}
}
}
}
}
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
}
2.3.2 结果集解密拦截器
拦截ResultSetHandler.handleResultSets方法,在数据查询后进行解密
bash
// 解密拦截器 - 用于处理SELECT操作(您的代码优化版)
@Component
@Intercepts({
@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})
})
public class DecryptInterceptor implements Interceptor {
@Autowired
private PgCryptoUtil pgCryptoUtil;
private final String ENCRYPTION_KEY = "1234567890abcdef";
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object result = invocation.proceed();
if (result == null) {
return null;
}
if (result instanceof List) {
for (Object item : (List) result) {
decryptItem(item);
}
} else {
decryptItem(result);
}
return result;
}
private void decryptItem(Object item) {
if (item != null && item.getClass().isAnnotationPresent(SensitiveData.class)) {
MetaObject metaObject = SystemMetaObject.forObject(item);
Field[] fields = item.getClass().getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(SensitiveField.class) && field.getType() == String.class) {
String fieldName = field.getName();
Object value = metaObject.getValue(fieldName);
// 关键修改:处理String类型的加密数据
if (value instanceof String && !((String) value).isEmpty()) {
try {
String decryptedValue = pgCryptoUtil.decrypt((String) value, ENCRYPTION_KEY);
metaObject.setValue(fieldName, decryptedValue);
} catch (Exception e) {
// 记录解密失败但不要抛出异常,以免影响正常查询
System.err.println("解密字段 " + fieldName + " 失败: " + e.getMessage());
// 可以选择保留原始加密值或设置为null
// metaObject.setValue(fieldName, null);
}
}
}
}
}
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
}
2.4 实体类使用
bash
@SensitiveData
@Data // lombok注解
public class UserInfo extends BaseEntity implements Serializable {
/**
* 主键
*/
private String id;
/**
*手机号
*/
@SensitiveField
@Excel(name = "手机号")
private String phone;
}
3. 数据库层面加解密函数
3.1 MySQL加解密函数
MySQL原生支持AES加解密函数,可在SQL层面实现加解密:
bash
-- 加密数据插入
INSERT INTO users (username, phone_encrypted)
VALUES ('john_doe', AES_ENCRYPT('13812345678', 'encryption_key'));
-- 查询解密数据
SELECT username, AES_DECRYPT(phone_encrypted, 'encryption_key') as phone
FROM users WHERE username = 'john_doe';
3.2 PostgreSQL加解密函数
PostgreSQL通过pgcrypto扩展提供加密功能:
bash
-- 启用pgcrypto扩展
CREATE EXTENSION IF NOT EXISTS pgcrypto;
-- 加密数据插入
INSERT INTO users (username, phone_encrypted)
VALUES ('john_doe', pgp_sym_encrypt('13812345678', 'encryption_key'));
-- 查询解密数据
SELECT username, pgp_sym_decrypt(phone_encrypted, 'encryption_key') as phone
FROM users WHERE username = 'john_doe';
-- 使用自定义函数简化操作
CREATE FUNCTION encrypt_value(value TEXT, key TEXT)
RETURNS TEXT AS $$
BEGIN
RETURN encode(pgp_sym_encrypt(value, key), 'base64');
END;
$$ LANGUAGE plpgsql;
单纯在 PostgreSQL 中使用更安全的加密方式,推荐使用其默认的 PGP 函数。注意:此方法与 MySQL 默认加密不兼容。
bash
-- 加密 (使用默认更安全的CBC模式等选项)
SELECT armor(pgp_sym_encrypt('hello', 'mykey')) AS encrypted_data_pgp;
-- 解密
SELECT pgp_sym_decrypt(dearmor('<上述armor函数输出的结果>'), 'mykey') AS decrypted_data_pgp;
3.3 支持 mysql和 pgsql 的通用函数定义
由于两种数据库默认的加密模式不同,为了实现兼容,核心思路是在 PostgreSQL 端使用 encrypt和 decrypt函数,并明确指定加密参数,以模拟 MySQL 的行为。
以下示例均使用明文 hello和密钥 mykey。
1. MySQL 加解密示例
MySQL 使用默认的 AES-128-ECB 模式。
bash
-- 加密 (结果为二进制, 通常用 HEX 转换为十六进制字符串存储或查看)
SELECT HEX(AES_ENCRYPT('hello', 'mykey')) AS encrypted_data;
-- 结果示例: '744661B08EB4698B64DE167B7B43BCD7'
-- 解密 (需先将十六进制字符串转换回二进制)
SELECT AES_DECRYPT(UNHEX('744661B08EB4698B64DE167B7B43BCD7'), 'mykey') AS decrypted_data;
-- 结果: 'hello'
2. PostgreSQL 兼容 MySQL 的加解密示例
在 PostgreSQL 中,使用 pgcrypto扩展的 encrypt/decrypt函数,并指定算法和模式为 aes-ecb以达到兼容。
bash
-- 首先确保启用 pgcrypto 扩展
CREATE EXTENSION IF NOT EXISTS pgcrypto;
-- 加密 (使用 'aes-ecb' 模式模拟MySQL,结果转换为十六进制以便比较和存储)
SELECT encode(encrypt('hello', 'mykey', 'aes-ecb'), 'hex') AS encrypted_data;
-- 结果应与MySQL一致: '744661B08EB4698B64DE167B7B43BCD7'
-- 解密
SELECT convert_from(decrypt(decode('744661B08EB4698B64DE167B7B43BCD7', 'hex'), 'mykey', 'aes-ecb'), 'utf8') AS decrypted_data;
-- 结果: 'hello'
四、相关开源项目 mybatis-encrypt-plugin 介绍
mybatis-encrypt-plugin 是一款开源的基于mybatis的插件机制编写的一套敏感数据加解密以及数据脱敏工具。
开源地址:https://github.com/andydazhong/mybatis-encrypt-plugin
1. 实现原理
-
- 拦截mybatis的StatementHandler 对读写请求进行脱敏和字段的加密。
-
- 拦截mybatis的ResultSetHandler,对读请求的响应进行加密字段的解密赋值。
2. 使用方式
1. 导入依赖
bash
<!-- mybatis数据脱敏插件 -->
<dependency>
<groupId>com.github.chenhaiyangs</groupId>
<artifactId>mybatis-encrypt-plugin</artifactId>
<version>1.0.0</version>
</dependency>
2. 编写加解密实现类以及配置mybatis的插件,
下面在springboot场景下的一个配置案例。
bash
/**
* 插件配置
*/
@Configuration
public class EncryptPluginConfig {
//加密方式
@Bean
Encrypt encryptor() throws Exception{
return new AesSupport("1870577f29b17d6787782f35998c4a79");
}
//配置插件
@Bean
ConfigurationCustomizer configurationCustomizer() throws Exception{
DecryptReadInterceptor decryptReadInterceptor = new DecryptReadInterceptor(encryptor());
SensitiveAndEncryptWriteInterceptor sensitiveAndEncryptWriteInterceptor = new SensitiveAndEncryptWriteInterceptor(encryptor());
return (configuration) -> {
configuration.addInterceptor(decryptReadInterceptor);
configuration.addInterceptor(sensitiveAndEncryptWriteInterceptor);
};
}
}
3. 在vo类上添加功能注解使得插件生效:
bash
@SensitiveEncryptEnabled
@Data
public class UserDTO {
private Integer id;
/**
* 用户名
*/
@EncryptField
private String userName;
/**
* 脱敏的用户名
*/
@SensitiveField(SensitiveType.CHINESE_NAME)
private String userNameSensitive;
/**
* 值的赋值不从数据库取,而是从userName字段获得。
*/
@SensitiveBinded(bindField = "userName",value = SensitiveType.CHINESE_NAME)
private String userNameOnlyDTO;
/**
* 身份证号
*/
@EncryptField
private String idcard;
/**
* 脱敏的身份证号
*/
@SensitiveField(SensitiveType.ID_CARD)
private String idcardSensitive;
/**
* 一个json串,需要脱敏
* SensitiveJSONField标记json中需要脱敏的字段
*/
@SensitiveJSONField(sensitivelist = {
@SensitiveJSONFieldKey(key = "idcard",type = SensitiveType.ID_CARD),
@SensitiveJSONFieldKey(key = "username",type = SensitiveType.CHINESE_NAME),
})
private String jsonStr;
private int age;
@SensitiveField(SensitiveType.EMAIL)
private String email;
}