SpringBoot 数据脱敏实战: 构建企业级敏感信息保护体系

发布于:2025-09-09 ⋅ 阅读:(24) ⋅ 点赞:(0)
  • 本文总字数:约 8500 字
  • 预计阅读时间:35 分钟

在当今数字化时代,数据安全已成为企业发展的生命线。用户手机号、身份证号、银行卡信息等敏感数据一旦泄露,不仅会给用户带来巨大损失,更会让企业面临信任危机和法律风险。据《2023 年数据安全漏洞报告》显示,因敏感信息泄露导致的企业平均损失已达 420 万美元,较去年增长 15%。

作为 Java 开发者,我们如何在 SpringBoot 应用中构建可靠的数据脱敏机制?本文将带你深入探索数据脱敏的底层原理,从基础概念到实战落地,全方位解析企业级数据脱敏方案。无论你是刚入行的新手还是资深开发者,都能从本文获得可直接应用于生产环境的解决方案。

一、数据脱敏核心概念与应用场景

1.1 什么是数据脱敏

数据脱敏(Data Masking)是指在不影响业务正常运行的前提下,对敏感信息进行变形处理,实现敏感隐私数据的可靠保护。脱敏后的信息不应能还原出原始数据,同时要保持数据的格式和业务特性不变。

例如,将手机号 "13812345678" 脱敏为 "138****5678",既保护了用户隐私,又保留了手机号的长度特征。

1.2 数据脱敏的必要性

数据脱敏的重要性主要体现在以下三个方面:

  1. 合规性要求:《网络安全法》《数据安全法》《个人信息保护法》等法律法规明确要求企业必须采取技术措施保护用户敏感信息。

  2. 数据安全防护:在开发、测试、数据分析等场景中,避免敏感数据直接暴露给非授权人员。

  3. 风险控制:即使发生数据泄露,脱敏后的数据也不会造成实质性危害。

1.3 常见应用场景

数据脱敏在企业系统中应用广泛,主要场景包括:

  • 接口返回数据:API 接口返回用户信息时对敏感字段进行脱敏
  • 日志记录:系统日志中避免记录完整敏感信息
  • 数据库存储:部分场景下对敏感字段进行持久化脱敏
  • 数据导出:报表导出、数据迁移时的脱敏处理
  • 开发测试环境:生产数据同步到测试环境时的脱敏转换

1.4 数据脱敏的基本原则

有效的数据脱敏方案应遵循以下原则:

  • 不可逆性:脱敏后的信息无法还原为原始数据(除非有特殊需求的可逆脱敏)
  • 一致性:相同的原始数据应脱敏为相同的结果
  • 业务保留性:脱敏后的数据应保持原有格式和业务特征
  • 可定制性:能够根据不同业务场景灵活配置脱敏规则
  • 性能影响小:脱敏处理不应显著影响系统性能

二、数据脱敏技术分类与实现方案

2.1 按脱敏时机分类

根据脱敏操作发生的时机,可分为以下几类:

  • 静态数据脱敏(SDM):对存储的数据进行永久性脱敏,通常用于将生产数据复制到开发、测试环境时使用。脱敏后的数据集可以安全地用于非生产环境,不会泄露敏感信息。

  • 动态数据脱敏(DDM):在数据被访问时实时进行脱敏处理,原始数据在存储中保持不变。这种方式可以根据用户角色、访问权限等动态调整脱敏策略,同一数据对不同权限的用户展示不同的脱敏结果。

2.2 按脱敏算法分类

常见的脱敏算法包括:

  1. 替换脱敏:用特定字符(如 *)替换敏感部分,如手机号中间 4 位替换为 *
  2. 截断脱敏:只保留部分字符,如身份证号只显示前 6 位和后 4 位
  3. 加密脱敏:使用加密算法对敏感数据进行加密,如 AES 加密
  4. 混淆脱敏:打乱数据顺序或替换为虚假但格式一致的数据
  5. 掩码脱敏:按照特定规则隐藏部分信息,如信用卡号每 4 位一组显示

2.3 主流实现方案对比

目前企业中常用的数据脱敏方案有以下几种:

方案 实现方式 优点 缺点 适用场景
数据库层脱敏 数据库自带功能或插件 性能好,对应用透明 灵活性差,规则难维护 简单场景,全系统统一规则
ORM 框架扩展 基于 MyBatis 等框架拦截器 与业务代码解耦,灵活 需熟悉框架原理 中复杂场景,基于 ORM 的应用
注解式脱敏 基于注解和 AOP 实现 侵入性低,配置灵活 需处理各种序列化场景 复杂场景,多维度脱敏规则
网关层脱敏 API 网关统一处理 集中管理,无需修改应用 无法处理内部服务调用 对外 API 接口,简单规则

在 SpringBoot 应用中,注解式脱敏结合 ORM 框架扩展是最常用的方案,既能保证灵活性,又能实现细粒度的脱敏控制。

三、SpringBoot 数据脱敏环境搭建

3.1 技术选型与版本说明

本文将使用以下技术栈实现数据脱敏方案:

  • JDK:17.0.9
  • SpringBoot:3.2.0
  • MyBatis-Plus:3.5.5
  • Lombok:1.18.30
  • Commons-lang3:3.14.0
  • SpringDoc-OpenAPI(Swagger3):2.2.0
  • MySQL:8.0.35

3.2 项目初始化与依赖配置

