基于枚举 + 注解的 Java 数据脱敏实践(fastjson 序列化场景)

从零实现一个可复用的脱敏方案:枚举封装脱敏策略、注解标记敏感字段、ValueFilter 在序列化时自动脱敏。

一、背景与痛点

在接口返回数据时,身份证号、手机号、银行卡号等敏感信息不能明文返回给前端,需要做脱敏处理,例如:

复制代码
110101199003074516  ->  110101********4516
13812341234         ->  138****1234

常见的做法是手写工具类、在 getter 里拼字符串,但这样会带来两个问题:

  1. 代码重复:每个字段都要写一遍脱敏逻辑;
  2. 污染业务数据 :在 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 方案还有一个好处:只在输出端脱敏,原始对象字段值不被修改,业务代码拿到的仍是明文。

五、进一步优化(生产建议)

  1. 封装成工具类,避免每个序列化点重复写反射逻辑:
java 复制代码
public final class SensitiveUtil {
    private static final ValueFilter FILTER = (object, name, value) -> { /* 同上 */ };

    public static String toJson(Object obj) {
        return JSON.toJSONString(obj, FILTER);
    }
}
  1. 抽成 AOP 切面:配合 Spring 对 Controller 返回值统一处理,对业务代码零侵入;
  2. 适配 Jackson :改用 JacksonAnnotationIntrospector + JsonSerializer,思路完全一致;
  3. 日志脱敏:打印日志时使用同一套工具,防止敏感信息落盘。

六、总结

环节 方案
策略定义 枚举 + Function<String, String>,策略即代码
字段标记 自定义注解 @Sensitive,声明式配置
触发执行 fastjson ValueFilter 反射扫描注解
数据安全 只在输出端脱敏,不污染原始对象

核心一句话:枚举管"怎么脱",注解管"脱哪里",过滤器管"什么时候脱"。三者解耦,任何一层都可以独立扩展。

相关推荐
SKH.16 分钟前
Linux软件编程(5)线程
java·linux·jvm
半亩码田23 分钟前
C#转Python第4.1篇:当 try-catch 遇上 try-except:异常处理的大不同
开发语言·python·c#
数据知道24 分钟前
Java 安全审计实战:SSRF、反序列化、SpEL 注入
java·开发语言·安全·网络安全
陈年老古董25 分钟前
矿物分类数据处理:缺失值填充方法详解
开发语言·python·机器学习·项目
嗝屁小孩纸29 分钟前
通用后端基础能力平台(多模块技术总结与避坑指南)
java
CodeStats32 分钟前
【Java 表达式引擎】如何设计一套 Java 表达式引擎:从递归下降到 AST 求值的完整实践
java·ai编程·表达式·引擎
whcyhhh1 小时前
头歌实践教学平台:数据科学与大数据技术导论(十八4)
大数据·开发语言·python
vipxieliang1 小时前
ValidX错误消息国际化完全指南:8种语言9个语言包与三级回退机制
java·spring boot
xbgRS1 小时前
RocketMQ消费者消息获取
java·rocketmq·java-rocketmq