文章目录

1. 设计背景与总体目标

  • 业务背景:系统包含用户手机号、身份证号、邮箱、银行卡号等多种敏感信息,需要在不同角色、不同场景下展示不同程度的脱敏结果,同时兼顾性能、安全与可扩展性。
  • 总体目标
    • 统一:提供统一的注解式脱敏能力,对业务代码侵入小、与现有 Spring Boot 3.x + MyBatis Plus + Redis + MySQL + JWT + Redisson + Caffeine 架构无缝集成。
    • 高性能:在高并发场景下(15,000+ QPS、RT < 10ms)不会成为性能瓶颈,支持响应式 WebFlux 与传统 MVC。
    • 安全可控:支持按角色 / 权限控制脱敏级别,敏感数据加密存储与安全传输,脱敏规则不可被恶意获取。
    • 可扩展:可插拔的策略模式,支持快速新增脱敏算法和业务自定义规则。

2. 技术栈与兼容性设计

  • 运行时框架:Spring Boot 3.x
  • 持久层:MyBatis Plus + MySQL
  • 缓存:Redis(分布式缓存) + Caffeine(本地缓存)+ 三级缓存架构
  • 分布式基础设施:Redisson 分布式锁
  • 安全与认证:Spring Security / WebFlux Security + JWT 鉴权
  • Web 框架:Spring MVC + Spring WebFlux

兼容性原则

  • 脱敏尽量在“出站”(返回给前端)阶段完成,尽量不影响持久层与业务逻辑,避免侵入现有 Mapper / Service 实现。
  • 统一序列化层处理:基于 Jackson/JSON 策略,在对象序列化为 JSON 时根据注解和当前用户权限进行脱敏。
  • 对 MyBatis Plus 的影响:数据从数据库读写仍使用原始敏感数据,写入阶段可以选择性加密;脱敏主要在返回 API 时生效。
  • 对缓存层的影响:缓存中可以存储原始数据或加密数据,脱敏在读取缓存数据并返回 API 前统一处理,避免重复计算,必要时结合 Caffeine 缓存脱敏后的结果(按“数据 + 角色”维度)。

3. 脱敏策略与规则模型

3.1 脱敏类型定义

定义脱敏类型枚举,覆盖常见场景并支持扩展:

