单体项目加密脱敏最佳实践

在使用这套数据加密脱敏框架时完全不用手动写加密解密代码——给实体类字段加注解就行:密码字段贴@Password,框架会自动用BCrypt做单向加密;手机、邮箱这类字段贴@FieldEncrypt,选个AES之类的可逆算法就搞定加密;需要脱敏的字段加@FieldDesensitize,既能指定脱敏规则(比如手机号隐藏中间4位),还能关联权限;数据存库时MyBatis的TypeHandler会自动把注解字段转成密文,取出来又自动解密回明文,全程无感;用加密字段查数据也不用管,查询拦截器会自动把参数加密后再查,保证索引能用,它还能智能解析List、嵌套对象这些复杂参数,性能也靠谱;脱敏只在接口返回前端时处理,管理员能看到原始数据方便调试,普通用户只能看脱敏后的,而且服务层流转的都是明文,发短信这类业务不会受影响,加密字段要做模糊查询的话,加个辅助列、分词加密或者同步到ES都能解决。

思路

以注解驱动为入口,策略模式为核心算法扩展内核,结合MyBatis 生态组件实现加密解密的无感知处理,同时通过权限校验实现脱敏逻辑的精细化管控。

一、注解体系设计

框架定义分层的自定义注解,作为加密 / 脱敏逻辑的触发标识与规则配置载体,注解通过枚举类型实现策略的精准绑定:

  1. 加密注解分类

    • 专用加密注解(@Password 等):无额外属性,内置绑定单向加密策略,专用于密码等特殊字段标注,语义化且配置极简。
    • 通用加密注解(@FieldEncrypt):核心属性为encryptAlgorithm(加密算法枚举,如 AES_DETERMINISTIC、SM4_DETERMINISTIC、BCRYPT 等),用于手机、邮箱等可逆加密场景,通过枚举指定具体加密策略。
  2. 脱敏注解(@FieldDesensitize)

    • 核心属性 1:desensitizeType(脱敏方式枚举,如 PHONE、EMAIL、ID_CARD 等),指定字段的脱敏规则;
    • 核心属性 2:permissionKey(权限标识字符串),关联权限校验逻辑,用于判断是否执行脱敏。
  3. 注解元数据配置:所有注解均设置@Target(ElementType.FIELD)(作用于实体类字段)、@Retention(RetentionPolicy.RUNTIME)(运行时保留,支持反射获取注解信息)。

二、策略模式的算法封装实现

基于策略模式解耦算法定义与实现,通过接口标准化行为,具体实现类承载不同算法逻辑,同时封装工具类提供统一入口:

  1. 加密策略层

    • 抽象策略接口(EncryptionStrategy):定义核心方法,String encrypt(String plaintext)(加密方法,所有算法必须实现)、String decrypt(String ciphertext)(解密方法,仅可逆算法实现,默认抛出不支持异常);

    • 具体策略实现类

      • BCryptEncryptionStrategy:实现encrypt方法,采用 BCrypt 算法进行单向加密,decrypt方法直接抛出 UnsupportedOperationException;
      • AesDeterministicEncryptionStrategy:实现encryptdecrypt方法,采用 AES 算法的确定性加密模式(固定密钥与填充方式,保证相同明文输出相同密文),支持可逆处理;
      • 扩展实现SM4EncryptionStrategyRSAEncryptionStrategy等,只需实现接口即可接入,供真实业务需求选择。
  2. 脱敏策略层

    • 抽象策略接口(DesensitizeStrategy):定义String desensitize(String rawData, String permission)方法,入参为原始数据与用户权限标识,返回处理后的数据;

    • 具体策略实现类

      • PhoneDesensitizeStrategy:对手机号执行138****1234格式的脱敏处理;
      • EmailDesensitizeStrategy:对邮箱执行xxx@163.com转为x**@163.com的脱敏处理;
      • 扩展IdCardDesensitizeStrategyAddressDesensitizeStrategy等。
  3. 策略工厂与工具类封装

    • 策略工厂(StrategyFactory):通过枚举类型作为 key,维护 “枚举 - 策略实现类” 的映射关系,提供getEncryptionStrategy(EncryptAlgorithm algorithm)getDesensitizeStrategy(DesensitizeType type)方法,实现策略的动态获取;
    • 加密脱敏工具类(EncryptDesensitizeUtil):封装策略工厂的调用逻辑,对外提供encrypt(Field field, String plaintext)decrypt(Field field, String ciphertext)desensitize(Field field, String rawData, String permission)方法,内部通过反射获取字段注解信息,自动匹配并调用对应策略。

三、MyBatis 生态整合的无感知处理

利用 MyBatis 的 TypeHandler 和 Interceptor 组件,实现数据库交互过程中加密解密的自动化处理,无需业务代码介入,并针对拦截器参数解析问题优化处理逻辑:

  1. TypeHandler 实现(EncryptTypeHandler)

    • 继承 MyBatis 的BaseTypeHandler<String>,重写核心方法:

      • setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType):当数据入库时,通过反射获取实体类字段的加密注解,调用工具类的encrypt方法将明文转为密文,再设置到 PreparedStatement 中;
      • getNullableResult(ResultSet rs, String columnName)/getNullableResult(ResultSet rs, int columnIndex)/getNullableResult(CallableStatement cs, int columnIndex):当数据出库时,获取数据库中的密文,通过反射获取字段注解,调用工具类的decrypt方法还原为明文后返回。
    • 配置方式:通过 MyBatis 的配置文件或注解@MappedJdbcTypes@MappedTypes将 TypeHandler 与字段类型绑定,或在实体类字段上通过@TypeHandler指定。

  2. 查询拦截器实现(EncryptQueryInterceptor)

    • 实现 MyBatis 的Interceptor接口,拦截点为StatementHandlerprepare方法,针对参数解析缺陷优化处理流程:

      • 步骤 1:通过StatementHandler获取BoundSql,进而获取 SQL 语句和查询参数;

      • 步骤 2:递归解析复杂参数(解决参数类型多样问题):

        • 若参数为简单类型(String/Long):直接检查是否带有加密注解(适用于单个参数场景);
        • 若参数为 Map(@Param 产生):遍历 Map 中的 value,递归解析每个 value 的类型;
        • 若参数为 JavaBean(DTO/POJO):反射遍历字段,识别带有加密注解的字段;
        • 若参数为 List/Collection:拆解集合,递归解析每个元素;
        • 若参数为嵌套对象:逐层反射解析嵌套字段,识别加密注解;
      • 步骤 3:对识别出的加密字段参数,调用工具类的encrypt方法进行加密处理,替换BoundSql中的原始参数为加密后的参数;

      • 步骤 4:放行拦截链,执行 SQL。

    • 性能优化方案:引入缓存机制(如 Guava Cache),缓存Class对象与对应加密字段的映射关系(即某个 Class 哪些字段带有加密注解),避免每次解析参数时都反射遍历所有字段,降低 CPU 消耗。

    • 作用:针对加密字段的查询(如where phone = ?),保证查询参数与数据库中存储的密文一致,使基于加密字段创建的索引正常生效,避免全表扫描。

四、权限脱敏的技术实现

