Skip to content

数据库字段加密 (Database Field Encryption)

数据库字段加密功能基于MyBatis拦截器实现,通过 @EncryptField 注解标记需要加密的字段,在数据入库前自动加密,查询时自动解密,对业务代码完全透明。

模块架构

整体架构

┌─────────────────────────────────────────────────────────────────────┐
│                         数据库字段加密模块                            │
├─────────────────────────────────────────────────────────────────────┤
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │                     注解层 (Annotation)                      │   │
│  │  ┌─────────────┐                                            │   │
│  │  │@EncryptField│ 标记需要加密的字段                          │   │
│  │  └─────────────┘                                            │   │
│  └─────────────────────────────────────────────────────────────┘   │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │                    拦截层 (Interceptor)                      │   │
│  │  ┌───────────────────────┐   ┌───────────────────────────┐  │   │
│  │  │MybatisEncryptInterceptor│  │MybatisDecryptInterceptor│  │   │
│  │  │    入参加密拦截器       │   │    出参解密拦截器        │  │   │
│  │  └───────────────────────┘   └───────────────────────────┘  │   │
│  └─────────────────────────────────────────────────────────────┘   │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │                     核心层 (Core)                            │   │
│  │  ┌───────────────┐  ┌──────────────┐  ┌───────────────┐     │   │
│  │  │EncryptorManager│  │EncryptContext│  │  IEncryptor   │     │   │
│  │  │  加密器管理器   │  │  加密上下文  │  │  加密器接口   │     │   │
│  │  └───────────────┘  └──────────────┘  └───────────────┘     │   │
│  └─────────────────────────────────────────────────────────────┘   │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │                    加密器层 (Encryptor)                      │   │
│  │  ┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐         │   │
│  │  │Base64 │ │  AES  │ │  RSA  │ │  SM2  │ │  SM4  │         │   │
│  │  │Encryptor│Encryptor│Encryptor│Encryptor│Encryptor        │   │
│  │  └───────┘ └───────┘ └───────┘ └───────┘ └───────┘         │   │
│  └─────────────────────────────────────────────────────────────┘   │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │                     工具层 (Utils)                           │   │
│  │  ┌──────────────────────────────────────────────────────┐   │   │
│  │  │                   EncryptUtils                        │   │   │
│  │  │ Base64/AES/RSA/SM2/SM4 加密解密 + 密钥生成工具        │   │   │
│  │  └──────────────────────────────────────────────────────┘   │   │
│  └─────────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────────┘

核心类结构

类名包路径说明
@EncryptFieldannotation字段加密注解
MybatisEncryptInterceptorinterceptor入参加密拦截器
MybatisDecryptInterceptorinterceptor出参解密拦截器
EncryptorManagercore加密器管理器
EncryptContextcore加密上下文
IEncryptorcore加密器接口
AbstractEncryptorcore.encryptor抽象加密器基类
Base64Encryptorcore.encryptorBase64编码器
AesEncryptorcore.encryptorAES加密器
RsaEncryptorcore.encryptorRSA加密器
Sm2Encryptorcore.encryptorSM2加密器
Sm4Encryptorcore.encryptorSM4加密器
EncryptorPropertiesproperties配置属性类
EncryptorAutoConfigurationconfig自动配置类

加解密流程

mermaid
sequenceDiagram
    participant App as 应用层
    participant EI as MybatisEncryptInterceptor
    participant EM as EncryptorManager
    participant Enc as IEncryptor
    participant DB as 数据库
    participant DI as MybatisDecryptInterceptor

    Note over App,DB: 数据写入流程
    App->>EI: 执行INSERT/UPDATE
    EI->>EI: 检查参数对象
    EI->>EM: getFieldCache(class)
    EM-->>EI: 返回加密字段集合
    loop 遍历加密字段
        EI->>EI: 获取字段值
        EI->>EM: encrypt(value, context)
        EM->>Enc: registAndGetEncryptor(context)
        Enc-->>EM: 返回加密器实例
        EM->>Enc: encrypt(value, encodeType)
        Enc-->>EM: 返回加密后的值
        EM-->>EI: 返回[ENC]前缀+密文
        EI->>EI: 设置加密后的字段值
    end
    EI->>DB: 执行SQL (加密数据)

    Note over App,DB: 数据读取流程
    App->>DB: 执行SELECT
    DB-->>DI: 返回结果集
    DI->>DI: 处理参数解密(防止重复加密)
    DI->>EM: getFieldCache(class)
    EM-->>DI: 返回加密字段集合
    loop 遍历加密字段
        DI->>DI: 获取字段值
        DI->>EM: decrypt(value, context)
        EM->>Enc: registAndGetEncryptor(context)
        Enc-->>EM: 返回加密器实例
        EM->>Enc: decrypt(value)
        Enc-->>EM: 返回解密后的值
        EM-->>DI: 返回明文
        DI->>DI: 设置解密后的字段值
    end
    DI-->>App: 返回解密后的结果

