从零实现一个可复用的脱敏方案:枚举封装脱敏策略、注解标记敏感字段、ValueFilter 在序列化时自动脱敏。
一、背景与痛点
在接口返回数据时,身份证号、手机号、银行卡号等敏感信息不能明文返回给前端,需要做脱敏处理,例如:
110101199003074516 -> 110101********4516
13812341234 -> 138****1234
常见的做法是手写工具类、在 getter 里拼字符串,但这样会带来两个问题:
- 代码重复:每个字段都要写一遍脱敏逻辑;
- 污染业务数据 :在
getter里做处理,会导致后续业务拿到的数据也是脱敏后的,无法还原。
更优雅的思路是:声明式脱敏------用注解标记"这个字段要脱敏",用枚举定义"脱敏成什么样",在序列化出口统一执行。
二、方案设计
三个角色,各司其职:
| 角色 | 职责 |
|---|---|
SensitiveStrategy 枚举 |
定义各种脱敏策略(姓名、手机号、身份证......),每种策略绑定一个脱敏函数 |
@Sensitive 注解 |
标记实体类的哪个字段需要脱敏,并指定用哪个策略 |
序列化拦截(ValueFilter) |
在对象转 JSON 时扫描注解、执行脱敏,不改动原始对象 |
设计核心点:枚举持函数 ------ 枚举常量通过构造器绑定一个 Function<String, String>,让"策略"本身变成可执行的逻辑。
三、代码实现
1. 脱敏策略枚举
java
package a28.com.md5.annotation;
import java.util.function.Function;
/**
* 脱敏策略枚举
*/
public enum SensitiveStrategy {
/** 姓名脱敏:张三 -> 张** */
NAME(s -> s.replaceAll("(.).*", "$1**")),
/** 身份证脱敏:110101199003074516 -> 110101********4516 */
ID_CARD(s -> s.replaceAll("(\\d{6})\\d{8}(\\w{4})", "$1********$2")),
/** 手机号脱敏:13812341234 -> 138****1234 */
MOBILE(s -> s.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2")),
/** 银行卡脱敏:6222021234567890 -> 6222 **** 7890 */
BANK_CARD(s -> s.replaceAll("(\\d{4})\\d+(\\d{4})", "$1 **** $2")),
/** 邮箱脱敏:zhangsan@163.com -> z****@163.com */
EMAIL(s -> s.replaceAll("(\\w).*(@.*)", "$1****$2")),
/** 地址脱敏:保留前 6 位,其余打码 */
ADDRESS(s -> s.length() > 6 ? s.substring(0, 6) + "****" : s),
/** 密码脱敏:全部打码 */
PASSWORD(s -> "******"),
/** 固话脱敏:010-12345678 -> 010-****5678 */
FIXED_PHONE(s -> s.replaceAll("(\\d{3,4}-)\\d{4}(\\d{4})", "$1****$2"));
private final Function<String, String> desensitizer;
SensitiveStrategy(Function<String, String> desensitizer) {
this.desensitizer = desensitizer;
}
public String desensitize(String value) {
if (value == null || value.isEmpty()) {
return value;
}
return desensitizer.apply(value);
}
}
要点说明:
- 每种策略就是一个 lambda,职责单一、易于扩展------新增策略只需加一个常量;
desensitize()统一入口对null/ 空串做了保护,避免 NPE 和下标越界。
2. 脱敏注解
java
package a28.com.md5.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 敏感字段脱敏注解
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Sensitive {
/** 脱敏策略,默认身份证脱敏 */
SensitiveStrategy strategy() default SensitiveStrategy.ID_CARD;
}
两个元注解缺一不可:
@Retention(RUNTIME):注解要保留到运行时,反射才能读到;@Target(FIELD):只允许标在字段上。
3. 实体类使用
java
public class Person {
@Sensitive(strategy = SensitiveStrategy.NAME)
private String name;
@Sensitive(strategy = SensitiveStrategy.ID_CARD)
private String idCard;
// 构造器、getter/setter 省略
}
4. 序列化时自动脱敏(关键一步)
java
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.ValueFilter;
import java.lang.reflect.Field;
public class Main {
public static void main(String[] args) {
Person per = new Person("宇文成都", "110101199003074516");
System.out.println(JSON.toJSONString(per, (ValueFilter) (object, name, value) -> {
if (value instanceof String) {
try {
Field field = object.getClass().getDeclaredField(name);
Sensitive sensitive = field.getAnnotation(Sensitive.class);
if (sensitive != null) {
return sensitive.strategy().desensitize((String) value);
}
} catch (NoSuchFieldException ignored) {
// 无对应字段则原样返回
}
}
return value;
}));
}
}
输出结果:
json
{"idCard":"110101********4516","name":"宇**"}
四、为什么"注解不会自动生效"?
这是很多新手踩的坑:注解本身是惰性的。
- 注解(哪怕是
@Retention(RUNTIME))只是一段附加在代码上的元数据,没有任何内在行为; - 必须由某个"执行者"通过反射读取注解,再去调用对应的逻辑;
- fastjson 序列化时只认它自己的注解(如
@JSONField),不会去扫描你的自定义注解。
所以"枚举 + 注解"只是搭好了骨架,还缺一个触发点。这里的 ValueFilter 就是触发点:fastjson 序列化每个字段时都会回调它,我们趁机反射查注解、执行脱敏。
ValueFilter 方案还有一个好处:只在输出端脱敏,原始对象字段值不被修改,业务代码拿到的仍是明文。
五、进一步优化(生产建议)
- 封装成工具类,避免每个序列化点重复写反射逻辑:
java
public final class SensitiveUtil {
private static final ValueFilter FILTER = (object, name, value) -> { /* 同上 */ };
public static String toJson(Object obj) {
return JSON.toJSONString(obj, FILTER);
}
}
- 抽成 AOP 切面:配合 Spring 对 Controller 返回值统一处理,对业务代码零侵入;
- 适配 Jackson :改用
JacksonAnnotationIntrospector+JsonSerializer,思路完全一致; - 日志脱敏:打印日志时使用同一套工具,防止敏感信息落盘。
六、总结
| 环节 | 方案 |
|---|---|
| 策略定义 | 枚举 + Function<String, String>,策略即代码 |
| 字段标记 | 自定义注解 @Sensitive,声明式配置 |
| 触发执行 | fastjson ValueFilter 反射扫描注解 |
| 数据安全 | 只在输出端脱敏,不污染原始对象 |
核心一句话:枚举管"怎么脱",注解管"脱哪里",过滤器管"什么时候脱"。三者解耦,任何一层都可以独立扩展。