脱敏逻辑与权限系统联动,明确脱敏执行时机,解决 Service 层逻辑错误问题,实现安全与调试效率的平衡:

  1. 权限校验接口(PermissionValidator):定义boolean hasPermission(String permissionKey, String userPermission)方法,提供权限判断的标准化接口,具体实现可对接 Spring Security、Shiro 等权限框架;

  2. 用户权限获取:通过 ThreadLocal 存储当前请求的用户权限信息(如在拦截器中从 Token、Session 中解析并放入 ThreadLocal),在脱敏时直接获取,保证线程安全;

  3. 脱敏执行时机与实现(核心优化)

    • 明确执行阶段:脱敏仅发生在 Controller 返回给前端的序列化阶段(Jackson Serializer),Service/DAO 层始终流转明文(TypeHandler 负责将数据库密文解密为明文),避免 Service 层业务逻辑(如发送短信)因脱敏失效。

    • 具体实现方式

      • 自定义 Jackson 的ContextualSerializer(如DesensitizeSerializer),实现JsonSerializer接口;

      • 在序列化阶段,通过反射获取字段的@FieldDesensitize注解,调用PermissionValidatorhasPermission方法进行权限判断:

        • 若返回true(如管理员、开发人员权限):直接序列化原始明文数据;
        • 若返回false(如普通用户权限):调用对应脱敏策略,序列化脱敏后的数据;
      • 配置方式:通过注解@JsonSerialize(using = DesensitizeSerializer.class)绑定到实体类字段,或全局配置序列化器自动识别@FieldDesensitize注解。

    • 防数据污染处理:不在 Service 层修改对象本身的值,仅在序列化阶段做脱敏处理,避免缓存(如 Redis)中存入脱敏后的无效数据。

五、扩展性与兼容性设计

  1. 算法扩展:新增加密算法或脱敏方式时,只需新增策略实现类,在枚举类中添加对应枚举值,修改策略工厂的映射关系,无需改动原有业务代码,遵循开闭原则;
  2. 密钥管理:对于对称加密算法(如 AES、SM4),框架提供密钥配置类(EncryptKeyConfig),支持从配置文件(application.yml/properties)读取密钥,或对接密钥管理服务(KMS)实现密钥的安全存储与获取;
  3. 多数据源兼容:通过 MyBatis 的插件机制,拦截器和 TypeHandler 可适配不同的数据源(MySQL、Oracle 等),只需保证字段类型与 JDBC 类型的映射一致。

六、模糊查询问题解决方案

针对加密字段模糊查询的 “死穴”,提供三种适配不同场景的解决方案,按需选择:

  1. 分词密文法(高安全场景)

    • 实现逻辑:对需模糊查询的字段(如手机号)进行分段加密(如手机号 13812345678 分为 138、1234、5678 三段,每段分别采用确定性加密);
    • 适用场景:仅需特定分段匹配的场景(如匹配手机号前缀),安全性高但实现复杂,不支持任意位置的模糊查询。
  2. 辅助明文索引列(常规通用场景)

    • 实现逻辑:在数据库表中新增辅助列,如phone_mask(存储138****1234)、phone_prefix(存储138)、email_domain(存储163.com)等,辅助列存储无需加密的模糊查询关键字段;
    • 适用场景:大部分常规模糊查询需求,实现简单且性能高,通过辅助列建立索引,支持快速模糊搜索。
  3. Elasticsearch 卸载(高频率模糊查询场景)

    • 实现逻辑:放弃 MySQL 的模糊查询,将 TypeHandler 解密后的明文数据同步到 Elasticsearch(通过 Canal、消息队列或业务代码触发同步);
    • 适用场景:模糊查询需求频繁、查询条件复杂的场景,在 ES 中建立索引实现高效搜索,获取数据 ID 后回 MySQL 查询详情数据。

架构

2-Learning/05-项目/08-企业级项目深读/02-damai_pro/05-业务深读/03-用户服务深度解析/assets/敏感数据保护框架架构-2919b0e3

规范

复用 Spring Boot MVC 项目最佳实践规范 中的规范

一、配置扩展

1.1 AppProperties 扩展

// 在 AppProperties 中添加加密配置
@Data
@Component
@ConfigurationProperties(prefix = "app")
public class AppProperties {
    
    // ... 已有配置 ...
    
    /**
     * 加密配置
     */
    private EncryptProperties encrypt = new EncryptProperties();
    
    @Data
    public static class EncryptProperties {
        
        /**
         * AES 密钥(16字节)
         */
        private String aesKey = "1234567890123456";
        
        /**
         * AES 初始向量(16字节,确定性加密使用固定IV)
         */
        private String aesIv = "abcdefghijklmnop";
        
        /**
         * SM4 密钥(16字节)
         */
        private String sm4Key = "1234567890123456";
        
        /**
         * SM4 初始向量
         */
        private String sm4Iv = "abcdefghijklmnop";
        
        /**
         * 是否启用字段加密缓存(提升性能)
         */
        private boolean enableCache = true;
        
        /**
         * 缓存过期时间(秒)
         */
        private int cacheExpireSeconds = 3600;
    }
}

1.2 ErrorCode 扩展

// 在 ErrorCode 枚举中添加加密相关错误码
public enum ErrorCode {
    
    // ... 已有错误码 ...
    
    // ============ 加密模块 (6xxxx) ============
    ENCRYPT_ERROR(60001, "encrypt error", "加密失败", ErrorType.SERVER),
    DECRYPT_ERROR(60002, "decrypt error", "解密失败", ErrorType.SERVER),
    UNSUPPORTED_ENCRYPT_ALGORITHM(60003, "unsupported algorithm", "不支持的加密算法", ErrorType.SERVER),
    ENCRYPT_KEY_ERROR(60004, "encrypt key error", "加密密钥配置错误", ErrorType.SERVER),
    DESENSITIZE_ERROR(60005, "desensitize error", "脱敏处理失败", ErrorType.SERVER);
    
    // ...
}

1.3 推荐目录结构

将所有加密、脱敏、算法策略相关的代码统一收敛到 security/crypto 包下。

src/main/java/com/zwnsyw/zwwwspringbootbasetemplate
├── config
│   ├── AppProperties.java                  [MOD] 添加 EncryptProperties 内部类
│   └── ...
├── exception
│   ├── ErrorCode.java                      [MOD] 添加加密相关错误码
│   └── ...
├── security                                [模块根目录]
│   ├── crypto                              [NEW] 数据安全核心模块
│   │   ├── annotation                      [NEW] 注解定义
│   │   │   ├── FieldDesensitize.java       (脱敏注解)
│   │   │   ├── FieldEncrypt.java           (通用加密注解)
│   │   │   └── Password.java               (密码专用注解)
│   │   ├── cache                           [NEW] 性能优化缓存
│   │   │   └── EncryptFieldCache.java      (缓存字段注解信息,减少反射消耗)
│   │   ├── enums                           [NEW] 算法枚举
│   │   │   ├── DesensitizeType.java        (脱敏类型: PHONE, EMAIL...)
│   │   │   └── EncryptAlgorithm.java       (加密算法: AES, BCRYPT...)
│   │   ├── mybatis                         [NEW] MyBatis 扩展组件
│   │   │   ├── handler                     
│   │   │   │   ├── EncryptTypeHandler.java         (通用 TypeHandler基类)
│   │   │   │   ├── PhoneEncryptTypeHandler.java    (具体字段实现)
│   │   │   │   ├── EmailEncryptTypeHandler.java
│   │   │   │   └── IdCardEncryptTypeHandler.java
│   │   │   └── interceptor
│   │   │       └── EncryptQueryInterceptor.java    (查询参数拦截器)
│   │   ├── serializer                      [NEW] Jackson 序列化
│   │   │   └── DesensitizeSerializer.java  (响应脱敏序列化器)
│   │   ├── service                         [NEW] 业务服务封装
│   │   │   └── PasswordService.java        (密码加密与匹配服务)
│   │   ├── strategy                        [NEW] 策略模式核心
│   │   │   ├── impl
│   │   │   │   ├── AesDeterministicEncryptionStrategy.java
│   │   │   │   ├── BCryptEncryptionStrategy.java
│   │   │   │   ├── PhoneDesensitizeStrategy.java
│   │   │   │   ├── ... (其他脱敏策略实现)
│   │   │   └── StrategyFactory.java        (策略工厂)
│   │   │   └── EncryptionStrategy.java     (加密接口)
│   │   │   └── DesensitizeStrategy.java    (脱敏接口)
│   │   └── utils                           [NEW] 工具类
│   │       └── EncryptDesensitizeUtil.java (统一门面工具类)
│   ├── annotation                          [EXIST] 现有的权限注解
│   ├── config                              [EXIST] 现有的安全配置
│   └── ...
└── ...