public enum MaskType {
    MOBILE,        // 手机号
    ID_CARD,       // 身份证号
    EMAIL,         // 邮箱
    BANK_CARD,     // 银行卡
    USERNAME,      // 姓名/用户名
    CUSTOM         // 自定义规则
}
3.2 规则参数与通用上下文
  • 基本规则参数

    • keepPrefix:保留前缀长度
    • keepSuffix:保留后缀长度
    • maskChar:掩码字符(默认为 *
    • maskLength:固定掩码长度(可选)
  • 脱敏上下文 DataMaskContext

    • 当前登录用户 ID
    • 用户角色/权限列表
    • 当前调用方应用 ID(如有多租户 / 合作方场景)
    • 字段上配置的脱敏注解信息
public class DataMaskContext {
    private Long userId;
    private List<String> roles;
    private Map<String, Object> attributes; // 可扩展

    // 构造 & getter/setter 省略
}

4. 注解设计与使用方式

4.1 核心注解:@DataMask
import java.lang.annotation.*;

@Documented
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface DataMask {

    MaskType type();

    // 可选参数:前缀、后缀等
    int prefix() default 0;

    int suffix() default 0;

    char maskChar() default '*';

    // 是否支持根据角色放开脱敏
    String[] allowRoles() default {};

    // 脱敏等级,例如:NONE(明文)/LOW/MEDIUM/HIGH
    String level() default "MEDIUM";

    // 自定义规则标识(与注册的策略 Bean 匹配)
    String customRule() default "";
}
4.2 语义别名注解:@Sensitive

对业务开发者提供更友好的注解别名:

@Documented
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@DataMask(type = MaskType.CUSTOM)
public @interface Sensitive {
    String value() default ""; // 指定自定义规则名
}
4.3 实体类使用示例
public class UserInfoVO {

    @DataMask(type = MaskType.MOBILE, prefix = 3, suffix = 4)
    private String mobile;

    @DataMask(type = MaskType.ID_CARD, prefix = 6, suffix = 4)
    private String idCardNo;

    @DataMask(type = MaskType.EMAIL)
    private String email;

    @DataMask(type = MaskType.BANK_CARD, prefix = 4, suffix = 4)
    private String bankCardNo;

    @Sensitive("VIP_CUSTOM_RULE")
    private String vipInfo;

    // getter/setter 省略
}

5. 脱敏引擎与策略模式设计

5.1 策略接口定义
public interface DataMasker {

    /**
     * 判断该策略是否支持当前字段配置
     */
    boolean supports(MaskType type, String customRule);

    /**
     * 执行脱敏
     */
    Object mask(Object origin, DataMask annotation, DataMaskContext context);
}
5.2 内置策略实现示例
public class MobileDataMasker implements DataMasker {

    @Override
    public boolean supports(MaskType type, String customRule) {
        return type == MaskType.MOBILE;
    }

    @Override
    public Object mask(Object origin, DataMask annotation, DataMaskContext context) {
        if (origin == null) {
            return null;
        }
        String value = origin.toString();
        int prefix = annotation.prefix() == 0 ? 3 : annotation.prefix();
        int suffix = annotation.suffix() == 0 ? 4 : annotation.suffix();
        return maskMiddle(value, prefix, suffix, annotation.maskChar());
    }

    private String maskMiddle(String value, int prefix, int suffix, char maskChar) {
        if (value.length() <= prefix + suffix) {
            return value;
        }
        StringBuilder sb = new StringBuilder();
        sb.append(value, 0, prefix);
        for (int i = 0; i < value.length() - prefix - suffix; i++) {
            sb.append(maskChar);
        }
        sb.append(value, value.length() - suffix, value.length());
        return sb.toString();
    }
}

身份证号、邮箱、银行卡号可采用类似实现,也可以抽取公用工具类进行序列化优化。

5.3 策略注册与工厂
public class DataMaskerFactory {

    private final List<DataMasker> maskers;

    public DataMaskerFactory(List<DataMasker> maskers) {
        this.maskers = maskers;
    }

    public DataMasker getMasker(MaskType type, String customRule) {
        return maskers.stream()
                .filter(m -> m.supports(type, customRule))
                .findFirst()
                .orElseThrow(() -> new IllegalArgumentException("No DataMasker found for type=" + type));
    }
}

DataMasker 列表通过 Spring 容器自动注入(可使用 @Component 注册每个策略)。


6. 注解驱动的脱敏实现方式

6.1 基于 Jackson 的序列化扩展(MVC & WebFlux 通用)

核心思想:通过自定义 BeanSerializerModifierJsonSerializer,在 JSON 序列化时根据字段上的 @DataMask 注解和当前用户角色执行脱敏。

public class DataMaskBeanSerializerModifier extends BeanSerializerModifier {

    private final DataMaskerFactory factory;
    private final SecurityContextProvider securityContextProvider;

    public DataMaskBeanSerializerModifier(DataMaskerFactory factory,
                                          SecurityContextProvider securityContextProvider) {
        this.factory = factory;
        this.securityContextProvider = securityContextProvider;
    }

    @Override
    public List<BeanPropertyWriter> changeProperties(SerializationConfig config,
                                                     BeanDescription beanDesc,
                                                     List<BeanPropertyWriter> beanProperties) {
        for (BeanPropertyWriter writer : beanProperties) {
            DataMask dataMask = writer.getAnnotation(DataMask.class);
            if (dataMask != null) {
                writer.assignSerializer(new DataMaskingJsonSerializer(writer, dataMask, factory, securityContextProvider));
            }
        }
        return beanProperties;
    }
}
public class DataMaskingJsonSerializer extends JsonSerializer<Object> {

    private final BeanPropertyWriter writer;
    private final DataMask dataMask;
    private final DataMaskerFactory factory;
    private final SecurityContextProvider securityContextProvider;

    public DataMaskingJsonSerializer(BeanPropertyWriter writer,
                                     DataMask dataMask,
                                     DataMaskerFactory factory,
                                     SecurityContextProvider securityContextProvider) {
        this.writer = writer;
        this.dataMask = dataMask;
        this.factory = factory;
        this.securityContextProvider = securityContextProvider;
    }

    @Override
    public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
        DataMaskContext context = securityContextProvider.buildMaskContext();

        // 基于角色/权限判断是否需要脱敏
        if (!shouldMask(context, dataMask)) {
            gen.writeObject(value);
            return;
        }

        DataMasker masker = factory.getMasker(dataMask.type(), dataMask.customRule());
        Object masked = masker.mask(value, dataMask, context);
        gen.writeObject(masked);
    }

    private boolean shouldMask(DataMaskContext context, DataMask dataMask) {
        String[] allowRoles = dataMask.allowRoles();
        if (allowRoles.length == 0) {
            return true; // 默认需要脱敏
        }
        if (context.getRoles() == null) {
            return true;
        }
        // 只要存在允许角色之一,则显示明文
        for (String role : allowRoles) {
            if (context.getRoles().contains(role)) {
                return false;
            }
        }
        return true;
    }
}
6.2 安全上下文获取(JWT + Spring Security 集成)
public interface SecurityContextProvider {
    DataMaskContext buildMaskContext();
}