配置说明

全局配置

application.yml 中配置默认的加密参数:

yaml
mybatis-encryptor:
  # 功能开关 - 控制是否启用字段加密功能
  enable: true

  # 默认加密算法(当注解中algorithm为DEFAULT时使用)
  # 可选值: BASE64, AES, RSA, SM2, SM4
  algorithm: AES

  # 默认编码方式
  # 可选值: BASE64, HEX
  encode: BASE64

  # 对称加密密钥(AES/SM4使用)
  # AES要求: 16位、24位或32位
  # SM4要求: 16位
  password: "1234567890123456"

  # 非对称加密公钥(RSA/SM2使用)
  # RSA密钥: 通常为Base64编码的1024/2048/4096位密钥
  # SM2密钥: 国密标准格式
  public-key: "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA..."

  # 非对称加密私钥(RSA/SM2使用)
  private-key: "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC..."

配置属性详解

java
@Data
@ConfigurationProperties(prefix = "mybatis-encryptor")
public class EncryptorProperties {

    /**
     * 加密功能总开关
     * true: 启用MyBatis字段加解密
     * false: 禁用加解密功能
     */
    private Boolean enable;

    /**
     * 默认加密算法
     * 当@EncryptField注解中algorithm为DEFAULT时使用此配置
     */
    private AlgorithmType algorithm;

    /**
     * 默认对称加密密钥
     * AES要求:16、24或32位
     * SM4要求:16位
     */
    private String password;

    /**
     * 默认非对称加密公钥
     * RSA/SM2算法使用,用于加密操作
     */
    private String publicKey;

    /**
     * 默认非对称加密私钥
     * RSA/SM2算法使用,用于解密操作
     */
    private String privateKey;

    /**
     * 默认编码方式
     * BASE64: Base64编码(推荐)
     * HEX: 十六进制编码
     */
    private EncodeType encode;
}

MyBatis-Plus集成配置

确保MyBatis-Plus的实体扫描包配置正确:

yaml
mybatis-plus:
  # 实体类扫描包(加密器会扫描这些包下的@EncryptField注解)
  # 支持多个包,用逗号分隔
  type-aliases-package: plus.ruoyi.**.domain.entity

  # Mapper扫描包
  mapper-package: plus.ruoyi.**.mapper

自动配置原理

系统通过 EncryptorAutoConfiguration 自动配置类注册相关Bean:

java
@AutoConfiguration(after = MybatisPlusAutoConfiguration.class)
@EnableConfigurationProperties(EncryptorProperties.class)
@ConditionalOnProperty(value = "mybatis-encryptor.enable", havingValue = "true")
public class EncryptorAutoConfiguration {

    @Autowired
    private EncryptorProperties properties;

    /**
     * 创建加密器管理器
     * 扫描指定包下的实体类,缓存包含@EncryptField注解的字段
     */
    @Bean
    public EncryptorManager encryptorManager(MybatisPlusProperties mybatisPlusProperties) {
        return new EncryptorManager(mybatisPlusProperties.getTypeAliasesPackage());
    }

    /**
     * 创建MyBatis加密拦截器
     * 拦截ParameterHandler.setParameters方法
     */
    @Bean
    public MybatisEncryptInterceptor mybatisEncryptInterceptor(EncryptorManager encryptorManager) {
        return new MybatisEncryptInterceptor(encryptorManager, properties);
    }

    /**
     * 创建MyBatis解密拦截器
     * 拦截ResultSetHandler.handleResultSets方法
     */
    @Bean
    public MybatisDecryptInterceptor mybatisDecryptInterceptor(EncryptorManager encryptorManager) {
        return new MybatisDecryptInterceptor(encryptorManager, properties);
    }
}

自动配置条件

  • 必须在 MybatisPlusAutoConfiguration 之后加载
  • 必须配置 mybatis-encryptor.enable=true

@EncryptField 注解详解

注解定义

java
@Documented
@Inherited
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface EncryptField {

    /**
     * 加密算法类型
     * @return 算法类型,默认使用配置文件中的算法
     */
    AlgorithmType algorithm() default AlgorithmType.DEFAULT;

    /**
     * 对称加密密钥(AES、SM4算法使用)
     * AES要求:16位、24位或32位
     * SM4要求:16位
     * @return 密钥字符串,为空时使用配置文件中的密钥
     */
    String password() default "";

    /**
     * 非对称加密公钥(RSA、SM2算法使用)
     * 用于加密操作
     * @return 公钥字符串,为空时使用配置文件中的公钥
     */
    String publicKey() default "";

    /**
     * 非对称加密私钥(RSA、SM2算法使用)
     * 用于解密操作
     * @return 私钥字符串,为空时使用配置文件中的私钥
     */
    String privateKey() default "";

    /**
     * 加密结果的编码方式
     * 支持BASE64和HEX两种编码
     * 注意:对BASE64算法本身不起作用
     * @return 编码类型,默认使用配置文件中的编码方式
     */
    EncodeType encode() default EncodeType.DEFAULT;
}