二、注解定义

2.1 加密算法枚举

package com.example.encrypt.enums;

import lombok.Getter;

/**
 * 加密算法枚举
 */
@Getter
public enum EncryptAlgorithm {
    
    /**
     * BCrypt - 单向哈希,专用于密码
     */
    BCRYPT("bcrypt", "BCrypt单向加密", false),
    
    /**
     * AES 确定性加密 - 相同明文产生相同密文,支持索引
     */
    AES_DETERMINISTIC("aes_deterministic", "AES确定性加密", true),
    
    /**
     * SM4 确定性加密 - 国密算法
     */
    SM4_DETERMINISTIC("sm4_deterministic", "SM4确定性加密", true),
    
    /**
     * AES 随机加密 - 更安全但不支持索引查询
     */
    AES_RANDOM("aes_random", "AES随机加密", true);
    
    private final String code;
    private final String description;
    private final boolean reversible;
    
    EncryptAlgorithm(String code, String description, boolean reversible) {
        this.code = code;
        this.description = description;
        this.reversible = reversible;
    }
}

2.2 脱敏类型枚举

package com.example.encrypt.enums;

import lombok.Getter;

/**
 * 脱敏类型枚举
 */
@Getter
public enum DesensitizeType {
    
    /**
     * 手机号:138****1234
     */
    PHONE("phone", "手机号脱敏"),
    
    /**
     * 邮箱:t***@example.com
     */
    EMAIL("email", "邮箱脱敏"),
    
    /**
     * 身份证:1101**********1234
     */
    ID_CARD("idCard", "身份证脱敏"),
    
    /**
     * 姓名:*三 或 *小明
     */
    NAME("name", "姓名脱敏"),
    
    /**
     * 银行卡:6222 **** **** 1234
     */
    BANK_CARD("bankCard", "银行卡脱敏"),
    
    /**
     * 地址:北京市朝阳区******
     */
    ADDRESS("address", "地址脱敏"),
    
    /**
     * 自定义:保留前N位和后M位
     */
    CUSTOM("custom", "自定义脱敏");
    
    private final String code;
    private final String description;
    
    DesensitizeType(String code, String description) {
        this.code = code;
        this.description = description;
    }
}

2.3 加密注解

package com.example.encrypt.annotation;

import com.example.encrypt.enums.EncryptAlgorithm;

import java.lang.annotation.*;

/**
 * 通用字段加密注解
 * 用于标记需要加密存储的字段
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FieldEncrypt {
    
    /**
     * 加密算法
     */
    EncryptAlgorithm algorithm() default EncryptAlgorithm.AES_DETERMINISTIC;
}
package com.example.encrypt.annotation;

import java.lang.annotation.*;

/**
 * 密码专用加密注解
 * 内置使用 BCrypt 算法,语义化且配置极简
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@FieldEncrypt(algorithm = EncryptAlgorithm.BCRYPT)  // 元注解,内置算法
public @interface Password {
    
    /**
     * 动态盐字段名(同一实体中的字段名)
     * 如果指定,会从该字段获取动态盐
     */
    String saltField() default "";
}

2.4 脱敏注解

package com.example.encrypt.annotation;

import com.example.encrypt.enums.DesensitizeType;
import com.example.encrypt.serializer.DesensitizeSerializer;
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;

import java.lang.annotation.*;

/**
 * 字段脱敏注解
 * 用于标记需要在返回给前端时脱敏的字段
 * 仅在 Jackson 序列化时生效,不影响 Service 层逻辑
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@JacksonAnnotationsInside
@JsonSerialize(using = DesensitizeSerializer.class)
public @interface FieldDesensitize {
    
    /**
     * 脱敏类型
     */
    DesensitizeType type();
    
    /**
     * 可查看原文的权限标识
     * 为空表示所有人都看脱敏后的数据
     * 配置后,拥有该权限的用户可查看原文
     */
    String permissionKey() default "";
    
    /**
     * 可查看原文的角色列表
     * 拥有任一角色即可查看原文
     */
    String[] roles() default {};
    
    /**
     * 自定义脱敏时保留的前缀长度
     * 仅当 type = CUSTOM 时生效
     */
    int prefixLength() default 3;
    
    /**
     * 自定义脱敏时保留的后缀长度
     * 仅当 type = CUSTOM 时生效
     */
    int suffixLength() default 4;
}

三、策略模式实现

3.1 加密策略接口

package com.example.encrypt.strategy;

/**
 * 加密策略接口
 */
public interface EncryptionStrategy {
    
    /**
     * 加密
     * @param plaintext 明文
     * @param salt 盐值(可选,用于密码加密)
     * @return 密文
     */
    String encrypt(String plaintext, String salt);
    
    /**
     * 加密(无盐)
     */
    default String encrypt(String plaintext) {
        return encrypt(plaintext, null);
    }
    
    /**
     * 解密
     * @param ciphertext 密文
     * @param salt 盐值(可选)
     * @return 明文
     */
    String decrypt(String ciphertext, String salt);
    
    /**
     * 解密(无盐)
     */
    default String decrypt(String ciphertext) {
        return decrypt(ciphertext, null);
    }
    
    /**
     * 是否可逆
     */
    boolean isReversible();
    
    /**
     * 验证明文与密文是否匹配(用于不可逆加密)
     */
    default boolean matches(String plaintext, String ciphertext, String salt) {
        throw new UnsupportedOperationException("此加密策略不支持验证操作");
    }
}

3.2 BCrypt 加密策略

package com.example.encrypt.strategy.impl;

import com.example.common.exception.BusinessException;
import com.example.common.exception.ErrorCode;
import com.example.config.AppProperties;
import com.example.encrypt.strategy.EncryptionStrategy;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.crypto.bcrypt.BCrypt;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

/**
 * BCrypt 加密策略
 * 单向加密,专用于密码
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class BCryptEncryptionStrategy implements EncryptionStrategy {
    
    private final AppProperties appProperties;
    
    @Override
    public String encrypt(String plaintext, String salt) {
        if (!StringUtils.hasText(plaintext)) {
            return plaintext;
        }
        
        try {
            // 组合:明文 + 静态盐 + 动态盐
            String staticSalt = appProperties.getSecurity().getPasswordSalt();
            String combinedText = plaintext + staticSalt + (salt != null ? salt : "");
            
            return BCrypt.hashpw(combinedText, BCrypt.gensalt(
                    appProperties.getSecurity().getBcryptStrength()));
        } catch (Exception e) {
            log.error("BCrypt加密失败", e);
            throw new BusinessException(ErrorCode.ENCRYPT_ERROR, "密码加密失败");
        }
    }
    
    @Override
    public String decrypt(String ciphertext, String salt) {
        throw new UnsupportedOperationException("BCrypt是单向加密,不支持解密");
    }
    
    @Override
    public boolean isReversible() {
        return false;
    }
    
    @Override
    public boolean matches(String plaintext, String ciphertext, String salt) {
        if (!StringUtils.hasText(plaintext) || !StringUtils.hasText(ciphertext)) {
            return false;
        }
        
        try {
            String staticSalt = appProperties.getSecurity().getPasswordSalt();
            String combinedText = plaintext + staticSalt + (salt != null ? salt : "");
            return BCrypt.checkpw(combinedText, ciphertext);
        } catch (Exception e) {
            log.error("BCrypt验证失败", e);
            return false;
        }
    }
}

3.3 AES 确定性加密策略

package com.example.encrypt.strategy.impl;

import com.example.common.exception.BusinessException;
import com.example.common.exception.ErrorCode;
import com.example.config.AppProperties;
import com.example.encrypt.strategy.EncryptionStrategy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

/**
 * AES 确定性加密策略
 * 使用固定IV,相同明文产生相同密文,支持索引查询
 */