public class SpringSecurityContextProvider implements SecurityContextProvider {

    @Override
    public DataMaskContext buildMaskContext() {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        Long userId = null;
        List<String> roles = new ArrayList<>();

        if (auth != null && auth.isAuthenticated()) {
            Object principal = auth.getPrincipal();
            // 可从 JWT 中解析 userId、roles
            // 这里示例从自定义 UserDetails 中获取
            if (principal instanceof CustomUserDetails cud) {
                userId = cud.getUserId();
                roles = cud.getRoleCodes();
            }
        }

        DataMaskContext context = new DataMaskContext();
        context.setUserId(userId);
        context.setRoles(roles);
        return context;
    }
}

通过现有 JWT 解析逻辑(例如在 JwtAuthenticationFilter 中)将角色与用户 ID 注入 Spring Security 的 Authentication,即可复用。

6.3 WebFlux 响应式兼容
  • WebFlux 仍使用 Jackson 作为 JSON 序列化框架,以上 BeanSerializerModifier / JsonSerializer 同样适用。
  • 通过配置 CodecCustomizer 或在 ObjectMapper Bean 中注册 DataMaskBeanSerializerModifier 即可。
@Configuration
public class JacksonMaskConfiguration {

    @Bean
    public ObjectMapper objectMapper(DataMaskerFactory factory,
                                     SecurityContextProvider securityContextProvider) {
        ObjectMapper mapper = new ObjectMapper();
        SimpleModule module = new SimpleModule();
        mapper.setSerializerFactory(
                mapper.getSerializerFactory()
                        .withSerializerModifier(new DataMaskBeanSerializerModifier(factory, securityContextProvider))
        );
        mapper.registerModule(module);
        return mapper;
    }
}

在 WebFlux 环境中,通过 WebFluxConfigurerCodecCustomizer 使用上述 ObjectMapper

@Bean
public CodecCustomizer codecCustomizer(ObjectMapper objectMapper) {
    return configurer -> configurer.defaultCodecs().jackson2JsonEncoder(new Jackson2JsonEncoder(objectMapper));
}

7. 全局配置与个性化配置

7.1 全局默认配置
  • application.yml 中配置默认脱敏策略(仅限非敏感字段参数,如开关、默认级别):
data-mask:
  enabled: true
  default-level: MEDIUM
  log-enabled: true
  • 通过 @ConfigurationProperties 绑定:
@ConfigurationProperties(prefix = "data-mask")
public class DataMaskProperties {
    private boolean enabled = true;
    private String defaultLevel = "MEDIUM";
    private boolean logEnabled = true;
    // getter/setter
}
7.2 个性化配置(按字段 / 角色)
  • 开发者在字段上通过 @DataMask 定制前缀、后缀、allowRoles 等参数。
  • 对于复杂场景(如特定租户/业务线规则),可以:
    • 使用 customRule 字段与自定义策略实现对应;
    • 或在 SecurityContextProvider 中注入租户信息,并在策略中根据上下文动态调整脱敏程度。

8. 与缓存(Redis + Caffeine)及分布式环境的协同