基础用法

java
@Data
@TableName("sys_user")
public class SysUser {

    @TableId
    private Long userId;

    private String userName;

    // 使用默认配置加密(采用全局配置的算法和密钥)
    @EncryptField
    private String phone;

    // 指定加密算法
    @EncryptField(algorithm = AlgorithmType.SM4)
    private String email;

    // 使用自定义密钥
    @EncryptField(
        algorithm = AlgorithmType.AES,
        password = "customkey123456789",
        encode = EncodeType.HEX
    )
    private String idCard;
}

注解参数说明

参数类型默认值说明
algorithmAlgorithmTypeDEFAULT加密算法类型
passwordString""对称加密密钥(AES/SM4使用)
publicKeyString""非对称加密公钥(RSA/SM2使用)
privateKeyString""非对称加密私钥(RSA/SM2使用)
encodeEncodeTypeDEFAULT编码方式(BASE64/HEX)

参数优先级:注解参数 > 全局配置

加密算法详解

算法类型枚举

java
@Getter
@AllArgsConstructor
public enum AlgorithmType {

    /**
     * 默认走yml配置
     */
    DEFAULT(null),

    /**
     * Base64编码(不是真正的加密,仅做数据混淆)
     */
    BASE64(Base64Encryptor.class),

    /**
     * AES对称加密(推荐)
     */
    AES(AesEncryptor.class),

    /**
     * RSA非对称加密
     */
    RSA(RsaEncryptor.class),

    /**
     * SM2国密非对称加密
     */
    SM2(Sm2Encryptor.class),

    /**
     * SM4国密对称加密
     */
    SM4(Sm4Encryptor.class);

    private final Class<? extends AbstractEncryptor> clazz;
}

编码类型枚举

java
public enum EncodeType {

    /**
     * 默认使用yml配置
     */
    DEFAULT,

    /**
     * Base64编码(推荐,结果更短)
     */
    BASE64,

    /**
     * 16进制编码(结果较长,但更直观)
     */
    HEX;
}

1. BASE64 编码

Base64不是加密算法,仅对数据进行编码混淆,安全性最低,适合不敏感但需要隐藏的数据。

java
public class User {
    // BASE64不需要密钥,主要用于数据混淆
    @EncryptField(algorithm = AlgorithmType.BASE64)
    private String remark;
}

Base64Encryptor 实现

java
public class Base64Encryptor extends AbstractEncryptor {

    public Base64Encryptor(EncryptContext context) {
        super(context);
    }

    @Override
    public AlgorithmType algorithm() {
        return AlgorithmType.BASE64;
    }

    @Override
    public String encrypt(String value, EncodeType encodeType) {
        // encodeType参数对Base64不起作用
        return EncryptUtils.encryptByBase64(value);
    }

    @Override
    public String decrypt(String value) {
        return EncryptUtils.decryptByBase64(value);
    }
}

适用场景

  • 临时数据混淆
  • 非敏感信息隐藏
  • 日志脱敏

2. AES 对称加密

AES是目前最广泛使用的对称加密算法,安全性高、性能好,是推荐的默认选择。

java
public class User {
    // 使用全局配置的AES密钥
    @EncryptField(algorithm = AlgorithmType.AES)
    private String phone;

    // 使用自定义AES密钥(16位)
    @EncryptField(
        algorithm = AlgorithmType.AES,
        password = "mykey1234567890"
    )
    private String email;

    // AES + HEX编码
    @EncryptField(
        algorithm = AlgorithmType.AES,
        password = "mykey1234567890",
        encode = EncodeType.HEX
    )
    private String idCard;
}

AesEncryptor 实现

java
public class AesEncryptor extends AbstractEncryptor {

    private final EncryptContext context;

    public AesEncryptor(EncryptContext context) {
        super(context);
        this.context = context;
    }

    @Override
    public AlgorithmType algorithm() {
        return AlgorithmType.AES;
    }

    @Override
    public String encrypt(String value, EncodeType encodeType) {
        if (encodeType == EncodeType.HEX) {
            return EncryptUtils.encryptByAesHex(value, context.getPassword());
        } else {
            return EncryptUtils.encryptByAes(value, context.getPassword());
        }
    }

    @Override
    public String decrypt(String value) {
        return EncryptUtils.decryptByAes(value, context.getPassword());
    }
}

AES密钥要求

密钥长度安全级别说明
16位AES-128基础安全级别
24位AES-192中等安全级别
32位AES-256最高安全级别(推荐)

适用场景

  • 手机号、邮箱等常规敏感信息
  • 需要高性能加解密的场景
  • 大量数据批量处理

3. RSA 非对称加密

RSA使用公钥加密、私钥解密,安全性最高,但性能较低,适合少量高敏感数据。

java
public class User {
    @EncryptField(
        algorithm = AlgorithmType.RSA,
        publicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...",
        privateKey = "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC..."
    )
    private String bankCard;
}