@Slf4j
@Component
public class AesDeterministicEncryptionStrategy implements EncryptionStrategy {
    
    private static final String ALGORITHM = "AES/CBC/PKCS5Padding";
    private static final String KEY_ALGORITHM = "AES";
    
    private final SecretKeySpec keySpec;
    private final IvParameterSpec ivSpec;
    
    public AesDeterministicEncryptionStrategy(AppProperties appProperties) {
        String key = appProperties.getEncrypt().getAesKey();
        String iv = appProperties.getEncrypt().getAesIv();
        
        // 验证密钥长度
        if (key.length() != 16) {
            throw new BusinessException(ErrorCode.ENCRYPT_KEY_ERROR, 
                    "AES密钥必须为16字节,当前: " + key.length());
        }
        if (iv.length() != 16) {
            throw new BusinessException(ErrorCode.ENCRYPT_KEY_ERROR, 
                    "AES IV必须为16字节,当前: " + iv.length());
        }
        
        this.keySpec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), KEY_ALGORITHM);
        this.ivSpec = new IvParameterSpec(iv.getBytes(StandardCharsets.UTF_8));
    }
    
    @Override
    public String encrypt(String plaintext, String salt) {
        if (!StringUtils.hasText(plaintext)) {
            return plaintext;
        }
        
        try {
            Cipher cipher = Cipher.getInstance(ALGORITHM);
            cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
            byte[] encrypted = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
            return Base64.getEncoder().encodeToString(encrypted);
        } catch (Exception e) {
            log.error("AES加密失败: {}", e.getMessage());
            throw new BusinessException(ErrorCode.ENCRYPT_ERROR, "数据加密失败");
        }
    }
    
    @Override
    public String decrypt(String ciphertext, String salt) {
        if (!StringUtils.hasText(ciphertext)) {
            return ciphertext;
        }
        
        try {
            Cipher cipher = Cipher.getInstance(ALGORITHM);
            cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
            byte[] decoded = Base64.getDecoder().decode(ciphertext);
            byte[] decrypted = cipher.doFinal(decoded);
            return new String(decrypted, StandardCharsets.UTF_8);
        } catch (Exception e) {
            log.error("AES解密失败: {}", e.getMessage());
            throw new BusinessException(ErrorCode.DECRYPT_ERROR, "数据解密失败");
        }
    }
    
    @Override
    public boolean isReversible() {
        return true;
    }
}

3.4 策略工厂

package com.example.encrypt.strategy;

import com.example.common.exception.BusinessException;
import com.example.common.exception.ErrorCode;
import com.example.encrypt.enums.DesensitizeType;
import com.example.encrypt.enums.EncryptAlgorithm;
import com.example.encrypt.strategy.impl.*;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.util.EnumMap;
import java.util.Map;

/**
 * 策略工厂
 * 管理加密策略和脱敏策略的映射关系
 */
@Component
@RequiredArgsConstructor
public class StrategyFactory {
    
    // 加密策略
    private final BCryptEncryptionStrategy bcryptStrategy;
    private final AesDeterministicEncryptionStrategy aesDeterministicStrategy;
    // private final Sm4DeterministicEncryptionStrategy sm4Strategy; // 如需SM4可扩展
    
    // 脱敏策略
    private final PhoneDesensitizeStrategy phoneStrategy;
    private final EmailDesensitizeStrategy emailStrategy;
    private final IdCardDesensitizeStrategy idCardStrategy;
    private final NameDesensitizeStrategy nameStrategy;
    private final BankCardDesensitizeStrategy bankCardStrategy;
    private final AddressDesensitizeStrategy addressStrategy;
    
    private final Map<EncryptAlgorithm, EncryptionStrategy> encryptStrategies = 
            new EnumMap<>(EncryptAlgorithm.class);
    private final Map<DesensitizeType, DesensitizeStrategy> desensitizeStrategies = 
            new EnumMap<>(DesensitizeType.class);
    
    @PostConstruct
    public void init() {
        // 注册加密策略
        encryptStrategies.put(EncryptAlgorithm.BCRYPT, bcryptStrategy);
        encryptStrategies.put(EncryptAlgorithm.AES_DETERMINISTIC, aesDeterministicStrategy);
        encryptStrategies.put(EncryptAlgorithm.AES_RANDOM, aesDeterministicStrategy); // 简化处理
        // encryptStrategies.put(EncryptAlgorithm.SM4_DETERMINISTIC, sm4Strategy);
        
        // 注册脱敏策略
        desensitizeStrategies.put(DesensitizeType.PHONE, phoneStrategy);
        desensitizeStrategies.put(DesensitizeType.EMAIL, emailStrategy);
        desensitizeStrategies.put(DesensitizeType.ID_CARD, idCardStrategy);
        desensitizeStrategies.put(DesensitizeType.NAME, nameStrategy);
        desensitizeStrategies.put(DesensitizeType.BANK_CARD, bankCardStrategy);
        desensitizeStrategies.put(DesensitizeType.ADDRESS, addressStrategy);
    }
    
    /**
     * 获取加密策略
     */
    public EncryptionStrategy getEncryptionStrategy(EncryptAlgorithm algorithm) {
        EncryptionStrategy strategy = encryptStrategies.get(algorithm);
        if (strategy == null) {
            throw new BusinessException(ErrorCode.UNSUPPORTED_ENCRYPT_ALGORITHM, 
                    "不支持的加密算法: " + algorithm);
        }
        return strategy;
    }
    
    /**
     * 获取脱敏策略
     */
    public DesensitizeStrategy getDesensitizeStrategy(DesensitizeType type) {
        DesensitizeStrategy strategy = desensitizeStrategies.get(type);
        if (strategy == null) {
            throw new BusinessException(ErrorCode.DESENSITIZE_ERROR, 
                    "不支持的脱敏类型: " + type);
        }
        return strategy;
    }
}

3.5 脱敏策略接口与实现

package com.example.encrypt.strategy;

/**
 * 脱敏策略接口
 */
public interface DesensitizeStrategy {
    
    /**
     * 执行脱敏
     * @param rawData 原始数据
     * @return 脱敏后的数据
     */
    String desensitize(String rawData);
    
    /**
     * 自定义参数脱敏
     * @param rawData 原始数据
     * @param prefixLength 保留前缀长度
     * @param suffixLength 保留后缀长度
     * @return 脱敏后的数据
     */
    default String desensitize(String rawData, int prefixLength, int suffixLength) {
        return desensitize(rawData);
    }
}
package com.example.encrypt.strategy.impl;

import com.example.encrypt.strategy.DesensitizeStrategy;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