8.1 缓存层策略
  • 推荐做法:缓存中存储的是原始数据(或加密后数据),脱敏在返回给前端前由序列化层完成。

    • 优点:缓存命中后仍能根据不同角色动态决定脱敏程度。
    • 缺点:内存中和 Redis 中存在原始敏感数据,需要严格控制访问权限与加密策略。
  • 安全增强选项

    • 对特别敏感字段采用加密存储:业务实体中存储的是加密字符串,在业务代码中只有在必要场景下解密使用;脱敏时基于明文进行掩码后输出。
    • 为防止频繁解密影响性能,可使用 Caffeine 对“已解密数据 + 掩码结果”进行本地缓存,设置短期 TTL。
8.2 三级缓存协同
  • 本地 Caffeine 缓存:缓存“字段元数据 + 注解配置”解析结果,避免每次反射扫描注解。
  • Redis 分布式缓存:缓存热点用户信息、订单信息等原始数据。
  • 数据库 MySQL:持久化原始数据(可结合列级加密或应用层加密)。
8.3 分布式环境统一脱敏
  • 脱敏逻辑全部在应用层统一实现,通过注解 + 策略 + Jackson 扩展完成。
  • 多实例环境中,各实例加载同一套规则配置,保证输出一致。
  • 如需运行时动态调整规则,可通过:
    • 规则配置存储在数据库/配置中心,
    • 通过 Redis pub/sub 或配置中心推送通知各实例更新本地规则缓存。

9. 性能优化设计

  • 反射优化
    • 对类字段和 @DataMask 元信息的解析只在第一次访问时进行,结果缓存到 Caffeine Map 中。
  • 字符串操作优化
    • 使用基于索引切割的 StringBuilder 操作,避免正则开销。
    • 对身份证、银行卡等固定长度的字段,可使用预计算的边界。
  • 条件判断优化
    • 通过 allowRoles 快速判断是否需要脱敏,避免不必要的字符串构造。
  • 异步与响应式
    • WebFlux 环境中,脱敏在 Reactor 链尾部的序列化阶段执行,避免阻塞 I/O 线程。
  • 性能指标
    • 在 15,000+ QPS、典型实体字段数量 < 30 的场景下,单次脱敏开销控制在微秒级,整体 RT 增量 < 1ms。

10. 安全要求与实现

10.1 脱敏规则配置安全
  • 原则

    • 规则配置仅在服务端维护,前端和外部系统不可直接获取详细规则(如保留几位、掩码规则等)。
    • 若必须提供文档或对外说明,只给出“效果示例”,不公开具体算法细节。
  • 具体措施

    • 规则配置存放在受控的配置中心/数据库中,访问受权限控制;
    • 对关键自定义规则标识(如 customRule)与实现映射关系仅在代码中维护,不开放动态脚本配置;
    • 严禁在日志、异常信息中打印脱敏规则的详细实现细节。
10.2 敏感数据加密存储与传输
  • 存储加密

    • 对银行卡号、身份证号等字段可采用应用层加密(如 AES-256)后再写入 DB/Redis;
    • 密钥由专用密钥管理系统(KMS)或环境变量/配置中心管理,不写入代码仓库。
  • 传输安全

    • 所有外部接口统一使用 HTTPS,防止中间人攻击;
    • 内部 RPC 调用也推荐使用 TLS 或内网专线。
10.3 与 JWT / Spring Security 集成
  • 在 JWT 中仅存放必要的身份信息(userId、roleCodes),避免存储明文敏感数据;
  • JwtAuthenticationFilter 中完成 JWT 解析并注入 Authentication,供 SecurityContextProvider 使用;
  • 对需要访问原始敏感数据的接口,结合 Spring Security 的方法级权限控制:
@PreAuthorize("hasAuthority('USER_SENSITIVE_READ')")
@GetMapping("/user/detail/raw")
public UserInfoVO getRawUserInfo() {
    // 返回值中不使用 @DataMask 或根据 allowRoles 自动判定不脱敏
}

11. 可扩展性设计

  • 新增脱敏算法步骤
    1. 创建新的枚举值 MaskType.NEW_TYPE
    2. 实现 DataMasker 接口,例如 NewTypeDataMasker 并加上 @Component
    3. 在业务实体字段上标记 @DataMask(type = MaskType.NEW_TYPE, ...) 即可。
  • 自定义规则扩展
    • 通过 @Sensitive("BIZ_RULE") + 对应 DataMasker 实现(supports 中判断 MaskType.CUSTOMcustomRule 匹配)实现特定业务规则。
  • 多租户 / 多业务线
    • DataMaskContext 中增加 tenantId、bizLine 等参数,策略实现中根据这些参数动态调整掩码规则。