RsaEncryptor 实现

java
public class RsaEncryptor extends AbstractEncryptor {

    private final EncryptContext context;

    public RsaEncryptor(EncryptContext context) {
        super(context);
        String privateKey = context.getPrivateKey();
        String publicKey = context.getPublicKey();
        if (StringUtils.isAnyEmpty(privateKey, publicKey)) {
            throw new IllegalArgumentException("RSA公私钥均需要提供,公钥加密,私钥解密。");
        }
        this.context = context;
    }

    @Override
    public AlgorithmType algorithm() {
        return AlgorithmType.RSA;
    }

    @Override
    public String encrypt(String value, EncodeType encodeType) {
        if (encodeType == EncodeType.HEX) {
            return EncryptUtils.encryptByRsaHex(value, context.getPublicKey());
        } else {
            return EncryptUtils.encryptByRsa(value, context.getPublicKey());
        }
    }

    @Override
    public String decrypt(String value) {
        return EncryptUtils.decryptByRsa(value, context.getPrivateKey());
    }
}

RSA密钥生成

java
// 使用工具类生成RSA密钥对
Map<String, String> keyMap = EncryptUtils.generateRsaKey();
String publicKey = keyMap.get(EncryptUtils.PUBLIC_KEY);
String privateKey = keyMap.get(EncryptUtils.PRIVATE_KEY);

密钥长度说明

密钥长度安全级别性能
1024位较低(不推荐)
2048位中等(推荐)适中
4096位较慢

适用场景

  • 银行卡号、密码等高敏感信息
  • 少量数据的加密存储
  • 需要密钥分离管理的场景

4. SM2 国密非对称加密

SM2是中国国家密码管理局发布的国产非对称加密算法,符合国密标准,适合政务和金融系统。

java
public class User {
    @EncryptField(
        algorithm = AlgorithmType.SM2,
        publicKey = "你的SM2公钥",
        privateKey = "你的SM2私钥"
    )
    private String socialSecurityNumber;
}

Sm2Encryptor 实现

java
public class Sm2Encryptor extends AbstractEncryptor {

    private final EncryptContext context;

    public Sm2Encryptor(EncryptContext context) {
        super(context);
        String privateKey = context.getPrivateKey();
        String publicKey = context.getPublicKey();
        if (StringUtils.isAnyEmpty(privateKey, publicKey)) {
            throw new IllegalArgumentException("SM2公私钥均需要提供,公钥加密,私钥解密。");
        }
        this.context = context;
    }

    @Override
    public AlgorithmType algorithm() {
        return AlgorithmType.SM2;
    }

    @Override
    public String encrypt(String value, EncodeType encodeType) {
        if (encodeType == EncodeType.HEX) {
            return EncryptUtils.encryptBySm2Hex(value, context.getPublicKey());
        } else {
            return EncryptUtils.encryptBySm2(value, context.getPublicKey());
        }
    }

    @Override
    public String decrypt(String value) {
        return EncryptUtils.decryptBySm2(value, context.getPrivateKey());
    }
}

SM2密钥生成

java
// 使用工具类生成SM2密钥对
Map<String, String> keyMap = EncryptUtils.generateSm2Key();
String publicKey = keyMap.get(EncryptUtils.PUBLIC_KEY);
String privateKey = keyMap.get(EncryptUtils.PRIVATE_KEY);

适用场景

  • 政务系统
  • 金融系统
  • 需要符合国密标准的项目

5. SM4 国密对称加密

SM4是中国国家密码管理局发布的国产对称加密算法,符合国密标准,性能接近AES。

java
public class User {
    // SM4密钥必须是16位
    @EncryptField(
        algorithm = AlgorithmType.SM4,
        password = "sm4key1234567890"
    )
    private String passport;
}

Sm4Encryptor 实现

java
public class Sm4Encryptor extends AbstractEncryptor {

    private final EncryptContext context;

    public Sm4Encryptor(EncryptContext context) {
        super(context);
        this.context = context;
    }

    @Override
    public AlgorithmType algorithm() {
        return AlgorithmType.SM4;
    }

    @Override
    public String encrypt(String value, EncodeType encodeType) {
        if (encodeType == EncodeType.HEX) {
            return EncryptUtils.encryptBySm4Hex(value, context.getPassword());
        } else {
            return EncryptUtils.encryptBySm4(value, context.getPassword());
        }
    }

    @Override
    public String decrypt(String value) {
        return EncryptUtils.decryptBySm4(value, context.getPassword());
    }
}

SM4密钥要求

  • 密钥长度必须是16位
  • 采用128位分组加密

适用场景

  • 国产化替代AES
  • 政务和金融系统
  • 需要高性能的国密加密场景

算法对比

算法类型安全性性能密钥要求适用场景
BASE64编码最快数据混淆
AES对称16/24/32位常规敏感数据
RSA非对称最高公私钥对高敏感数据
SM2非对称最高较慢公私钥对国密合规
SM4对称16位国密合规