首先创建一个 SpringBoot 项目,在 pom.xml 中添加以下依赖:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.0</version>
        <relativePath/>
    </parent>
    
    <groupId>com.jam</groupId>
    <artifactId>springboot-data-masking</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>springboot-data-masking</name>
    <description>SpringBoot数据脱敏实战项目</description>
    
    <properties>
        <java.version>17</java.version>
        <mybatis-plus.version>3.5.5</mybatis-plus.version>
        <lombok.version>1.18.30</lombok.version>
        <commons-lang3.version>3.14.0</commons-lang3.version>
        <springdoc.version>2.2.0</springdoc.version>
    </properties>
    
    <dependencies>
        <!-- SpringBoot核心依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        
        <!-- 数据库依赖 -->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>
        
        <!-- MyBatis-Plus -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>${mybatis-plus.version}</version>
        </dependency>
        
        <!-- Lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${lombok.version}</version>
            <optional>true</optional>
        </dependency>
        
        <!-- 工具类 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>${commons-lang3.version}</version>
        </dependency>
        
        <!-- Swagger3 -->
        <dependency>
            <groupId>org.springdoc</groupId>
            <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
            <version>${springdoc.version}</version>
        </dependency>
        
        <!-- 测试依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

3.3 数据库配置

在 application.yml 中配置数据库连接信息:

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/data_masking_demo?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver

# MyBatis-Plus配置
mybatis-plus:
  mapper-locations: classpath:mapper/**/*.xml
  type-aliases-package: com.jam.datamasking.entity
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    map-underscore-to-camel-case: true

# 日志配置
logging:
  level:
    com.jam.datamasking: debug

# Swagger3配置
springdoc:
  api-docs:
    path: /api-docs
  swagger-ui:
    path: /swagger-ui.html
    operationsSorter: method
  packages-to-scan: com.jam.datamasking.controller

3.4 创建数据库表

创建用户表用于演示数据脱敏功能:

CREATE DATABASE IF NOT EXISTS data_masking_demo DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

USE data_masking_demo;

CREATE TABLE `user` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `username` varchar(50) NOT NULL COMMENT '用户名',
  `real_name` varchar(50) NOT NULL COMMENT '真实姓名',
  `id_card` varchar(20) NOT NULL COMMENT '身份证号',
  `phone` varchar(20) NOT NULL COMMENT '手机号',
  `email` varchar(100) NOT NULL COMMENT '邮箱',
  `bank_card` varchar(30) NOT NULL COMMENT '银行卡号',
  `address` varchar(200) DEFAULT NULL COMMENT '地址',
  `password` varchar(100) NOT NULL COMMENT '密码(加密存储)',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表';

-- 插入测试数据
INSERT INTO `user` (`username`, `real_name`, `id_card`, `phone`, `email`, `bank_card`, `address`, `password`)
VALUES
('zhangsan', '张三', '110101199001011234', '13812345678', 'zhangsan@example.com', '6222021234567890123', '北京市朝阳区', 'e10adc3949ba59abbe56e057f20f883e'),
('lisi', '李四', '310101199203045678', '13987654321', 'lisi@example.com', '6228481234567890123', '上海市浦东新区', 'e10adc3949ba59abbe56e057f20f883e');

四、注解式数据脱敏核心实现

4.1 脱敏策略设计

首先定义常用的脱敏策略枚举类,包含不同敏感信息的脱敏规则:

package com.jam.datamasking.enums;

import lombok.AllArgsConstructor;
import lombok.Getter;

/**
 * 脱敏策略枚举类
 * 定义不同敏感信息的脱敏规则
 *
 * @author 果酱
 */
@Getter
@AllArgsConstructor
public enum MaskStrategy {

    /**
     * 手机号脱敏:保留前3位和后4位,中间4位用*代替
     * 例如:138****5678
     */
    PHONE(3, 4, "****"),

    /**
     * 身份证号脱敏:保留前6位和后4位,中间用*代替
     * 例如:110101********1234
     */
    ID_CARD(6, 4, "********"),

    /**
     * 邮箱脱敏:保留前3位和域名,中间用*代替
     * 例如:zha***@example.com
     */
    EMAIL(3, 0, "***"),

    /**
     * 真实姓名脱敏:中文姓名保留姓氏,其他用*代替
     * 例如:张*、李**
     */
    REAL_NAME(1, 0, "*"),

    /**
     * 银行卡号脱敏:保留前6位和后4位,中间用*代替
     * 例如:622202********1234
     */
    BANK_CARD(6, 4, "********"),

    /**
     * 地址脱敏:保留前6位,后面用*代替
     * 例如:北京市朝***
     */
    ADDRESS(6, 0, "***");

    /**
     * 保留的前缀长度
     */
    private final int prefixLength;

    /**
     * 保留的后缀长度
     */
    private final int suffixLength;

    /**
     * 替换字符
     */
    private final String replaceStr;
}

4.2 脱敏注解定义

创建自定义脱敏注解,用于标记需要脱敏的字段:

package com.jam.datamasking.annotation;

import com.jam.datamasking.enums.MaskStrategy;
import java.lang.annotation.*;

/**
 * 数据脱敏注解
 * 用于标记需要进行脱敏处理的字段
 *
 * @author 果酱
 */
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataMask {

    /**
     * 脱敏策略
     *
     * @return 脱敏策略枚举
     */
    MaskStrategy strategy();

    /**
     * 是否在日志打印时脱敏
     *
     * @return true-脱敏,false-不脱敏
     */
    boolean maskInLog() default true;
}

4.3 脱敏工具类实现