/**
 * 手机号脱敏策略
 * 138****1234
 */
@Component
public class PhoneDesensitizeStrategy implements DesensitizeStrategy {
    
    @Override
    public String desensitize(String rawData) {
        if (!StringUtils.hasText(rawData) || rawData.length() < 7) {
            return rawData;
        }
        return rawData.substring(0, 3) + "****" + rawData.substring(rawData.length() - 4);
    }
}

/**
 * 邮箱脱敏策略
 * t***@example.com
 */
@Component
public class EmailDesensitizeStrategy implements DesensitizeStrategy {
    
    @Override
    public String desensitize(String rawData) {
        if (!StringUtils.hasText(rawData) || !rawData.contains("@")) {
            return rawData;
        }
        int atIndex = rawData.indexOf("@");
        String prefix = rawData.substring(0, atIndex);
        String suffix = rawData.substring(atIndex);
        
        if (prefix.length() <= 1) {
            return prefix + "***" + suffix;
        }
        return prefix.charAt(0) + "***" + suffix;
    }
}

/**
 * 身份证脱敏策略
 * 1101**********1234
 */
@Component
public class IdCardDesensitizeStrategy implements DesensitizeStrategy {
    
    @Override
    public String desensitize(String rawData) {
        if (!StringUtils.hasText(rawData) || rawData.length() < 8) {
            return rawData;
        }
        int prefixLen = 4;
        int suffixLen = 4;
        int maskLen = rawData.length() - prefixLen - suffixLen;
        return rawData.substring(0, prefixLen) + 
               "*".repeat(Math.max(0, maskLen)) + 
               rawData.substring(rawData.length() - suffixLen);
    }
}

/**
 * 姓名脱敏策略
 * 张三 → *三
 * 张小明 → *小明
 */
@Component
public class NameDesensitizeStrategy implements DesensitizeStrategy {
    
    @Override
    public String desensitize(String rawData) {
        if (!StringUtils.hasText(rawData)) {
            return rawData;
        }
        return "*" + rawData.substring(1);
    }
}

/**
 * 银行卡脱敏策略
 * 6222 **** **** 1234
 */
@Component
public class BankCardDesensitizeStrategy implements DesensitizeStrategy {
    
    @Override
    public String desensitize(String rawData) {
        if (!StringUtils.hasText(rawData) || rawData.length() < 8) {
            return rawData;
        }
        return rawData.substring(0, 4) + " **** **** " + 
               rawData.substring(rawData.length() - 4);
    }
}

/**
 * 地址脱敏策略
 * 北京市朝阳区建国路88号 → 北京市朝阳区******
 */
@Component
public class AddressDesensitizeStrategy implements DesensitizeStrategy {
    
    @Override
    public String desensitize(String rawData) {
        if (!StringUtils.hasText(rawData) || rawData.length() <= 6) {
            return rawData;
        }
        return rawData.substring(0, 6) + "******";
    }
}

3.6 加密脱敏工具类

package com.example.encrypt.util;

import com.example.encrypt.annotation.FieldDesensitize;
import com.example.encrypt.annotation.FieldEncrypt;
import com.example.encrypt.annotation.Password;
import com.example.encrypt.enums.EncryptAlgorithm;
import com.example.encrypt.strategy.DesensitizeStrategy;
import com.example.encrypt.strategy.EncryptionStrategy;
import com.example.encrypt.strategy.StrategyFactory;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

import java.lang.reflect.Field;

/**
 * 加密脱敏工具类
 * 提供统一的加解密和脱敏入口
 */
@Component
@RequiredArgsConstructor
public class EncryptDesensitizeUtil {
    
    private final StrategyFactory strategyFactory;
    
    /**
     * 根据字段注解加密
     */
    public String encrypt(Field field, String plaintext) {
        return encrypt(field, plaintext, null);
    }
    
    /**
     * 根据字段注解加密(带盐)
     */
    public String encrypt(Field field, String plaintext, String salt) {
        EncryptAlgorithm algorithm = getEncryptAlgorithm(field);
        if (algorithm == null) {
            return plaintext;
        }
        
        EncryptionStrategy strategy = strategyFactory.getEncryptionStrategy(algorithm);
        return strategy.encrypt(plaintext, salt);
    }
    
    /**
     * 根据字段注解解密
     */
    public String decrypt(Field field, String ciphertext) {
        return decrypt(field, ciphertext, null);
    }
    
    /**
     * 根据字段注解解密(带盐)
     */
    public String decrypt(Field field, String ciphertext, String salt) {
        EncryptAlgorithm algorithm = getEncryptAlgorithm(field);
        if (algorithm == null) {
            return ciphertext;
        }
        
        EncryptionStrategy strategy = strategyFactory.getEncryptionStrategy(algorithm);
        if (!strategy.isReversible()) {
            // 不可逆加密直接返回密文
            return ciphertext;
        }
        return strategy.decrypt(ciphertext, salt);
    }
    
    /**
     * 根据字段注解脱敏
     */
    public String desensitize(Field field, String rawData) {
        FieldDesensitize annotation = field.getAnnotation(FieldDesensitize.class);
        if (annotation == null) {
            return rawData;
        }
        
        DesensitizeStrategy strategy = strategyFactory.getDesensitizeStrategy(annotation.type());
        return strategy.desensitize(rawData, annotation.prefixLength(), annotation.suffixLength());
    }
    
    /**
     * 获取字段的加密算法
     */
    private EncryptAlgorithm getEncryptAlgorithm(Field field) {
        // 先检查 @Password 注解
        Password password = field.getAnnotation(Password.class);
        if (password != null) {
            return EncryptAlgorithm.BCRYPT;
        }
        
        // 再检查 @FieldEncrypt 注解
        FieldEncrypt encrypt = field.getAnnotation(FieldEncrypt.class);
        if (encrypt != null) {
            return encrypt.algorithm();
        }
        
        return null;
    }
    
    /**
     * 检查字段是否需要加密
     */
    public boolean needsEncrypt(Field field) {
        return field.isAnnotationPresent(FieldEncrypt.class) || 
               field.isAnnotationPresent(Password.class);
    }
    
    /**
     * 检查字段是否可逆加密
     */
    public boolean isReversible(Field field) {
        EncryptAlgorithm algorithm = getEncryptAlgorithm(field);
        if (algorithm == null) {
            return false;
        }
        return strategyFactory.getEncryptionStrategy(algorithm).isReversible();
    }
}

四、MyBatis 集成

4.1 字段加密信息缓存

package com.example.encrypt.cache;

import com.example.encrypt.annotation.FieldEncrypt;
import com.example.encrypt.annotation.Password;
import com.example.encrypt.enums.EncryptAlgorithm;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import lombok.Data;
import org.springframework.stereotype.Component;

import java.lang.reflect.Field;
import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * 字段加密信息缓存
 * 避免每次都反射解析字段注解
 */
@Component
public class EncryptFieldCache {
    
    /**
     * 类 -> 加密字段信息列表 的缓存
     */
    private final Cache<Class<?>, List<EncryptFieldInfo>> cache = CacheBuilder.newBuilder()
            .maximumSize(1000)
            .expireAfterAccess(1, TimeUnit.HOURS)
            .build();
    
    /**
     * 获取类的加密字段信息
     */
    public List<EncryptFieldInfo> getEncryptFields(Class<?> clazz) {
        List<EncryptFieldInfo> cached = cache.getIfPresent(clazz);
        if (cached != null) {
            return cached;
        }
        
        List<EncryptFieldInfo> fieldInfos = parseEncryptFields(clazz);
        cache.put(clazz, fieldInfos);
        return fieldInfos;
    }
    