核心实现原理

EncryptorManager 加密器管理器

EncryptorManager 是加密模块的核心,负责字段缓存和加密器实例管理:

java
@Slf4j
@NoArgsConstructor
public class EncryptorManager {

    /**
     * 加密器实例缓存
     * Key: EncryptContext的hashCode,确保相同配置复用实例
     * Value: 对应的加密器实例
     */
    Map<Integer, IEncryptor> encryptorMap = new ConcurrentHashMap<>();

    /**
     * 类的加密字段缓存
     * Key: 实体类的Class对象
     * Value: 该类中标注了@EncryptField注解的字段集合
     */
    Map<Class<?>, Set<Field>> fieldCache = new ConcurrentHashMap<>();

    /**
     * 构造方法,初始化时扫描实体类
     */
    public EncryptorManager(String typeAliasesPackage) {
        scanEncryptClasses(typeAliasesPackage);
    }

    /**
     * 统一加密接口
     * 加密后的值会自动添加[ENC]前缀用于标识
     */
    public String encrypt(String value, EncryptContext encryptContext) {
        // 已加密的数据直接返回(通过前缀判断)
        if (StringUtils.startsWith(value, Constants.ENCRYPT_HEADER)) {
            return value;
        }
        IEncryptor encryptor = this.registAndGetEncryptor(encryptContext);
        String encrypt = encryptor.encrypt(value, encryptContext.getEncode());
        return Constants.ENCRYPT_HEADER + encrypt;
    }

    /**
     * 统一解密接口
     * 只有带[ENC]前缀的数据才会被解密
     */
    public String decrypt(String value, EncryptContext encryptContext) {
        // 没有加密标识的数据直接返回
        if (!StringUtils.startsWith(value, Constants.ENCRYPT_HEADER)) {
            return value;
        }
        IEncryptor encryptor = this.registAndGetEncryptor(encryptContext);
        String str = StringUtils.removeStart(value, Constants.ENCRYPT_HEADER);
        return encryptor.decrypt(str);
    }

    /**
     * 注册并获取加密器实例(使用缓存机制)
     */
    public IEncryptor registAndGetEncryptor(EncryptContext encryptContext) {
        int key = encryptContext.hashCode();
        if (encryptorMap.containsKey(key)) {
            return encryptorMap.get(key);
        }
        // 使用反射创建加密器实例
        IEncryptor encryptor = ReflectUtil.newInstance(
            encryptContext.getAlgorithm().getClazz(),
            encryptContext
        );
        encryptorMap.put(key, encryptor);
        return encryptor;
    }
}

加密标识机制

系统使用 [ENC] 前缀标识已加密的数据:

  • 加密时自动添加前缀
  • 解密时先检查前缀,没有前缀则直接返回原值
  • 防止数据被重复加密

MybatisEncryptInterceptor 加密拦截器

入参加密拦截器拦截 ParameterHandler.setParameters 方法:

java
@Intercepts({@Signature(
    type = ParameterHandler.class,
    method = "setParameters",
    args = {PreparedStatement.class})
})
@AllArgsConstructor
public class MybatisEncryptInterceptor implements Interceptor {

    private final EncryptorManager encryptorManager;
    private final EncryptorProperties defaultProperties;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        return invocation;
    }

    /**
     * 插件包装方法
     * 在ParameterHandler被调用前,先对参数进行加密处理
     */
    @Override
    public Object plugin(Object target) {
        if (target instanceof ParameterHandler parameterHandler) {
            Object parameterObject = parameterHandler.getParameterObject();
            if (ObjectUtil.isNotNull(parameterObject) && !(parameterObject instanceof String)) {
                this.encryptHandler(parameterObject);
            }
        }
        return target;
    }

    /**
     * 递归处理参数对象中的加密字段
     * 支持Map、List、普通对象等多种参数类型
     */
    private void encryptHandler(Object sourceObject) {
        if (ObjectUtil.isNull(sourceObject)) {
            return;
        }

        // 处理Map类型参数
        if (sourceObject instanceof Map<?, ?> map) {
            new HashSet<>(map.values()).forEach(this::encryptHandler);
            return;
        }

        // 处理List类型参数(带性能优化)
        if (sourceObject instanceof List<?> list) {
            if (CollUtil.isEmpty(list)) {
                return;
            }
            Object firstItem = list.get(0);
            if (ObjectUtil.isNull(firstItem) ||
                CollUtil.isEmpty(encryptorManager.getFieldCache(firstItem.getClass()))) {
                return; // 第一个元素没有加密字段,整个List跳过
            }
            list.forEach(this::encryptHandler);
            return;
        }

        // 处理普通对象
        Set<Field> fields = encryptorManager.getFieldCache(sourceObject.getClass());
        if (ObjectUtil.isNull(fields)) {
            return;
        }

        for (Field field : fields) {
            Object fieldValue = field.get(sourceObject);
            String encryptedValue = this.encryptField(Convert.toStr(fieldValue), field);
            field.set(sourceObject, encryptedValue);
        }
    }

    /**
     * 对单个字段进行加密
     * 优先级:字段注解配置 > 全局默认配置
     */
    private String encryptField(String value, Field field) {
        if (ObjectUtil.isNull(value)) {
            return null;
        }

        EncryptField encryptField = field.getAnnotation(EncryptField.class);
        EncryptContext encryptContext = new EncryptContext();

        // 构建加密上下文,优先使用注解配置
        encryptContext.setAlgorithm(
            encryptField.algorithm() == AlgorithmType.DEFAULT ?
                defaultProperties.getAlgorithm() : encryptField.algorithm()
        );
        encryptContext.setEncode(
            encryptField.encode() == EncodeType.DEFAULT ?
                defaultProperties.getEncode() : encryptField.encode()
        );
        encryptContext.setPassword(
            StringUtils.isBlank(encryptField.password()) ?
                defaultProperties.getPassword() : encryptField.password()
        );
        encryptContext.setPrivateKey(
            StringUtils.isBlank(encryptField.privateKey()) ?
                defaultProperties.getPrivateKey() : encryptField.privateKey()
        );
        encryptContext.setPublicKey(
            StringUtils.isBlank(encryptField.publicKey()) ?
                defaultProperties.getPublicKey() : encryptField.publicKey()
        );

        return this.encryptorManager.encrypt(value, encryptContext);
    }
}