实现核心的脱敏工具类,提供各种脱敏策略的具体实现:

package com.jam.datamasking.util;

import com.jam.datamasking.enums.MaskStrategy;
import org.apache.commons.lang3.StringUtils;

/**
 * 数据脱敏工具类
 * 实现各种敏感信息的脱敏逻辑
 *
 * @author 果酱
 */
public class MaskUtils {

    /**
     * 根据策略对字符串进行脱敏
     *
     * @param str      原始字符串
     * @param strategy 脱敏策略
     * @return 脱敏后的字符串
     */
    public static String mask(String str, MaskStrategy strategy) {
        // 字符串为空直接返回
        if (!StringUtils.hasText(str)) {
            return str;
        }

        // 根据不同策略进行脱敏
        return switch (strategy) {
            case PHONE -> maskPhone(str);
            case ID_CARD -> maskIdCard(str);
            case EMAIL -> maskEmail(str);
            case REAL_NAME -> maskRealName(str);
            case BANK_CARD -> maskBankCard(str);
            case ADDRESS -> maskAddress(str);
        };
    }

    /**
     * 手机号脱敏
     * 保留前3位和后4位,中间4位用*代替
     *
     * @param phone 手机号
     * @return 脱敏后的手机号
     */
    public static String maskPhone(String phone) {
        if (!StringUtils.hasText(phone) || phone.length() != 11) {
            return phone;
        }
        return phone.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
    }

    /**
     * 身份证号脱敏
     * 保留前6位和后4位,中间用*代替
     *
     * @param idCard 身份证号
     * @return 脱敏后的身份证号
     */
    public static String maskIdCard(String idCard) {
        if (!StringUtils.hasText(idCard) || (idCard.length() != 15 && idCard.length() != 18)) {
            return idCard;
        }
        return idCard.replaceAll("(\\d{6})\\d+(\\d{4})", "$1********$2");
    }

    /**
     * 邮箱脱敏
     * 保留前3位和域名,中间用*代替
     *
     * @param email 邮箱地址
     * @return 脱敏后的邮箱地址
     */
    public static String maskEmail(String email) {
        if (!StringUtils.hasText(email) || !email.contains("@")) {
            return email;
        }
        
        String[] parts = email.split("@");
        String prefix = parts[0];
        String domain = parts[1];
        
        // 前缀长度小于等于3位则全部显示,否则显示前3位
        int showLength = Math.min(prefix.length(), 3);
        return prefix.substring(0, showLength) + "***@" + domain;
    }

    /**
     * 真实姓名脱敏
     * 中文姓名保留姓氏,其他用*代替
     *
     * @param realName 真实姓名
     * @return 脱敏后的姓名
     */
    public static String maskRealName(String realName) {
        if (!StringUtils.hasText(realName)) {
            return realName;
        }
        
        // 长度为1,不脱敏
        if (realName.length() == 1) {
            return realName;
        }
        
        // 长度为2,显示第一个字,第二个字用*代替
        if (realName.length() == 2) {
            return realName.charAt(0) + "*";
        }
        
        // 长度大于2,显示第一个字,后面的用*代替
        return realName.charAt(0) + StringUtils.repeat("*", realName.length() - 1);
    }

    /**
     * 银行卡号脱敏
     * 保留前6位和后4位,中间用*代替
     *
     * @param bankCard 银行卡号
     * @return 脱敏后的银行卡号
     */
    public static String maskBankCard(String bankCard) {
        if (!StringUtils.hasText(bankCard) || bankCard.length() < 10) {
            return bankCard;
        }
        return bankCard.replaceAll("(\\d{6})\\d+(\\d{4})", "$1********$2");
    }

    /**
     * 地址脱敏
     * 保留前6位,后面用*代替
     *
     * @param address 地址
     * @return 脱敏后的地址
     */
    public static String maskAddress(String address) {
        if (!StringUtils.hasText(address)) {
            return address;
        }
        
        int showLength = Math.min(address.length(), 6);
        return address.substring(0, showLength) + "***";
    }
}

4.4 Jackson 序列化脱敏实现

通过自定义 Jackson 序列化器,实现 API 接口返回数据的自动脱敏:

package com.jam.datamasking.serializer;

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 com.jam.datamasking.annotation.DataMask;
import com.jam.datamasking.enums.MaskStrategy;
import com.jam.datamasking.util.MaskUtils;
import java.io.IOException;
import java.util.Objects;

/**
 * 数据脱敏序列化器
 * 用于在JSON序列化时对标记了@DataMask注解的字段进行脱敏处理
 *
 * @author 果酱
 */
public class DataMaskSerializer extends JsonSerializer<String> implements ContextualSerializer {

    /**
     * 脱敏策略
     */
    private MaskStrategy strategy;

    /**
     * 默认构造函数
     */
    public DataMaskSerializer() {
    }

    /**
     * 带参数构造函数
     *
     * @param strategy 脱敏策略
     */
    public DataMaskSerializer(MaskStrategy strategy) {
        this.strategy = strategy;
    }

    @Override
    public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
        // 应用脱敏策略并写入JSON
        gen.writeString(MaskUtils.mask(value, strategy));
    }

    @Override
    public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException {
        // 获取字段上的@DataMask注解
        DataMask annotation = property.getAnnotation(DataMask.class);
        
        // 如果注解存在且字段类型是String,则使用自定义脱敏序列化器
        if (Objects.nonNull(annotation) && Objects.equals(property.getType().getRawClass(), String.class)) {
            return new DataMaskSerializer(annotation.strategy());
        }
        
        // 否则使用默认序列化器
        return prov.findValueSerializer(property.getType(), property);
    }
}