    /**
     * 解析类的加密字段
     */
    private List<EncryptFieldInfo> parseEncryptFields(Class<?> clazz) {
        List<EncryptFieldInfo> result = new ArrayList<>();
        
        // 遍历所有字段(包括父类)
        Class<?> currentClass = clazz;
        while (currentClass != null && currentClass != Object.class) {
            for (Field field : currentClass.getDeclaredFields()) {
                EncryptFieldInfo info = parseField(field);
                if (info != null) {
                    result.add(info);
                }
            }
            currentClass = currentClass.getSuperclass();
        }
        
        return result;
    }
    
    /**
     * 解析单个字段
     */
    private EncryptFieldInfo parseField(Field field) {
        // 检查 @Password 注解
        Password password = field.getAnnotation(Password.class);
        if (password != null) {
            EncryptFieldInfo info = new EncryptFieldInfo();
            info.setField(field);
            info.setFieldName(field.getName());
            info.setAlgorithm(EncryptAlgorithm.BCRYPT);
            info.setSaltFieldName(password.saltField());
            info.setReversible(false);
            return info;
        }
        
        // 检查 @FieldEncrypt 注解
        FieldEncrypt encrypt = field.getAnnotation(FieldEncrypt.class);
        if (encrypt != null) {
            EncryptFieldInfo info = new EncryptFieldInfo();
            info.setField(field);
            info.setFieldName(field.getName());
            info.setAlgorithm(encrypt.algorithm());
            info.setReversible(encrypt.algorithm().isReversible());
            return info;
        }
        
        return null;
    }
    
    /**
     * 加密字段信息
     */
    @Data
    public static class EncryptFieldInfo {
        private Field field;
        private String fieldName;
        private EncryptAlgorithm algorithm;
        private String saltFieldName;
        private boolean reversible;
    }
}

4.2 通用加密 TypeHandler

package com.example.encrypt.handler;

import com.example.encrypt.enums.EncryptAlgorithm;
import com.example.encrypt.strategy.EncryptionStrategy;
import com.example.encrypt.strategy.StrategyFactory;
import com.example.util.SpringContextHolder;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;

import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

/**
 * 通用加密 TypeHandler
 * 处理可逆加密字段的自动加解密
 */
@Slf4j
public class EncryptTypeHandler extends BaseTypeHandler<String> {
    
    private final EncryptAlgorithm algorithm;
    private EncryptionStrategy strategy;
    
    public EncryptTypeHandler(EncryptAlgorithm algorithm) {
        this.algorithm = algorithm;
    }
    
    /**
     * 延迟获取策略(避免循环依赖)
     */
    private EncryptionStrategy getStrategy() {
        if (strategy == null) {
            StrategyFactory factory = SpringContextHolder.getBean(StrategyFactory.class);
            strategy = factory.getEncryptionStrategy(algorithm);
        }
        return strategy;
    }
    
    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) 
            throws SQLException {
        EncryptionStrategy encryptStrategy = getStrategy();
        String encrypted = encryptStrategy.encrypt(parameter);
        ps.setString(i, encrypted);
    }
    
    @Override
    public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
        String value = rs.getString(columnName);
        return decryptIfPossible(value);
    }
    
    @Override
    public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        String value = rs.getString(columnIndex);
        return decryptIfPossible(value);
    }
    
    @Override
    public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        String value = cs.getString(columnIndex);
        return decryptIfPossible(value);
    }
    
    private String decryptIfPossible(String value) {
        if (value == null) {
            return null;
        }
        EncryptionStrategy encryptStrategy = getStrategy();
        if (encryptStrategy.isReversible()) {
            return encryptStrategy.decrypt(value);
        }
        return value;
    }
}

4.3 专用 TypeHandler

package com.example.encrypt.handler;

import com.example.encrypt.enums.EncryptAlgorithm;

/**
 * 手机号加密 TypeHandler
 */
public class PhoneEncryptTypeHandler extends EncryptTypeHandler {
    public PhoneEncryptTypeHandler() {
        super(EncryptAlgorithm.AES_DETERMINISTIC);
    }
}

/**
 * 身份证加密 TypeHandler
 */
public class IdCardEncryptTypeHandler extends EncryptTypeHandler {
    public IdCardEncryptTypeHandler() {
        super(EncryptAlgorithm.AES_DETERMINISTIC);
    }
}

/**
 * 邮箱加密 TypeHandler
 */
public class EmailEncryptTypeHandler extends EncryptTypeHandler {
    public EmailEncryptTypeHandler() {
        super(EncryptAlgorithm.AES_DETERMINISTIC);
    }
}

4.4 查询拦截器(处理 WHERE 条件)

package com.example.encrypt.interceptor;

import com.example.encrypt.cache.EncryptFieldCache;
import com.example.encrypt.cache.EncryptFieldCache.EncryptFieldInfo;
import com.example.encrypt.strategy.EncryptionStrategy;
import com.example.encrypt.strategy.StrategyFactory;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.plugin.*;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.lang.reflect.Field;
import java.sql.PreparedStatement;
import java.util.*;

/**
 * 查询参数加密拦截器
 * 拦截 SQL 参数,对带有 @FieldEncrypt 注解的字段参数进行加密
 * 保证 WHERE 条件中的加密字段能正确匹配数据库中的密文
 */
@Slf4j
@Component
@Intercepts({
    @Signature(type = ParameterHandler.class, method = "setParameters", args = {PreparedStatement.class})
})
@RequiredArgsConstructor
public class EncryptQueryInterceptor implements Interceptor {
    
    private final StrategyFactory strategyFactory;
    private final EncryptFieldCache encryptFieldCache;
    
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        ParameterHandler parameterHandler = (ParameterHandler) invocation.getTarget();
        
        // 获取参数对象
        Field parameterField = parameterHandler.getClass().getDeclaredField("parameterObject");
        parameterField.setAccessible(true);
        Object parameterObject = parameterField.get(parameterHandler);
        
        if (parameterObject != null) {
            processParameter(parameterObject);
        }
        
        return invocation.proceed();
    }
    
    /**
     * 处理参数
     */
    private void processParameter(Object parameter) {
        if (parameter == null) {
            return;
        }
        
        Class<?> clazz = parameter.getClass();
        
        // 1. 简单类型跳过
        if (isSimpleType(clazz)) {
            return;
        }
        
        // 2. Map 类型
        if (parameter instanceof Map) {
            processMapParameter((Map<?, ?>) parameter);
            return;
        }
        
        // 3. Collection 类型
        if (parameter instanceof Collection) {
            for (Object item : (Collection<?>) parameter) {
                processParameter(item);
            }
            return;
        }
        
        // 4. 普通 JavaBean
        processEntityParameter(parameter);
    }
    
    /**
     * 处理 Map 参数
     */
    private void processMapParameter(Map<?, ?> map) {
        for (Object value : map.values()) {
            if (value != null && !isSimpleType(value.getClass())) {
                processParameter(value);
            }
        }
    }
    
    /**
     * 处理实体参数
     */
    private void processEntityParameter(Object entity) {
        Class<?> clazz = entity.getClass();
        
        // 从缓存获取加密字段信息
        List<EncryptFieldInfo> encryptFields = encryptFieldCache.getEncryptFields(clazz);
        
        for (EncryptFieldInfo fieldInfo : encryptFields) {
            // 密码字段跳过(不应该在 WHERE 条件中)
            if (!fieldInfo.isReversible()) {
                continue;
            }
            
            try {
                Field field = fieldInfo.getField();
                field.setAccessible(true);
                Object value = field.get(entity);
                
                if (value instanceof String && StringUtils.hasText((String) value)) {
                    // 获取加密策略并加密
                    EncryptionStrategy strategy = strategyFactory.getEncryptionStrategy(fieldInfo.getAlgorithm());
                    String encrypted = strategy.encrypt((String) value);
                    field.set(entity, encrypted);
                    
                    log.debug("拦截器加密字段: {}.{}", clazz.getSimpleName(), fieldInfo.getFieldName());
                }
            } catch (Exception e) {
                log.error("拦截器加密失败: {}.{}", clazz.getSimpleName(), fieldInfo.getFieldName(), e);
            }
        }
    }
    
    /**
     * 判断是否为简单类型
     */
    private boolean isSimpleType(Class<?> clazz) {
        return clazz.isPrimitive()
                || clazz == String.class
                || Number.class.isAssignableFrom(clazz)
                || clazz == Boolean.class
                || clazz == Character.class
                || Date.class.isAssignableFrom(clazz)
                || clazz.isEnum()
                || clazz.getName().startsWith("java.time");
    }
    
    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }
    
    @Override
    public void setProperties(Properties properties) {
        // 可从配置中读取属性
    }
}