MybatisDecryptInterceptor 解密拦截器

出参解密拦截器拦截 ResultSetHandler.handleResultSets 方法:

java
@Intercepts({@Signature(
    type = ResultSetHandler.class,
    method = "handleResultSets",
    args = {Statement.class})
})
@AllArgsConstructor
public class MybatisDecryptInterceptor implements Interceptor {

    private final EncryptorManager encryptorManager;
    private final EncryptorProperties defaultProperties;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 步骤1:处理查询参数解密,防止重复加密
        handleParameterDecryption(invocation);

        // 步骤2:执行原始的结果集处理
        Object result = invocation.proceed();
        if (result == null) {
            return null;
        }

        // 步骤3:对查询结果进行解密处理
        decryptHandler(result);
        return result;
    }

    /**
     * 处理查询参数解密
     * 主要解决以下问题:
     * 1. 防止业务层已加密的参数被再次加密
     * 2. 确保查询条件与数据库存储的加密格式一致
     */
    private void handleParameterDecryption(Invocation invocation) {
        try {
            ResultSetHandler resultSetHandler = (ResultSetHandler) invocation.getTarget();

            // 通过反射获取ParameterHandler
            Field parameterHandlerField = resultSetHandler.getClass()
                .getDeclaredField("parameterHandler");
            parameterHandlerField.setAccessible(true);
            Object target = parameterHandlerField.get(resultSetHandler);

            if (target instanceof ParameterHandler parameterHandler) {
                Object parameterObject = parameterHandler.getParameterObject();
                if (ObjectUtil.isNotNull(parameterObject) &&
                    !(parameterObject instanceof String)) {
                    decryptHandler(parameterObject);
                }
            }
        } catch (Exception e) {
            log.error("处理查询参数解密时出错", e);
        }
    }

    /**
     * 递归处理对象中的解密字段
     */
    private void decryptHandler(Object sourceObject) {
        if (ObjectUtil.isNull(sourceObject)) {
            return;
        }

        // 处理Map类型数据
        if (sourceObject instanceof Map<?, ?> map) {
            new HashSet<>(map.values()).forEach(this::decryptHandler);
            return;
        }

        // 处理List类型数据
        if (sourceObject instanceof List<?> list) {
            if (CollUtil.isEmpty(list)) {
                return;
            }
            Object firstItem = list.get(0);
            if (ObjectUtil.isNull(firstItem) ||
                CollUtil.isEmpty(encryptorManager.getFieldCache(firstItem.getClass()))) {
                return;
            }
            list.forEach(this::decryptHandler);
            return;
        }

        // 处理普通对象
        processObjectFields(sourceObject);
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }
}

支持的数据类型

1. 普通对象

java
// 直接查询实体
SysUser user = userMapper.selectById(1L);
// phone字段自动解密
System.out.println(user.getPhone()); // 13800138000

2. List 集合

java
// 批量查询
List<SysUser> users = userMapper.selectList(null);
// 每个user对象的phone字段都会自动解密
for (SysUser user : users) {
    System.out.println(user.getPhone());
}

3. 分页查询

java
// MyBatis-Plus分页
Page<SysUser> page = new Page<>(1, 10);
Page<SysUser> result = userMapper.selectPage(page, null);
// 分页结果中的记录会自动解密
List<SysUser> records = result.getRecords();

4. Map 结果

java
// 自定义SQL返回Map
List<Map<String, Object>> maps = userMapper.selectMaps(null);
// Map中的加密字段值会自动解密
for (Map<String, Object> map : maps) {
    System.out.println(map.get("phone"));
}