12. 监控与日志设计

12.1 日志记录
  • 日志内容
    • 不记录原始敏感值
    • 仅记录:脱敏字段名、脱敏类型、调用方、用户 ID、脱敏策略版本号等;
    • 关键操作如“关闭脱敏开关”、“变更规则配置”必须审计记录。
@Slf4j
public class MaskAuditLogger {

    public void logMask(String fieldName, MaskType type, DataMaskContext context) {
        log.debug("mask field={}, type={}, userId={}, roles={}",
                fieldName,
                type,
                context.getUserId(),
                context.getRoles());
    }
}
12.2 监控指标
  • 通过 Micrometer + Prometheus + Grafana 建立监控指标,例如:
    • 每秒脱敏调用次数:data_mask_requests_total
    • 平均脱敏耗时:data_mask_latency_ms
    • 按角色统计的脱敏比例:data_mask_role_ratio
    • 脱敏失败次数(异常):data_mask_errors_total

DataMaskingJsonSerializer 中埋点:

@Timed(value = "data_mask_serialize", histogram = true)
public void serialize(...) {
    // 原有逻辑
}

13. 代码结构建议

建议在 src/main/java/com/video/common/mask 下组织以下结构:

  • annotation
    • DataMask.java
    • Sensitive.java
  • core
    • MaskType.java
    • DataMaskContext.java
    • DataMasker.java
    • DataMaskerFactory.java
    • SecurityContextProvider.java
  • strategy
    • MobileDataMasker.java
    • IdCardDataMasker.java
    • EmailDataMasker.java
    • BankCardDataMasker.java
    • CustomRuleDataMasker.java
  • jackson
    • DataMaskBeanSerializerModifier.java
    • DataMaskingJsonSerializer.java
    • JacksonMaskConfiguration.java
  • monitor
    • MaskAuditLogger.java
  • config
    • DataMaskProperties.java

14. 测试方案设计

14.1 单元测试
  • 覆盖点

    • 各内置策略类(如 MobileDataMasker)对不同输入的脱敏结果:
      • 正常长度
      • 长度不足前缀+后缀
      • 空字符串 / null
    • DataMaskerFactory 的策略选择逻辑;
    • DataMaskingJsonSerializer 在不同 allowRolesroles 场景下是否执行脱敏;
    • SecurityContextProvider 能否从 Spring Security 上下文中正确构建 DataMaskContext
  • 示例测试代码

class MobileDataMaskerTest {

    private final MobileDataMasker masker = new MobileDataMasker();

    @Test
    void testMaskNormalMobile() {
        DataMask annotation = ...; // 使用动态代理或自定义注解实现构造
        DataMaskContext context = new DataMaskContext();
        String result = (String) masker.mask("13812348888", annotation, context);
        assertEquals("138****8888", result);
    }
}
14.2 集成测试(MVC & WebFlux)
  • Spring MVC 集成测试

    • 使用 @SpringBootTest + MockMvc
      • 构造不同角色(普通用户、管理员)登录场景(mock JWT 或 mock SecurityContext);
      • 调用返回包含 @DataMask 字段的接口,验证 JSON 输出是否符合预期脱敏规则。
  • WebFlux 集成测试

    • 使用 WebTestClient
      • 模拟 Reactor 环境下的请求;
      • 验证响应体中脱敏字段是否正确。
14.3 性能与压力测试
  • 使用 JMeter / Gatling / Locust 对关键接口进行压测:

    • 对比开启/关闭脱敏功能时的 QPS 与 RT;
    • 在 15,000+ QPS 下观察 CPU、GC、响应时间分布;
    • 确认脱敏不会成为瓶颈点。
  • 可针对 DataMaskingJsonSerializer 编写简单 micro-benchmark,校验单次脱敏耗时。


15. 总结

