加密脱敏最佳实践

在使用这套数据加密脱敏框架时完全不用手动写加密解密代码——给实体类字段加注解就行:密码字段贴@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 查询详情数据。

架构

7-Blog/后端与微服务/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")。
7-Blog/后端与微服务/assets/数据生命周期-ad3c3f00

九、权限脱敏效果

7-Blog/后端与微服务/assets/不同角色视图-16f00d4e

设计总结

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