5. 嵌套对象

java
@Data
public class Order {
    private Long orderId;

    // 嵌套对象中的加密字段也会被处理
    private SysUser user;
}

性能优化

1. 字段缓存机制

java
// 字段缓存:避免重复反射
Map<Class<?>, Set<Field>> fieldCache;

// 系统启动时扫描并缓存所有加密字段
private void scanEncryptClasses(String typeAliasesPackage) {
    // 扫描指定包下的所有类
    for (Resource resource : resources) {
        Class<?> clazz = loadClass(resource);
        Set<Field> encryptFieldSet = getEncryptFieldSetFromClazz(clazz);
        if (CollUtil.isNotEmpty(encryptFieldSet)) {
            fieldCache.put(clazz, encryptFieldSet);
        }
    }
}

2. 加密器缓存机制

java
// 加密器缓存:避免重复创建实例
Map<Integer, IEncryptor> encryptorMap;

// 使用EncryptContext的hashCode作为缓存键
public IEncryptor registAndGetEncryptor(EncryptContext encryptContext) {
    int key = encryptContext.hashCode();
    if (encryptorMap.containsKey(key)) {
        return encryptorMap.get(key);  // 命中缓存直接返回
    }
    // 创建新实例并缓存
    IEncryptor encryptor = ReflectUtil.newInstance(
        encryptContext.getAlgorithm().getClazz(), encryptContext
    );
    encryptorMap.put(key, encryptor);
    return encryptor;
}

3. 智能跳过优化

java
// 如果类中没有加密字段,直接跳过处理
Set<Field> fields = encryptorManager.getFieldCache(sourceObject.getClass());
if (ObjectUtil.isNull(fields)) {
    return; // 直接返回,不做任何处理
}

4. 批量处理优化

java
// 对于List类型,检查第一个元素是否有加密字段
if (sourceObject instanceof List<?> list) {
    if (CollUtil.isEmpty(list)) {
        return;
    }
    Object firstItem = list.get(0);
    if (ObjectUtil.isNull(firstItem) ||
        CollUtil.isEmpty(encryptorManager.getFieldCache(firstItem.getClass()))) {
        return; // 如果第一个元素没有加密字段,整个List都跳过
    }
    list.forEach(this::decryptHandler);
}

5. 性能测试数据

场景无加密启用加密性能损耗
单条插入5ms6ms~20%
批量插入(1000条)200ms280ms~40%
单条查询3ms4ms~33%
批量查询(1000条)100ms140ms~40%

最佳实践

1. 字段选择原则

适合加密的字段

  • 用户隐私信息(手机号、邮箱、身份证)
  • 敏感业务数据(银行卡号、密码)
  • 法规要求保护的数据(如GDPR、等保要求)

不适合加密的字段

  • 需要进行范围查询的字段(如金额、日期)
  • 需要排序的字段
  • 外键关联字段
  • 频繁查询的索引字段
java
// ❌ 不推荐:对所有字符串字段都加密
@EncryptField private String name;        // 姓名通常不需要加密
@EncryptField private String address;     // 地址可能需要模糊查询
@EncryptField private String department;  // 部门名称不需要加密

// ✅ 推荐:只对真正敏感的字段加密
@EncryptField private String phone;       // 手机号需要保护
@EncryptField private String idCard;      // 身份证号需要保护
@EncryptField private String bankCard;    // 银行卡号需要保护

2. 算法选择策略

java
// 根据敏感级别选择算法
public class User {
    // 低敏感:BASE64混淆(仅隐藏,非真正加密)
    @EncryptField(algorithm = AlgorithmType.BASE64)
    private String nickname;

    // 中敏感:AES加密(推荐,性能好)
    @EncryptField(algorithm = AlgorithmType.AES)
    private String phone;

    // 高敏感:RSA加密(安全性最高)
    @EncryptField(algorithm = AlgorithmType.RSA)
    private String idCard;

    // 国产化项目:使用国密算法
    @EncryptField(algorithm = AlgorithmType.SM2)
    private String bankCard;
}

3. 密钥管理建议

java
// ❌ 不推荐:密钥硬编码在代码中
@EncryptField(
    algorithm = AlgorithmType.AES,
    password = "hardcodedkey12345"  // 不安全!
)
private String phone;

// ✅ 推荐:使用配置文件管理密钥
@EncryptField(algorithm = AlgorithmType.AES)  // 使用全局配置的密钥
private String phone;

// ✅ 推荐:使用环境变量
# application.yml
mybatis-encryptor:
  password: ${ENCRYPT_PASSWORD:defaultPassword}

4. 查询注意事项

java
// ❌ 加密字段不支持直接条件查询
LambdaQueryWrapper<SysUser> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SysUser::getPhone, "13800138000"); // 查询不到结果!

// ✅ 如果需要按加密字段查询,需要手动加密查询条件
@Autowired
private EncryptorManager encryptorManager;