注册自定义序列化器,使 Jackson 能够识别并应用:

package com.jam.datamasking.config;

import com.fasterxml.jackson.databind.module.SimpleModule;
import com.jam.datamasking.serializer.DataMaskSerializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;

/**
 * Jackson配置类
 * 注册自定义的脱敏序列化器
 *
 * @author 果酱
 */
@Configuration
public class JacksonConfig {

    /**
     * 配置Jackson,注册自定义序列化器
     *
     * @param builder Jackson对象映射构建器
     * @return 配置后的ObjectMapper
     */
    @Bean
    public com.fasterxml.jackson.databind.ObjectMapper objectMapper(Jackson2ObjectMapperBuilder builder) {
        com.fasterxml.jackson.databind.ObjectMapper objectMapper = builder.createXmlMapper(false).build();
        
        // 创建自定义模块并注册脱敏序列化器
        SimpleModule module = new SimpleModule();
        module.addSerializer(String.class, new DataMaskSerializer());
        
        objectMapper.registerModule(module);
        return objectMapper;
    }
}

4.5 日志脱敏 AOP 实现

通过 AOP 实现日志打印时的脱敏处理,防止敏感信息泄露到日志中:

package com.jam.datamasking.aspect;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.jam.datamasking.annotation.DataMask;
import com.jam.datamasking.util.MaskUtils;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.util.ReflectionUtils;

/**
 * 日志脱敏切面
 * 用于在日志打印时对标记了@DataMask注解的字段进行脱敏处理
 *
 * @author 果酱
 */
@Slf4j
@Aspect
@Component
public class LogMaskAspect {

    private final ObjectMapper objectMapper;

    /**
     * 构造函数注入ObjectMapper
     *
     * @param objectMapper Jackson对象映射器
     */
    public LogMaskAspect(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }

    /**
     * 定义切入点:所有标有@Slf4j注解的类的方法
     */
    @Pointcut("@within(lombok.extern.slf4j.Slf4j)")
    public void logPointcut() {
    }

    /**
     * 环绕通知:对方法参数进行脱敏后再打印日志
     *
     * @param joinPoint 连接点
     * @return 方法执行结果
     * @throws Throwable 异常
     */
    @Around("logPointcut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        // 获取方法参数并进行脱敏
        Object[] args = joinPoint.getArgs();
        if (Objects.nonNull(args)) {
            for (int i = 0; i < args.length; i++) {
                args[i] = maskObject(args[i]);
            }
        }

        // 记录方法调用日志
        log.debug("调用方法: {}.{},参数: {}", 
                joinPoint.getTarget().getClass().getName(),
                joinPoint.getSignature().getName(),
                toJsonString(args));

        // 执行目标方法
        Object result = joinPoint.proceed();

        // 记录方法返回结果日志
        log.debug("方法: {}.{} 返回结果: {}",
                joinPoint.getTarget().getClass().getName(),
                joinPoint.getSignature().getName(),
                toJsonString(maskObject(result)));

        return result;
    }

    /**
     * 对对象进行脱敏处理
     *
     * @param obj 需要脱敏的对象
     * @return 脱敏后的对象
     */
    private Object maskObject(Object obj) {
        if (Objects.isNull(obj)) {
            return null;
        }

        // 如果是基本类型或字符串,直接返回
        if (obj.getClass().isPrimitive() || obj instanceof String || 
            obj instanceof Number || obj instanceof Boolean) {
            return obj;
        }

        // 如果是集合类型,递归处理集合中的元素
        if (obj instanceof List<?>) {
            List<Object> list = new ArrayList<>();
            for (Object item : (List<?>) obj) {
                list.add(maskObject(item));
            }
            return list;
        }

        // 对对象的字段进行脱敏处理
        try {
            // 创建对象的副本,避免修改原对象
            Object copy = obj.getClass().getDeclaredConstructor().newInstance();
            Field[] fields = obj.getClass().getDeclaredFields();
            
            for (Field field : fields) {
                // 设置字段可访问
                ReflectionUtils.makeAccessible(field);
                
                // 获取字段值
                Object fieldValue = ReflectionUtils.getField(field, obj);
                
                // 如果字段标记了@DataMask注解且需要在日志中脱敏,则进行脱敏处理
                DataMask dataMask = field.getAnnotation(DataMask.class);
                if (Objects.nonNull(dataMask) && dataMask.maskInLog() && 
                    fieldValue instanceof String stringValue) {
                    // 应用脱敏策略
                    String maskedValue = MaskUtils.mask(stringValue, dataMask.strategy());
                    ReflectionUtils.setField(field, copy, maskedValue);
                } else {
                    // 否则直接复制字段值
                    ReflectionUtils.setField(field, copy, fieldValue);
                }
            }
            
            return copy;
        } catch (Exception e) {
            log.warn("对象脱敏处理失败", e);
            // 脱敏失败时返回原对象
            return obj;
        }
    }

    /**
     * 将对象转换为JSON字符串
     *
     * @param obj 要转换的对象
     * @return JSON字符串
     */
    private String toJsonString(Object obj) {
        try {
            return objectMapper.writeValueAsString(obj);
        } catch (JsonProcessingException e) {
            log.warn("对象转JSON失败", e);
            return obj.toString();
        }
    }
}

4.6 MyBatis 查询结果脱敏实现