五、脱敏序列化器(权限控制)

package com.example.encrypt.serializer;

import com.example.encrypt.annotation.FieldDesensitize;
import com.example.encrypt.strategy.DesensitizeStrategy;
import com.example.encrypt.strategy.StrategyFactory;
import com.example.security.util.SecurityUtils;
import com.example.util.SpringContextHolder;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.ContextualSerializer;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;

import java.io.IOException;
import java.util.Arrays;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * 脱敏 JSON 序列化器
 * 在返回给前端时根据权限决定是否脱敏
 * 
 * 核心机制:
 * 1. Service 层始终处理明文
 * 2. 仅在 Jackson 序列化时(Controller 返回)执行脱敏
 * 3. 根据用户权限决定是否展示原文
 */
@Slf4j
@NoArgsConstructor
public class DesensitizeSerializer extends JsonSerializer<String> implements ContextualSerializer {
    
    private FieldDesensitize annotation;
    
    public DesensitizeSerializer(FieldDesensitize annotation) {
        this.annotation = annotation;
    }
    
    @Override
    public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) 
            throws IOException {
        
        if (!StringUtils.hasText(value)) {
            gen.writeString(value);
            return;
        }
        
        // 检查是否有权限查看原文
        if (hasPermissionToViewRaw()) {
            gen.writeString(value);
            log.debug("用户有权限查看原文");
            return;
        }
        
        // 执行脱敏
        try {
            StrategyFactory factory = SpringContextHolder.getBean(StrategyFactory.class);
            DesensitizeStrategy strategy = factory.getDesensitizeStrategy(annotation.type());
            
            String desensitized = strategy.desensitize(value, 
                    annotation.prefixLength(), annotation.suffixLength());
            gen.writeString(desensitized);
        } catch (Exception e) {
            log.error("脱敏失败,返回原值", e);
            gen.writeString(value);
        }
    }
    
    /**
     * 检查当前用户是否有权限查看原文
     */
    private boolean hasPermissionToViewRaw() {
        // 1. 检查权限标识
        String permissionKey = annotation.permissionKey();
        if (StringUtils.hasText(permissionKey)) {
            if (SecurityUtils.hasPermission(permissionKey)) {
                return true;
            }
        }
        
        // 2. 检查角色
        String[] allowedRoles = annotation.roles();
        if (allowedRoles != null && allowedRoles.length > 0) {
            Set<String> roleSet = Arrays.stream(allowedRoles).collect(Collectors.toSet());
            if (SecurityUtils.hasAnyRole(roleSet.toArray(new String[0]))) {
                return true;
            }
        }
        
        // 3. 超级管理员始终可以查看
        if (SecurityUtils.isSuperAdmin()) {
            return true;
        }
        
        return false;
    }
    
    @Override
    public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) 
            throws JsonMappingException {
        
        if (property != null) {
            FieldDesensitize desensitize = property.getAnnotation(FieldDesensitize.class);
            if (desensitize == null) {
                desensitize = property.getContextAnnotation(FieldDesensitize.class);
            }
            if (desensitize != null) {
                return new DesensitizeSerializer(desensitize);
            }
        }
        return this;
    }
}

六、密码服务(特殊处理)

package com.example.encrypt.service;

import com.example.encrypt.enums.EncryptAlgorithm;
import com.example.encrypt.strategy.EncryptionStrategy;
import com.example.encrypt.strategy.StrategyFactory;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.UUID;

/**
 * 密码服务
 * 密码是特殊字段,需要单独处理:
 * 1. 使用 BCrypt 单向加密
 * 2. 支持动态盐
 * 3. 不走 TypeHandler(TypeHandler 无法获取同行其他字段的值作为盐)
 */
@Service
@RequiredArgsConstructor
public class PasswordService {
    
    private final StrategyFactory strategyFactory;
    
    /**
     * 加密密码
     * @param rawPassword 原始密码
     * @param dynamicSalt 动态盐
     * @return 加密后的密码
     */
    public String encryptPassword(String rawPassword, String dynamicSalt) {
        EncryptionStrategy strategy = strategyFactory.getEncryptionStrategy(EncryptAlgorithm.BCRYPT);
        return strategy.encrypt(rawPassword, dynamicSalt);
    }
    
    /**
     * 验证密码
     * @param rawPassword 用户输入的密码
     * @param encodedPassword 数据库中的加密密码
     * @param dynamicSalt 动态盐
     * @return 是否匹配
     */
    public boolean matchesPassword(String rawPassword, String encodedPassword, String dynamicSalt) {
        EncryptionStrategy strategy = strategyFactory.getEncryptionStrategy(EncryptAlgorithm.BCRYPT);
        return strategy.matches(rawPassword, encodedPassword, dynamicSalt);
    }
    
    /**
     * 生成动态盐
     * @return 16位随机盐
     */
    public String generateSalt() {
        return UUID.randomUUID().toString().replace("-", "").substring(0, 16);
    }
}

七、使用示例

7.1 实体类

package com.example.entity;

import com.example.encrypt.annotation.FieldEncrypt;
import com.example.encrypt.annotation.Password;
import com.example.encrypt.enums.EncryptAlgorithm;
import lombok.Data;

/**
 * 用户实体
 */
@Data
public class User {
    
    private Long id;
    
    private String username;
    
    /**
     * 密码 - BCrypt 单向加密
     * 需要配合 PasswordService 使用,不走 TypeHandler
     */
    @Password(saltField = "salt")
    private String password;
    
    /**
     * 动态盐
     */
    private String salt;
    
    /**
     * 手机号 - AES 加密
     * 自动通过 TypeHandler 加解密
     */
    @FieldEncrypt(algorithm = EncryptAlgorithm.AES_DETERMINISTIC)
    private String phone;
    
    /**
     * 身份证 - AES 加密
     */
    @FieldEncrypt(algorithm = EncryptAlgorithm.AES_DETERMINISTIC)
    private String idCard;
    
    /**
     * 邮箱 - AES 加密
     */
    @FieldEncrypt(algorithm = EncryptAlgorithm.AES_DETERMINISTIC)
    private String email;
}

7.2 VO 类

package com.example.vo;

import com.example.encrypt.annotation.FieldDesensitize;
import com.example.encrypt.enums.DesensitizeType;
import lombok.Data;