public SysUser findByPhone(String phone) {
    // 构建加密上下文
    EncryptContext context = new EncryptContext();
    context.setAlgorithm(AlgorithmType.AES);
    context.setPassword("your-password");
    context.setEncode(EncodeType.BASE64);

    // 加密查询条件
    String encryptedPhone = encryptorManager.encrypt(phone, context);

    LambdaQueryWrapper<SysUser> wrapper = new LambdaQueryWrapper<>();
    wrapper.eq(SysUser::getPhone, encryptedPhone);
    return userMapper.selectOne(wrapper);
}

5. 数据库设计建议

sql
-- ❌ 不推荐:加密字段设置索引
CREATE INDEX idx_phone ON sys_user(phone);  -- 索引无效

-- ✅ 推荐:对于需要查询的加密字段,考虑存储加密后的哈希值
CREATE TABLE sys_user (
    user_id BIGINT PRIMARY KEY,
    phone VARCHAR(255),       -- 加密存储的手机号
    phone_hash VARCHAR(64),   -- 手机号的哈希值(用于查询)
    email VARCHAR(255),
    id_card VARCHAR(255)
);
CREATE INDEX idx_phone_hash ON sys_user(phone_hash);

-- 查询时使用哈希值
SELECT * FROM sys_user WHERE phone_hash = SHA2('13800138000', 256);

6. 多租户场景

java
// 不同租户使用不同的加密密钥
@Data
public class TenantUser {
    private Long tenantId;

    // 使用租户特定的密钥(通过配置或数据库获取)
    @EncryptField(
        algorithm = AlgorithmType.AES,
        password = "${tenant.encrypt.password}"  // 动态配置
    )
    private String phone;
}

故障排查

常见问题

1. 字段值为null

问题现象:加密后的数据为null

可能原因

  • 字段本身被赋值为null
  • 字段类型不是String

解决方案

java
// 检查字段赋值
if (user.getPhone() != null) {
    userMapper.insert(user);
}

// 确保字段类型为String
@EncryptField
private String phone;  // ✅ 正确

@EncryptField
private Long phoneNumber;  // ❌ 不支持非String类型

2. 加密配置不生效

问题现象:数据未被加密存储

排查步骤

  1. 检查 mybatis-encryptor.enable 是否为 true
  2. 检查实体类是否在扫描包路径内
  3. 检查字段是否为 String 类型
  4. 检查注解是否正确添加
yaml
# 确保配置正确
mybatis-encryptor:
  enable: true  # 必须为true

mybatis-plus:
  type-aliases-package: plus.ruoyi.**.domain.entity  # 确保包路径正确

3. 密钥长度错误

问题现象:启动报错或加解密失败

解决方案

java
// AES:密钥必须是16/24/32位
mybatis-encryptor:
  password: "1234567890123456"  # 16位 ✅
  password: "12345678901234567890"  # 20位 ❌

// SM4:密钥必须是16位
mybatis-encryptor:
  password: "1234567890123456"  # 16位 ✅

4. 查询结果乱码

问题现象:解密后的数据显示乱码

可能原因

  • 数据库字符集设置不正确
  • encode配置与加密时不一致

解决方案

yaml
# 检查数据库字符集
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/db?characterEncoding=utf8mb4

# 确保encode配置一致
mybatis-encryptor:
  encode: BASE64  # 加密和解密必须使用相同的编码

5. 重复加密问题

问题现象:数据被多次加密

解决方案: 系统已内置防重复加密机制(通过 [ENC] 前缀判断):

java
public String encrypt(String value, EncryptContext encryptContext) {
    // 已加密的数据直接返回
    if (StringUtils.startsWith(value, Constants.ENCRYPT_HEADER)) {
        return value;
    }
    // ... 执行加密
}

调试建议

yaml
# 开启MyBatis SQL日志
logging:
  level:
    plus.ruoyi.common.encrypt: DEBUG
    org.apache.ibatis: DEBUG

通过日志可以查看:

  • 加密字段扫描结果
  • 加密解密的执行过程
  • 异常堆栈信息

数据迁移

对于已有的明文数据,需要进行加密迁移:

java
@Service
public class DataMigrationService {

    @Autowired
    private EncryptorManager encryptorManager;

    public void migrateUserData() {
        // 1. 查询所有未加密的数据
        List<SysUser> users = userMapper.selectList(
            new LambdaQueryWrapper<SysUser>()
                .notLike(SysUser::getPhone, "[ENC]")
        );

        // 2. 构建加密上下文
        EncryptContext context = new EncryptContext();
        context.setAlgorithm(AlgorithmType.AES);
        context.setPassword("your-password");
        context.setEncode(EncodeType.BASE64);

        // 3. 逐条加密并更新
        for (SysUser user : users) {
            String encryptedPhone = encryptorManager.encrypt(user.getPhone(), context);
            user.setPhone(encryptedPhone);
            userMapper.updateById(user);
        }
    }
}