本方案基于 注解 + 策略模式 + Jackson 序列化扩展 实现了与现有 Spring Boot 3.x 技术栈兼容的通用数据脱敏能力:

  • 对业务开发者友好:仅需在实体字段上添加 @DataMask@Sensitive 即可;
  • 满足安全与合规需求:支持按角色/权限控制脱敏级别,支持敏感数据加密存储与传输,规则配置受控不可泄露;
  • 高性能与可扩展:缓存元数据、避免反射开销,支持 WebFlux 与 MVC,在高并发场景下性能影响可控,并可通过简单新增策略类扩展新的脱敏算法;
  • 运维友好:结合日志与监控体系,便于审计、回溯与性能分析。

16. 与现有项目的集成示例

16.1 目录与包结构对齐

在当前项目中,建议将本方案的实现代码落地到 com.video.common.mask 包下,与已有基础能力(如 JwtUtilUserContextResult 等)保持同一层级,方便统一维护:

  • common
    • JwtUtil:JWT 解析、生成工具,可为 SecurityContextProvider 提供用户信息。
    • UserContext / UserInfo:封装当前登录用户信息,可作为 DataMaskContext 的数据来源之一。
    • mask(新增):承载数据脱敏相关的注解、核心接口、策略实现和 Jackson 扩展。

这样可以尽量复用当前项目在安全、上下文、响应包装等方面的已有能力,减少重复轮子。

16.2 Controller 层使用示例

以一个典型的用户详情接口为例:

@RestController
@RequestMapping("/api/user")
public class UserController {

    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @GetMapping("/profile")
    public Result<UserProfileVO> profile() {
        Long userId = UserContext.getUserId();
        UserProfileVO profile = userService.getProfile(userId);
        // 返回时由 Jackson + DataMask 注解自动完成脱敏
        return Result.ok(profile);
    }
}

对应的 UserProfileVO 示例:

public class UserProfileVO {

    private Long id;

    @DataMask(type = MaskType.MOBILE, prefix = 3, suffix = 4)
    private String mobile;

    @DataMask(type = MaskType.EMAIL)
    private String email;

    @DataMask(type = MaskType.ID_CARD, prefix = 6, suffix = 4, allowRoles = {"ROLE_ADMIN"})
    private String idCard;

    // 其他字段略
}
  • 普通用户访问 /api/user/profile 时,将看到脱敏后的手机、邮箱、身份证号;
  • 拥有 ROLE_ADMIN 的管理员访问时,身份证号字段会根据 allowRoles 判定为无需脱敏,直接展示明文。
16.3 与 MyBatis Plus 的衔接

Service 层通过 MyBatis Plus 读取实体数据后,再转换为 VO 即可:

@Service
public class UserService {

    private final UserMapper userMapper;

    public UserService(UserMapper userMapper) {
        this.userMapper = userMapper;
    }

    public UserProfileVO getProfile(Long userId) {
        User entity = userMapper.selectById(userId);
        if (entity == null) {
            return null;
        }
        UserProfileVO vo = new UserProfileVO();
        // 常规 bean 拷贝
        BeanUtils.copyProperties(entity, vo);
        return vo;
    }
}
  • MyBatis Plus 实体 User 中可以不加 @DataMask,只在 VO 层标注;
  • 这样可以确保数据库层和缓存层仍然持有完整数据,脱敏专注在返回给前端的视图对象上,风险边界更清晰。
16.4 与 JWT / JwtAuthenticationFilter 的集成

当前项目已经存在基于 JWT 的认证过滤器(如 JwtAuthenticationFilter),在该过滤器中完成 JWT 解析并将用户信息放入 SecurityContextUserContext,即可被 SpringSecurityContextProvider 复用:

public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {
        String token = resolveToken(request);
        if (StringUtils.hasText(token)) {
            CustomUserDetails userDetails = parseToken(token);
            UsernamePasswordAuthenticationToken authentication =
                    new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
            SecurityContextHolder.getContext().setAuthentication(authentication);

            // 同时写入自定义 UserContext 供业务侧使用
            UserContext.setUserInfo(convertToUserInfo(userDetails));
        }
        filterChain.doFilter(request, response);
    }
}

数据脱敏模块无需直接感知 JWT 细节,只依赖标准的 SecurityContextHolder 和/或 UserContext 即可。


17. 落地实施步骤与 Checklist

