加密脱敏最佳实践
在使用这套数据加密脱敏框架时完全不用手动写加密解密代码——给实体类字段加注解就行:密码字段贴@Password,框架会自动用BCrypt做单向加密;手机、邮箱这类字段贴@FieldEncrypt,选个AES之类的可逆算法就搞定加密;需要脱敏的字段加@FieldDesensitize,既能指定脱敏规则(比如手机号隐藏中间4位),还能关联权限;数据存库时MyBatis的TypeHandler会自动把注解字段转成密文,取出来又自动解密回明文,全程无感;用加密字段查数据也不用管,查询拦截器会自动把参数加密后再查,保证索引能用,它还能智能解析List、嵌套对象这些复杂参数,性能也靠谱;脱敏只在接口返回前端时处理,管理员能看到原始数据方便调试,普通用户只能看脱敏后的,而且服务层流转的都是明文,发短信这类业务不会受影响,加密字段要做模糊查询的话,加个辅助列、分词加密或者同步到ES都能解决。
思路
以注解驱动为入口,策略模式为核心算法扩展内核,结合MyBatis 生态组件实现加密解密的无感知处理,同时通过权限校验实现脱敏逻辑的精细化管控。
一、注解体系设计
框架定义分层的自定义注解,作为加密 / 脱敏逻辑的触发标识与规则配置载体,注解通过枚举类型实现策略的精准绑定:
- 加密注解分类
- 专用加密注解(@Password 等):无额外属性,内置绑定单向加密策略,专用于密码等特殊字段标注,语义化且配置极简。
- 通用加密注解(@FieldEncrypt):核心属性为
encryptAlgorithm(加密算法枚举,如 AES_DETERMINISTIC、SM4_DETERMINISTIC、BCRYPT 等),用于手机、邮箱等可逆加密场景,通过枚举指定具体加密策略。
- 脱敏注解(@FieldDesensitize)
- 核心属性 1:
desensitizeType(脱敏方式枚举,如 PHONE、EMAIL、ID_CARD 等),指定字段的脱敏规则; - 核心属性 2:
permissionKey(权限标识字符串),关联权限校验逻辑,用于判断是否执行脱敏。
- 核心属性 1:
- 注解元数据配置:所有注解均设置
@Target(ElementType.FIELD)(作用于实体类字段)、@Retention(RetentionPolicy.RUNTIME)(运行时保留,支持反射获取注解信息)。
二、策略模式的算法封装实现
基于策略模式解耦算法定义与实现,通过接口标准化行为,具体实现类承载不同算法逻辑,同时封装工具类提供统一入口:
- 加密策略层
- 抽象策略接口(EncryptionStrategy):定义核心方法,
String encrypt(String plaintext)(加密方法,所有算法必须实现)、String decrypt(String ciphertext)(解密方法,仅可逆算法实现,默认抛出不支持异常); - 具体策略实现类:
BCryptEncryptionStrategy:实现encrypt方法,采用 BCrypt 算法进行单向加密,decrypt方法直接抛出 UnsupportedOperationException;AesDeterministicEncryptionStrategy:实现encrypt和decrypt方法,采用 AES 算法的确定性加密模式(固定密钥与填充方式,保证相同明文输出相同密文),支持可逆处理;- 扩展实现
SM4EncryptionStrategy、RSAEncryptionStrategy等,只需实现接口即可接入,供真实业务需求选择。
- 抽象策略接口(EncryptionStrategy):定义核心方法,
- 脱敏策略层
- 抽象策略接口(DesensitizeStrategy):定义
String desensitize(String rawData, String permission)方法,入参为原始数据与用户权限标识,返回处理后的数据; - 具体策略实现类:
PhoneDesensitizeStrategy:对手机号执行138****1234格式的脱敏处理;EmailDesensitizeStrategy:对邮箱执行xxx@163.com转为x**@163.com的脱敏处理;- 扩展
IdCardDesensitizeStrategy、AddressDesensitizeStrategy等。
- 抽象策略接口(DesensitizeStrategy):定义
- 策略工厂与工具类封装
- 策略工厂(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)方法,内部通过反射获取字段注解信息,自动匹配并调用对应策略。
- 策略工厂(StrategyFactory):通过枚举类型作为 key,维护 “枚举 - 策略实现类” 的映射关系,提供
三、MyBatis 生态整合的无感知处理
利用 MyBatis 的 TypeHandler 和 Interceptor 组件,实现数据库交互过程中加密解密的自动化处理,无需业务代码介入,并针对拦截器参数解析问题优化处理逻辑:
- 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指定。
- 继承 MyBatis 的
- 查询拦截器实现(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。
- 步骤 1:通过
- 性能优化方案:引入缓存机制(如 Guava Cache),缓存
Class对象与对应加密字段的映射关系(即某个 Class 哪些字段带有加密注解),避免每次解析参数时都反射遍历所有字段,降低 CPU 消耗。 - 作用:针对加密字段的查询(如
where phone = ?),保证查询参数与数据库中存储的密文一致,使基于加密字段创建的索引正常生效,避免全表扫描。
- 实现 MyBatis 的
四、权限脱敏的技术实现
脱敏逻辑与权限系统联动,明确脱敏执行时机,解决 Service 层逻辑错误问题,实现安全与调试效率的平衡:
- 权限校验接口(PermissionValidator):定义
boolean hasPermission(String permissionKey, String userPermission)方法,提供权限判断的标准化接口,具体实现可对接 Spring Security、Shiro 等权限框架; - 用户权限获取:通过 ThreadLocal 存储当前请求的用户权限信息(如在拦截器中从 Token、Session 中解析并放入 ThreadLocal),在脱敏时直接获取,保证线程安全;
- 脱敏执行时机与实现(核心优化)
- 明确执行阶段:脱敏仅发生在 Controller 返回给前端的序列化阶段(Jackson Serializer),Service/DAO 层始终流转明文(TypeHandler 负责将数据库密文解密为明文),避免 Service 层业务逻辑(如发送短信)因脱敏失效。
- 具体实现方式:
- 自定义 Jackson 的
ContextualSerializer(如DesensitizeSerializer),实现JsonSerializer接口; - 在序列化阶段,通过反射获取字段的
@FieldDesensitize注解,调用PermissionValidator的hasPermission方法进行权限判断:- 若返回
true(如管理员、开发人员权限):直接序列化原始明文数据; - 若返回
false(如普通用户权限):调用对应脱敏策略,序列化脱敏后的数据;
- 若返回
- 配置方式:通过注解
@JsonSerialize(using = DesensitizeSerializer.class)绑定到实体类字段,或全局配置序列化器自动识别@FieldDesensitize注解。
- 自定义 Jackson 的
- 防数据污染处理:不在 Service 层修改对象本身的值,仅在序列化阶段做脱敏处理,避免缓存(如 Redis)中存入脱敏后的无效数据。
五、扩展性与兼容性设计
- 算法扩展:新增加密算法或脱敏方式时,只需新增策略实现类,在枚举类中添加对应枚举值,修改策略工厂的映射关系,无需改动原有业务代码,遵循开闭原则;
- 密钥管理:对于对称加密算法(如 AES、SM4),框架提供密钥配置类(EncryptKeyConfig),支持从配置文件(application.yml/properties)读取密钥,或对接密钥管理服务(KMS)实现密钥的安全存储与获取;
- 多数据源兼容:通过 MyBatis 的插件机制,拦截器和 TypeHandler 可适配不同的数据源(MySQL、Oracle 等),只需保证字段类型与 JDBC 类型的映射一致。
六、模糊查询问题解决方案
针对加密字段模糊查询的 “死穴”,提供三种适配不同场景的解决方案,按需选择:
- 分词密文法(高安全场景)
- 实现逻辑:对需模糊查询的字段(如手机号)进行分段加密(如手机号 13812345678 分为 138、1234、5678 三段,每段分别采用确定性加密);
- 适用场景:仅需特定分段匹配的场景(如匹配手机号前缀),安全性高但实现复杂,不支持任意位置的模糊查询。
- 辅助明文索引列(常规通用场景)
- 实现逻辑:在数据库表中新增辅助列,如
phone_mask(存储138****1234)、phone_prefix(存储138)、email_domain(存储163.com)等,辅助列存储无需加密的模糊查询关键字段; - 适用场景:大部分常规模糊查询需求,实现简单且性能高,通过辅助列建立索引,支持快速模糊搜索。
- 实现逻辑:在数据库表中新增辅助列,如
- Elasticsearch 卸载(高频率模糊查询场景)
- 实现逻辑:放弃 MySQL 的模糊查询,将 TypeHandler 解密后的明文数据同步到 Elasticsearch(通过 Canal、消息队列或业务代码触发同步);
- 适用场景:模糊查询需求频繁、查询条件复杂的场景,在 ES 中建立索引实现高效搜索,获取数据 ID 后回 MySQL 查询详情数据。
架构
规范
复用 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) {
// 这里拿到的是明文手机号,可以正常发短信
}
}
八、数据流转图
流程解析:
- 入库 (Insert/Update):
Service层传入明文数据 (e.g., phone: "13800001234")。MyBatis执行 SQL 时,EncryptTypeHandler捕获该参数。- 调用
EncryptionStrategy将明文转为密文 (e.g., "AES..."). - DB 中存储密文。
- 查询 (Select):
- 参数处理:
Controller接收查询参数 (明文)。EncryptQueryInterceptor拦截 SQL 构建过程,通过EncryptFieldCache识别加密字段,将查询参数转为密文。 - SQL 执行:
SELECT * FROM user WHERE phone = 'AES_CIPHER_TEXT'. - 结果处理:
ResultSet返回密文,EncryptTypeHandler自动将其解密为明文注入到Entity对象中。 Service层拿到的是明文对象。
- 参数处理:
- 响应 (Response):
Controller返回VO对象。- Spring Boot 调用
Jackson进行序列化。 DesensitizeSerializer介入,检查用户权限 (SecurityUtils)。- 有权限: 写入明文。
- 无权限: 调用
DesensitizeStrategy写入掩码数据 (e.g., "138****1234")。
九、权限脱敏效果
设计总结
| 模块 | 实现方式 | 说明 |
|---|---|---|
| 注解体系 | @Password、@FieldEncrypt、 @FieldDesensitize |
声明式配置,语义清晰 |
| 策略模式 | EncryptionStrategy、DesensitizeStrategy |
算法可扩展,符合开闭原则 |
| 透明加解密 | MyBatis TypeHandler | 结果集自动加解密 |
| WHERE条件加密 | MyBatis Interceptor + 字段缓存 | 保证索引有效 |
| 权限脱敏 | Jackson ContextualSerializer | 仅序列化时脱敏,不影响业务 |
| 密码特殊处理 | PasswordService | 支持动态盐,手动调用 |
| 性能优化 | EncryptFieldCache | 缓存反射结果 |
| 配置管理 | AppProperties 扩展 | 统一配置入口 |
💬 评论