通过 MyBatis 的 TypeHandler 实现数据库查询结果的脱敏处理:

package com.jam.datamasking.handler;

import com.jam.datamasking.annotation.DataMask;
import com.jam.datamasking.enums.MaskStrategy;
import com.jam.datamasking.util.MaskUtils;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Objects;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;

/**
 * MyBatis数据脱敏类型处理器
 * 用于在查询数据库时对敏感字段进行脱敏处理
 *
 * @author 果酱
 */
@MappedJdbcTypes(JdbcType.VARCHAR)
@MappedTypes(String.class)
public class MaskTypeHandler extends BaseTypeHandler<String> {

    /**
     * 脱敏策略
     */
    private MaskStrategy strategy;

    /**
     * 设置脱敏策略
     *
     * @param strategy 脱敏策略
     */
    public void setStrategy(MaskStrategy strategy) {
        this.strategy = strategy;
    }

    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
        // 插入数据时不脱敏,直接存储原始值
        ps.setString(i, parameter);
    }

    @Override
    public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
        // 从结果集中获取值并进行脱敏
        return maskValue(rs.getString(columnName));
    }

    @Override
    public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        // 从结果集中获取值并进行脱敏
        return maskValue(rs.getString(columnIndex));
    }

    @Override
    public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        // 从存储过程中获取值并进行脱敏
        return maskValue(cs.getString(columnIndex));
    }

    /**
     * 对值进行脱敏处理
     *
     * @param value 原始值
     * @return 脱敏后的值
     */
    private String maskValue(String value) {
        // 如果策略不为空且值不为空,则进行脱敏
        if (Objects.nonNull(strategy)) {
            return MaskUtils.mask(value, strategy);
        }
        return value;
    }
}

为了让 TypeHandler 能够根据字段上的注解动态应用不同的脱敏策略,我们需要自定义一个 MyBatis 插件:

package com.jam.datamasking.plugin;

import com.jam.datamasking.annotation.DataMask;
import com.jam.datamasking.handler.MaskTypeHandler;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Properties;
import org.apache.ibatis.executor.resultset.ResultSetHandler;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.springframework.stereotype.Component;
import org.springframework.util.ReflectionUtils;

/**
 * MyBatis数据脱敏插件
 * 用于在查询结果映射时应用不同的脱敏策略
 *
 * @author 果酱
 */
@Component
@Intercepts({@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {java.sql.Statement.class})})
public class MaskPlugin implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 执行原始查询获取结果
        Object result = invocation.proceed();
        
        // 如果结果为空,直接返回
        if (Objects.isNull(result)) {
            return null;
        }
        
        // 如果结果是集合,处理集合中的每个元素
        if (result instanceof List<?>) {
            List<Object> list = new ArrayList<>();
            for (Object item : (List<?>) result) {
                list.add(maskItem(item));
            }
            return list;
        } else {
            // 处理单个对象
            return maskItem(result);
        }
    }

    /**
     * 对单个对象进行脱敏处理
     *
     * @param item 需要脱敏的对象
     * @return 脱敏后的对象
     */
    private Object maskItem(Object item) {
        if (Objects.isNull(item)) {
            return null;
        }
        
        // 获取对象的所有字段
        Field[] fields = item.getClass().getDeclaredFields();
        
        for (Field field : fields) {
            // 检查字段是否标记了@DataMask注解
            DataMask dataMask = field.getAnnotation(DataMask.class);
            if (Objects.nonNull(dataMask) && field.getType() == String.class) {
                // 对标记了注解的字段进行脱敏处理
                ReflectionUtils.makeAccessible(field);
                String originalValue = (String) ReflectionUtils.getField(field, item);
                String maskedValue = MaskUtils.mask(originalValue, dataMask.strategy());
                ReflectionUtils.setField(field, item, maskedValue);
            }
        }
        
        return item;
    }

    @Override
    public Object plugin(Object target) {
        // 包装目标对象,应用插件
        if (target instanceof ResultSetHandler) {
            return Plugin.wrap(target, this);
        }
        return target;
    }

    @Override
    public void setProperties(Properties properties) {
        // 可以通过properties配置插件参数
    }
}

五、实战应用:用户信息脱敏示例

5.1 实体类定义

创建用户实体类,并在需要脱敏的字段上添加 @DataMask 注解:

package com.jam.datamasking.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.jam.datamasking.annotation.DataMask;
import com.jam.datamasking.enums.MaskStrategy;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.Serializable;
import java.time.LocalDateTime;
import lombok.Data;

/**
 * 用户实体类
 *
 * @author 果酱
 */
@Data
@TableName("user")
@Schema(description = "用户信息实体")
public class User implements Serializable {

    private static final long serialVersionUID = 1L;

    @TableId(type = IdType.AUTO)
    @Schema(description = "主键ID")
    private Long id;

    @Schema(description = "用户名")
    private String username;

    @DataMask(strategy = MaskStrategy.REAL_NAME)
    @Schema(description = "真实姓名")
    private String realName;

    @DataMask(strategy = MaskStrategy.ID_CARD)
    @Schema(description = "身份证号")
    private String idCard;

    @DataMask(strategy = MaskStrategy.PHONE)
    @Schema(description = "手机号")
    private String phone;

    @DataMask(strategy = MaskStrategy.EMAIL)
    @Schema(description = "邮箱")
    private String email;

    @DataMask(strategy = MaskStrategy.BANK_CARD)
    @Schema(description = "银行卡号")
    private String bankCard;