/**
 * 用户信息 VO
 * 返回给前端时根据权限自动脱敏
 */
@Data
public class UserVO {
    
    private Long id;
    
    private String username;
    
    /**
     * 手机号 - ADMIN 和 SUPER_ADMIN 可查看原文
     */
    @FieldDesensitize(type = DesensitizeType.PHONE, roles = {"ADMIN", "SUPER_ADMIN"})
    private String phone;
    
    /**
     * 身份证 - 需要 user:sensitive:view 权限才能查看原文
     */
    @FieldDesensitize(type = DesensitizeType.ID_CARD, permissionKey = "user:sensitive:view")
    private String idCard;
    
    /**
     * 邮箱 - ADMIN 可查看原文
     */
    @FieldDesensitize(type = DesensitizeType.EMAIL, roles = {"ADMIN"})
    private String email;
    
    /**
     * 姓名 - 所有人都看脱敏后的
     */
    @FieldDesensitize(type = DesensitizeType.NAME)
    private String realName;
}

7.3 Mapper 配置

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" 
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mapper.UserMapper">
    
    <resultMap id="UserResultMap" type="com.example.entity.User">
        <id column="id" property="id"/>
        <result column="username" property="username"/>
        <result column="password" property="password"/>
        <result column="salt" property="salt"/>
        <!-- 使用自定义 TypeHandler 自动解密 -->
        <result column="phone" property="phone" 
                typeHandler="com.example.encrypt.handler.PhoneEncryptTypeHandler"/>
        <result column="id_card" property="idCard" 
                typeHandler="com.example.encrypt.handler.IdCardEncryptTypeHandler"/>
        <result column="email" property="email" 
                typeHandler="com.example.encrypt.handler.EmailEncryptTypeHandler"/>
    </resultMap>
    
    <insert id="insert" parameterType="com.example.entity.User">
        INSERT INTO user (username, password, salt, phone, id_card, email)
        VALUES (
            #{username},
            #{password},
            #{salt},
            #{phone, typeHandler=com.example.encrypt.handler.PhoneEncryptTypeHandler},
            #{idCard, typeHandler=com.example.encrypt.handler.IdCardEncryptTypeHandler},
            #{email, typeHandler=com.example.encrypt.handler.EmailEncryptTypeHandler}
        )
    </insert>
    
    <!-- 根据手机号查询 - 拦截器会自动加密 phone 参数 -->
    <select id="selectByPhone" resultMap="UserResultMap">
        SELECT * FROM user WHERE phone = #{phone}
    </select>
    
</mapper>

7.4 Service 层

package com.example.service;

import com.example.common.exception.ErrorCode;
import com.example.common.exception.ThrowUtils;
import com.example.encrypt.service.PasswordService;
import com.example.entity.User;
import com.example.mapper.UserMapper;
import com.example.vo.UserVO;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * 用户服务
 */
@Service
@RequiredArgsConstructor
public class UserService {
    
    private final UserMapper userMapper;
    private final PasswordService passwordService;
    
    /**
     * 用户注册
     */
    @Transactional(rollbackFor = Exception.class)
    public void register(UserRegisterDTO dto) {
        // 1. 生成动态盐
        String salt = passwordService.generateSalt();
        
        // 2. 创建用户实体
        User user = new User();
        user.setUsername(dto.getUsername());
        user.setSalt(salt);
        
        // 3. 密码加密(手动处理,因为需要动态盐)
        String encryptedPassword = passwordService.encryptPassword(dto.getPassword(), salt);
        user.setPassword(encryptedPassword);
        
        // 4. 其他字段直接赋值,TypeHandler 会自动加密
        user.setPhone(dto.getPhone());
        user.setIdCard(dto.getIdCard());
        user.setEmail(dto.getEmail());
        
        // 5. 插入数据库
        userMapper.insert(user);
    }
    
    /**
     * 用户登录
     */
    public UserVO login(String phone, String password) {
        // 1. 根据手机号查询(拦截器自动加密 phone 参数)
        User user = userMapper.selectByPhone(phone);
        ThrowUtils.throwIfNull(user, ErrorCode.USER_NOT_FOUND);
        
        // 2. 验证密码
        boolean matches = passwordService.matchesPassword(password, user.getPassword(), user.getSalt());
        ThrowUtils.throwIf(!matches, ErrorCode.PASSWORD_ERROR);
        
        // 3. 转换为 VO(序列化时会自动脱敏)
        return convertToVO(user);
    }
    
    /**
     * 获取用户信息
     */
    public UserVO getUserInfo(Long userId) {
        User user = userMapper.selectById(userId);
        ThrowUtils.throwIfNull(user, ErrorCode.USER_NOT_FOUND);
        
        // Service 层拿到的是明文
        // 可以用 phone 发短信、用 email 发邮件
        sendSms(user.getPhone(), "您的验证码是...");
        
        // 返回 VO,Jackson 序列化时根据权限自动脱敏
        return convertToVO(user);
    }
    
    private UserVO convertToVO(User user) {
        UserVO vo = new UserVO();
        BeanUtils.copyProperties(user, vo);
        return vo;
    }
    
    private void sendSms(String phone, String message) {
        // 这里拿到的是明文手机号,可以正常发短信
    }
}

八、数据流转图

流程解析:

  1. 入库 (Insert/Update):
    • Service 层传入明文数据 (e.g., phone: "13800001234")。
    • MyBatis 执行 SQL 时,EncryptTypeHandler 捕获该参数。
    • 调用 EncryptionStrategy 将明文转为密文 (e.g., "AES...").
    • DB 中存储密文。
  2. 查询 (Select):
    • 参数处理: Controller 接收查询参数 (明文)。EncryptQueryInterceptor 拦截 SQL 构建过程,通过 EncryptFieldCache 识别加密字段,将查询参数转为密文。
    • SQL 执行: SELECT * FROM user WHERE phone = 'AES_CIPHER_TEXT'.
    • 结果处理: ResultSet 返回密文,EncryptTypeHandler 自动将其解密为明文注入到 Entity 对象中。
    • Service 层拿到的是明文对象。
  3. 响应 (Response):
    • Controller 返回 VO 对象。
    • Spring Boot 调用 Jackson 进行序列化。
    • DesensitizeSerializer 介入,检查用户权限 (SecurityUtils)。
    • 有权限: 写入明文。
    • 无权限: 调用 DesensitizeStrategy 写入掩码数据 (e.g., "138****1234")。
2-Learning/05-项目/08-企业级项目深读/02-damai_pro/05-业务深读/03-用户服务深度解析/assets/数据生命周期-ad3c3f00

九、权限脱敏效果

2-Learning/05-项目/08-企业级项目深读/02-damai_pro/05-业务深读/03-用户服务深度解析/assets/不同角色视图-16f00d4e

设计总结

模块 实现方式 说明
注解体系 @Password@FieldEncrypt@FieldDesensitize 声明式配置,语义清晰
策略模式 EncryptionStrategyDesensitizeStrategy 算法可扩展,符合开闭原则
透明加解密 MyBatis TypeHandler 结果集自动加解密
WHERE条件加密 MyBatis Interceptor + 字段缓存 保证索引有效
权限脱敏 Jackson ContextualSerializer 仅序列化时脱敏,不影响业务
密码特殊处理 PasswordService 支持动态盐,手动调用
性能优化 EncryptFieldCache 缓存反射结果
配置管理 AppProperties 扩展 统一配置入口

企业级项目导航:⬅️ 09-高并发业务设计思维框架 | 10-单体项目加密脱敏最佳实践 | ➡️ 01-Elasticsearch 学习笔记