--- title: "04-加密脱敏最佳实践" created: 2026-01-09 tags: - 博客 --- # 加密脱敏最佳实践 > 在使用这套数据加密脱敏框架时完全不用手动写加密解密代码——给实体类字段加注解就行:密码字段贴@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`:实现 `encrypt`和`decrypt`方法,采用 AES 算法的确定性加密模式(固定密钥与填充方式,保证相同明文输出相同密文),支持可逆处理; - 扩展实现`SM4EncryptionStrategy`、`RSAEncryptionStrategy`等,只需实现接口即可接入,供真实业务需求选择。 2. **脱敏策略层** - **抽象策略接口(DesensitizeStrategy)**:定义`String desensitize(String rawData, String permission)`方法,入参为原始数据与用户权限标识,返回处理后的数据; - **具体策略实现类**: - `PhoneDesensitizeStrategy`:对手机号执行`138****1234`格式的脱敏处理; - `EmailDesensitizeStrategy`:对邮箱执行`xxx@163.com`转为 `x**@163.com`的脱敏处理; - 扩展`IdCardDesensitizeStrategy`、`AddressDesensitizeStrategy`等。 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`,重写核心方法: - `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`接口,拦截点为`StatementHandler`的 `prepare`方法,针对参数解析缺陷优化处理流程: - **步骤 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`注解,调用 `PermissionValidator`的`hasPermission`方法进行权限判断: - 若返回`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.jpg]] ## 规范 复用 [[2-Learning/05-项目/05-最佳实践/01-MVC分层与项目规范/01-Spring Boot MVC 项目最佳实践规范|Spring Boot MVC 项目最佳实践规范]] 中的规范 ## **一、配置扩展** ### **1.1 AppProperties 扩展** ```java // 在 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 加密算法枚举** ``` java 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 脱敏类型枚举** ``` java 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 加密注解** ```java 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; } ``` ```java 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 脱敏注解** ```java 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 加密策略接口** ```java 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 加密策略** ```java 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 确定性加密策略** ```java 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 策略工厂** ```java 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 encryptStrategies = new EnumMap<>(EncryptAlgorithm.class); private final Map 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 脱敏策略接口与实现** ```typescript 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); } } ``` ```java 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 加密脱敏工具类** ```java 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 字段加密信息缓存** ```java 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, List> cache = CacheBuilder.newBuilder() .maximumSize(1000) .expireAfterAccess(1, TimeUnit.HOURS) .build(); /** * 获取类的加密字段信息 */ public List getEncryptFields(Class clazz) { List cached = cache.getIfPresent(clazz); if (cached != null) { return cached; } List fieldInfos = parseEncryptFields(clazz); cache.put(clazz, fieldInfos); return fieldInfos; } /** * 解析类的加密字段 */ private List parseEncryptFields(Class clazz) { List 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** ```java 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 { 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** ```java 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 条件)** ```java 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 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) { // 可从配置中读取属性 } } ``` ## **五、脱敏序列化器(权限控制)** ```java 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 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 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; } } ``` ## **六、密码服务(特殊处理)** ```java 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 实体类** ```java 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 类** ```java 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 配置** ```sql 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} ) ``` ### **7.4 Service 层** ```java 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.jpg]] ## **九、权限脱敏效果** ![[7-Blog/后端与微服务/assets/不同角色视图-16f00d4e.jpg]] ## **设计总结** | **模块** | **实现方式** | **说明** | | --- | --- | --- | | **注解体系** | `@Password`、`@FieldEncrypt`、 `@FieldDesensitize` | 声明式配置,语义清晰 | | **策略模式** | `EncryptionStrategy`、`DesensitizeStrategy` | 算法可扩展,符合开闭原则 | | **透明加解密** | MyBatis TypeHandler | 结果集自动加解密 | | **WHERE条件加密** | MyBatis Interceptor + 字段缓存 | 保证索引有效 | | **权限脱敏** | Jackson ContextualSerializer | 仅序列化时脱敏,不影响业务 | | **密码特殊处理** | PasswordService | 支持动态盐,手动调用 | | **性能优化** | EncryptFieldCache | 缓存反射结果 | | **配置管理** | AppProperties 扩展 | 统一配置入口 |