    @DataMask(strategy = MaskStrategy.ADDRESS)
    @Schema(description = "地址")
    private String address;

    @JsonIgnore
    @Schema(description = "密码(加密存储)", hidden = true)
    private String password;

    @Schema(description = "创建时间")
    private LocalDateTime createTime;

    @Schema(description = "更新时间")
    private LocalDateTime updateTime;
}

5.2 Mapper 接口定义

使用 MyBatis-Plus 的 BaseMapper 简化数据库操作:

package com.jam.datamasking.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jam.datamasking.entity.User;
import org.apache.ibatis.annotations.Mapper;

/**
 * 用户Mapper接口
 *
 * @author 果酱
 */
@Mapper
public interface UserMapper extends BaseMapper<User> {
}

5.3 Service 层实现

创建用户服务接口和实现类:

package com.jam.datamasking.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.jam.datamasking.entity.User;
import java.util.List;

/**
 * 用户服务接口
 *
 * @author 果酱
 */
public interface UserService extends IService<User> {

    /**
     * 获取所有用户
     *
     * @return 用户列表
     */
    List<User> getAllUsers();

    /**
     * 根据ID获取用户
     *
     * @param id 用户ID
     * @return 用户信息
     */
    User getUserById(Long id);

    /**
     * 创建用户
     *
     * @param user 用户信息
     * @return 创建成功的用户
     */
    User createUser(User user);
}
package com.jam.datamasking.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.jam.datamasking.entity.User;
import com.jam.datamasking.mapper.UserMapper;
import com.jam.datamasking.service.UserService;
import java.util.List;
import java.util.Objects;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;

/**
 * 用户服务实现类
 *
 * @author 果酱
 */
@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {

    @Override
    public List<User> getAllUsers() {
        log.info("查询所有用户信息");
        return baseMapper.selectList(null);
    }

    @Override
    public User getUserById(Long id) {
        Objects.requireNonNull(id, "用户ID不能为空");
        log.info("根据ID查询用户信息, ID: {}", id);
        return baseMapper.selectById(id);
    }

    @Override
    public User createUser(User user) {
        Objects.requireNonNull(user, "用户信息不能为空");
        Objects.requireNonNull(StringUtils.hasText(user.getUsername()), "用户名不能为空");
        log.info("创建新用户: {}", user);
        
        baseMapper.insert(user);
        return user;
    }
}

5.4 Controller 层实现

创建用户控制器,提供 API 接口:

package com.jam.datamasking.controller;

import com.jam.datamasking.entity.User;
import com.jam.datamasking.service.UserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.List;
import java.util.Objects;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 用户控制器
 * 提供用户相关的API接口
 *
 * @author 果酱
 */
@Slf4j
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
@Tag(name = "用户管理", description = "用户信息相关接口")
public class UserController {

    private final UserService userService;

    /**
     * 获取所有用户
     *
     * @return 用户列表
     */
    @GetMapping
    @Operation(summary = "获取所有用户", description = "查询系统中所有用户的信息")
    @ApiResponses({
        @ApiResponse(responseCode = "200", description = "查询成功",
                content = @Content(schema = @Schema(implementation = User.class)))
    })
    public ResponseEntity<List<User>> getAllUsers() {
        log.info("接收获取所有用户的请求");
        List<User> users = userService.getAllUsers();
        return ResponseEntity.ok(users);
    }

    /**
     * 根据ID获取用户
     *
     * @param id 用户ID
     * @return 用户信息
     */
    @GetMapping("/{id}")
    @Operation(summary = "根据ID获取用户", description = "根据用户ID查询用户详细信息")
    @ApiResponses({
        @ApiResponse(responseCode = "200", description = "查询成功",
                content = @Content(schema = @Schema(implementation = User.class))),
        @ApiResponse(responseCode = "404", description = "用户不存在")
    })
    public ResponseEntity<User> getUserById(
            @Parameter(description = "用户ID", required = true)
            @PathVariable Long id) {
        log.info("接收根据ID获取用户的请求, ID: {}", id);
        User user = userService.getUserById(id);
        
        if (Objects.isNull(user)) {
            return ResponseEntity.notFound().build();
        }
        
        return ResponseEntity.ok(user);
    }

    /**
     * 创建用户
     *
     * @param user 用户信息
     * @return 创建成功的用户
     */
    @PostMapping
    @Operation(summary = "创建用户", description = "新增用户信息")
    @ApiResponses({
        @ApiResponse(responseCode = "200", description = "创建成功",
                content = @Content(schema = @Schema(implementation = User.class)))
    })
    public ResponseEntity<User> createUser(
            @Parameter(description = "用户信息", required = true)
            @RequestBody User user) {
        log.info("接收创建用户的请求: {}", user);
        User createdUser = userService.createUser(user);
        return ResponseEntity.ok(createdUser);
    }
}

5.5 主启动类

package com.jam.datamasking;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * 应用主启动类
 *
 * @author 果酱
 */
@SpringBootApplication
@MapperScan("com.jam.datamasking.mapper")
public class SpringbootDataMaskingApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringbootDataMaskingApplication.class, args);
    }
}

六、测试验证与结果分析

6.1 接口返回数据脱敏验证

启动应用后,访问 Swagger3 界面:http://localhost:8080/swagger-ui.html

调用GET /api/users接口,查看返回结果:

[
  {
    "id": 1,
    "username": "zhangsan",
    "realName": "张*",
    "idCard": "110101********1234",
    "phone": "138****5678",
    "email": "zha***@example.com",
    "bankCard": "622202********1234",
    "address": "北京市朝***",
    "createTime": "2023-12-01T10:00:00",
    "updateTime": "2023-12-01T10:00:00"
  },
  {
    "id": 2,
    "username": "lisi",
    "realName": "李*",
    "idCard": "310101********5678",
    "phone": "139****4321",
    "email": "lis***@example.com",
    "bankCard": "622848********1234",
    "address": "上海市浦***",
    "createTime": "2023-12-01T10:00:00",
    "updateTime": "2023-12-01T10:00:00"
  }
]

可以看到,所有标记了 @DataMask 注解的字段都按照预期进行了脱敏处理,而未标记的字段(如 username)则正常显示。

6.2 日志脱敏验证

查看应用启动日志,可以看到日志中的用户信息也进行了脱敏处理:

2023-12-01 10:30:00.123 DEBUG 12345 --- [nio-8080-exec-1] c.j.d.aspect.LogMaskAspect               : 调用方法: com.jam.datamasking.service.impl.UserServiceImpl.getAllUsers,参数: []
2023-12-01 10:30:00.125  INFO 12345 --- [nio-8080-exec-1] c.j.d.s.impl.UserServiceImpl             : 查询所有用户信息
2023-12-01 10:30:00.156 DEBUG 12345 --- [nio-8080-exec-1] c.j.d.aspect.LogMaskAspect               : 方法: com.jam.datamasking.service.impl.UserServiceImpl.getAllUsers 返回结果: [{"id":1,"username":"zhangsan","realName":"张*","idCard":"110101********1234","phone":"138****5678","email":"zha***@example.com","bankCard":"622202********1234","address":"北京市朝***","createTime":"2023-12-01T10:00:00","updateTime":"2023-12-01T10:00:00"},{"id":2,"username":"lisi","realName":"李*","idCard":"310101********5678","phone":"139****4321","email":"lis***@example.com","bankCard":"622848********1234","address":"上海市浦***","createTime":"2023-12-01T10:00:00","updateTime":"2023-12-01T10:00:00"}]

6.3 数据库存储验证

查看数据库中的原始数据,确认数据是以原始形式存储的,脱敏仅发生在查询和返回过程中:

SELECT * FROM user;

查询结果显示所有敏感信息都是完整存储的,这验证了我们的脱敏处理不会修改原始数据,只是在展示和传输过程中进行脱敏。

七、高级特性与扩展方案

7.1 基于角色的动态脱敏策略

在实际应用中,不同角色的用户可能需要访问不同敏感程度的数据。例如,管理员可以查看完整的手机号,而普通用户只能看到脱敏后的手机号。

实现思路:

  1. 定义角色枚举和脱敏级别枚举
  2. 创建角色与脱敏级别的映射关系
  3. 修改脱敏注解,支持指定不同角色对应的脱敏策略
  4. 在脱敏序列化器中,根据当前登录用户的角色动态选择脱敏策略

核心代码实现:

// 角色枚举
public enum Role {
    ADMIN, OPERATOR, USER, GUEST
}

// 脱敏级别枚举
public enum MaskLevel {
    NONE, LOW, MEDIUM, HIGH
}

// 扩展的脱敏注解
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DynamicDataMask {
    // 默认脱敏策略
    MaskStrategy defaultStrategy();
    
    // 不同角色对应的脱敏策略
    RoleMask[] roleStrategies() default {};
}

// 角色与脱敏策略的映射
public @interface RoleMask {
    Role role();
    MaskStrategy strategy();
}

// 动态脱敏序列化器
public class DynamicDataMaskSerializer extends JsonSerializer<String> implements ContextualSerializer {
    // 实现根据当前用户角色动态选择脱敏策略的逻辑
}

7.2 可逆脱敏方案

在某些场景下,我们需要对数据进行可逆脱敏,即可以根据需要还原原始数据。例如,客服人员在验证用户身份后,可以查看完整的手机号。

可逆脱敏通常采用加密算法实现,如 AES 对称加密:

package com.jam.datamasking.util;

import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

/**
 * 可逆脱敏工具类
 * 使用AES算法实现数据的加密和解密
 *
 * @author 果酱
 */
public class ReversibleMaskUtils {

    /**
     * 加密算法
     */
    private static final String ALGORITHM = "AES";
    
    /**
     * 加密密钥 (实际应用中应从安全的配置源获取)
     */
    private static final String KEY = "your-secret-key-16bytes"; // AES-128需要16字节密钥

    /**
     * 对数据进行加密(可逆脱敏)
     *
     * @param data 原始数据
     * @return 加密后的数据
     */
    public static String encrypt(String data) {
        try {
            SecretKeySpec keySpec = new SecretKeySpec(KEY.getBytes(StandardCharsets.UTF_8), ALGORITHM);
            Cipher cipher = Cipher.getInstance(ALGORITHM);
            cipher.init(Cipher.ENCRYPT_MODE, keySpec);
            byte[] encrypted = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));
            return Base64.getEncoder().encodeToString(encrypted);
        } catch (Exception e) {
            throw new RuntimeException("数据加密失败", e);
        }
    }

    /**
     * 对加密数据进行解密(还原原始数据)
     *
     * @param encryptedData 加密后的数据
     * @return 原始数据
     */
    public static String decrypt(String encryptedData) {
        try {
            SecretKeySpec keySpec = new SecretKeySpec(KEY.getBytes(StandardCharsets.UTF_8), ALGORITHM);
            Cipher cipher = Cipher.getInstance(ALGORITHM);
            cipher.init(Cipher.DECRYPT_MODE, keySpec);
            byte[] decrypted = cipher.doFinal(Base64.getDecoder().decode(encryptedData));
            return new String(decrypted, StandardCharsets.UTF_8);
        } catch (Exception e) {
            throw new RuntimeException("数据解密失败", e);
        }
    }
}