为降低改造风险,推荐采用分阶段、渐进式引入:

17.1 阶段一:基础能力搭建
  • 步骤

    • com.video.common.mask 下创建注解、核心接口、策略实现和 Jackson 配置;
    • common 模块中新增 SecurityContextProvider 的实现,封装从 SecurityContextHolderUserContext 读取数据的逻辑;
    • 在全局 ObjectMapper / WebFlux CodecCustomizer 中注册 DataMaskBeanSerializerModifier
    • application.yml 中加入 data-mask 开关配置,并默认开启。
  • 验证点

    • 编写一个简单的 Demo VO 和 Controller,手动调用并验证 JSON 输出中字段是否按注解规则正确脱敏;
    • 检查非敏感接口是否未受到影响(未标注注解的字段不应被修改)。
17.2 阶段二:核心业务场景覆盖
  • 优先覆盖

    • 用户信息、账号资产、订单、支付相关接口;
    • 管理后台中对用户数据的查询接口,根据角色配置 allowRoles
  • 实施建议

    • 以 VO 为单位梳理,优先对对外暴露的响应对象添加 @DataMask
    • 与安全/风控同学确认各角色的脱敏级别和策略;
    • 通过灰度发布,在部分实例/部分租户先开启脱敏,观察日志与监控指标。
17.3 阶段三:全量推广与持续优化
  • 推广

    • 将脱敏规则扩展到更多业务模块(私信、礼物、活动等),统一规范;
    • 对于动态变化频繁的规则,考虑引入配置中心,并增加规则版本号字段用于审计。
  • 优化

    • 基于监控指标优化热点接口的脱敏策略实现,例如:
      • 调整字符串处理方式;
      • 微调 Caffeine 缓存的 TTL 与容量;
      • 观察 GC 和 CPU 使用情况,避免过度分配临时对象。
17.4 回滚与降级方案
  • 支持通过 data-mask.enabled=false 全局开关快速关闭脱敏逻辑;
  • 对关键接口,可增加接口级参数(仅内部使用)临时关闭脱敏,用于紧急排查问题;
  • 重要:即便关闭脱敏,也不影响敏感数据的加密存储与传输机制,两者应解耦设计。

18. 常见问题与最佳实践

18.1 常见问题 FAQ
  • 是否需要对所有敏感字段都加 @DataMask

    • 实际上建议至少对所有对外输出的敏感字段进行标注;
    • 对于仅用于内部计算、不直接返回前端的字段,可以不加注解,但仍建议在存储层进行加密处理。
  • 脱敏在 Service 层做还是在返回层做?

    • 本方案推荐在 JSON 序列化阶段统一完成,避免污染业务逻辑;
    • 如确有特殊需要(例如导出文件、第三方对接),可以在业务层额外调用公共的脱敏工具方法。
  • 缓存中是否存放明文?

    • 推荐对特别敏感的数据进行应用层加密后再写入缓存;
    • 对一般敏感数据,可以在受控环境下存明文,但要确保访问缓存的代码路径和账号可控,并配合脱敏模块严格限制对外输出。
  • 如何避免重复脱敏?

    • 通过 allowRoles + DataMaskContext 的角色判断机制,确保只在必要时进行;
    • 对于已经是脱敏后的字符串,避免再次执行脱敏可以通过增加一个简单的“已脱敏标记位”或正则判断来实现(可按业务情况取舍)。
18.2 最佳实践清单
  • 优先 VO 层脱敏:保持 Entity/DO 只承载存储语义,将展示语义集中在 VO。
  • 角色模型前置设计:在设计脱敏策略前,与安全和产品一起确定“哪些角色可以看到明文、哪些只能看到部分信息、哪些完全看不到”。
  • 配置与代码分层:将与业务强相关的规则放在代码中(如字段级注解),将可调的策略参数放在配置中心(如开关、默认级别),避免全部规则都硬编码或全部放配置。
  • 日志脱敏一致性:日志打印时应复用相同的脱敏策略或至少保证不泄露原始敏感数据,避免“接口返回是脱敏的、日志里是明文”的情况。
  • 定期回顾与审计:定期梳理系统中新增的字段与接口,确保新的敏感字段也纳入脱敏体系,并通过审计日志核对规则变更。
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