Mybatis 敏感数据加解密插件完整实现方案

本文将详细介绍基于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. 实现原理

    1. 拦截mybatis的StatementHandler 对读写请求进行脱敏和字段的加密。
    1. 拦截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;
}
相关推荐
薛晓刚2 小时前
2025 年度个人回顾总结
数据库
TDengine (老段)2 小时前
TDengine 在智能制造领域的应用实践
java·大数据·数据库·制造·时序数据库·tdengine·涛思数据
Coder_Boy_2 小时前
基于 MQTT 的单片机与 Java 业务端双向通信全流程
java·单片机·嵌入式硬件
Asurplus2 小时前
Centos7安装Maven环境
java·centos·maven·apache·yum
想学后端的前端工程师2 小时前
【Spring Boot微服务开发实战:从入门到企业级应用】
java·开发语言·python
男孩李2 小时前
浅谈PostgreSQL 模式(SCHEMA)
数据库·postgresql
TG:@yunlaoda360 云老大2 小时前
如何在华为云国际站代理商控制台进行基础状态核查的常见问题解答
数据库·华为云·php
徐老总2 小时前
手机号脱敏处理(Python/Scala 双版本实现)
java
夏末4722 小时前
面试必问!多线程操作集合避坑指南:用synchronized搞定线程安全
java