使用可逆脱敏时需要注意:

  • 密钥管理至关重要,应使用安全的密钥管理服务
  • 解密操作需要严格的权限控制
  • 敏感操作需要记录审计日志

7.3 性能优化与缓存策略

对于高并发场景,频繁的脱敏操作可能会影响系统性能。可以采用以下优化策略:

  1. 结果缓存:对同一数据的脱敏结果进行缓存,避免重复计算
  2. 异步处理:非实时场景下,采用异步方式进行脱敏处理
  3. 批量处理:对批量数据采用批量脱敏算法,提高处理效率

缓存实现示例:

package com.jam.datamasking.util;

import com.jam.datamasking.enums.MaskStrategy;
import org.apache.commons.lang3.StringUtils;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 带缓存的脱敏工具类
 * 对脱敏结果进行缓存,提高性能
 *
 * @author 果酱
 */
public class CachedMaskUtils {

    /**
     * 脱敏结果缓存
     * 结构: strategy -> originalValue -> maskedValue
     */
    private static final Map<MaskStrategy, Map<String, String>> MASK_CACHE = new ConcurrentHashMap<>();

    /**
     * 缓存最大容量
     */
    private static final int MAX_CACHE_SIZE = 10000;

    /**
     * 根据策略对字符串进行脱敏(带缓存)
     *
     * @param str      原始字符串
     * @param strategy 脱敏策略
     * @return 脱敏后的字符串
     */
    public static String mask(String str, MaskStrategy strategy) {
        // 字符串为空直接返回
        if (!StringUtils.hasText(str)) {
            return str;
        }

        // 从缓存获取脱敏结果
        Map<String, String> strategyCache = MASK_CACHE.computeIfAbsent(strategy, k -> new HashMap<>(1000));
        String cachedResult = strategyCache.get(str);
        
        // 缓存命中,直接返回
        if (StringUtils.hasText(cachedResult)) {
            return cachedResult;
        }

        // 缓存未命中,计算脱敏结果
        String maskedResult = MaskUtils.mask(str, strategy);
        
        // 控制缓存大小,防止内存溢出
        if (strategyCache.size() < MAX_CACHE_SIZE) {
            strategyCache.put(str, maskedResult);
        }
        
        return maskedResult;
    }

    /**
     * 清空缓存
     */
    public static void clearCache() {
        MASK_CACHE.clear();
    }

    /**
     * 清空指定策略的缓存
     *
     * @param strategy 脱敏策略
     */
    public static void clearCache(MaskStrategy strategy) {
        Map<String, String> strategyCache = MASK_CACHE.get(strategy);
        if (strategyCache != null) {
            strategyCache.clear();
        }
    }
}

八、最佳实践与避坑指南

8.1 数据脱敏最佳实践

  1. 最小权限原则:只对必要的字段进行脱敏,只向必要的人员展示必要的信息

  2. 分层脱敏策略

    • 传输层:API 接口返回数据脱敏
    • 应用层:日志打印脱敏、页面展示脱敏
    • 存储层:敏感字段加密存储(如密码)
  3. 统一脱敏规则:在企业内部制定统一的脱敏规则和标准,确保脱敏行为的一致性

  4. 定期审计:定期检查脱敏规则的执行情况,确保敏感数据得到有效保护

  5. 结合数据分类:根据数据敏感度分级,对不同级别的数据应用不同的脱敏策略

8.2 常见问题与解决方案

  1. 脱敏与数据校验冲突

问题:脱敏后的字段可能无法通过格式校验(如手机号格式)

解决方案:

  • 校验逻辑应基于原始数据执行
  • 脱敏操作应在数据校验之后进行
  • 前端展示时可同时显示脱敏值和原始格式说明
  1. 脱敏性能问题

问题:高并发场景下,大量数据的脱敏处理可能导致性能瓶颈

解决方案:

  • 采用缓存机制减少重复计算
  • 对非敏感接口关闭脱敏处理
  • 复杂脱敏逻辑异步处理
  1. 序列化框架兼容性

问题:不同的序列化框架(如 Jackson、Fastjson)可能需要不同的脱敏实现

解决方案:

  • 统一应用的序列化框架
  • 为不同框架实现对应的脱敏处理器
  • 核心脱敏逻辑与序列化框架解耦
  1. 脱敏规则变更

问题:脱敏规则变更时需要修改大量代码

解决方案:

  • 将脱敏规则配置化,支持动态调整
  • 脱敏策略与业务代码解耦
  • 实现脱敏规则的热更新

九、参考资料

  1. 《信息安全技术 个人信息安全规范》(GB/T 35273-2020) - 国家标准
  2. Spring 官方文档 - https://spring.io/docs
  3. MyBatis-Plus 官方文档 - MyBatis-Plus 🚀 为简化开发而生
  4. 《数据安全架构设计与实战》- 机械工业出版社
  5. OWASP 数据脱敏指南 - https://cheatsheetseries.owasp.org/cheatsheets/Data_Protection_Cheat_Sheet.html

    网站公告

    今日签到

    点亮在社区的每一天
    去签到