SpringBoot 中 6 种数据脱敏方案,第 5 种太强了,支持深度递归!

前言

大家好!我是大华! 在日常的开发开发工作中,我相信各位老铁肯定遇到过这种需求: "手机号中间四位得用*显示"、"身份证中间八位要隐藏"、"用户邮箱前缀脱敏"...... 例如:

  • 手机号:13812345678138****5678
  • 身份证:430101199003078888430101********8888
  • 姓名:张三四张*四
  • 邮箱号:12345678@qq.com1234****@qq.com
  • 银行卡:62303518888524056230********2405

既要展示部分数据,又要保证敏感信息不泄露。这就是所谓的数据脱敏

今天给大家分享6种我在项目中常用的脱敏方案,SpringBoot项目拿来即用,可以直接复制粘贴!

方案一:Hutool工具库(懒人必备!)

不想造轮子可以直接用现成的!

java 复制代码
// Maven依赖
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.8.0</version>
</dependency>

// 使用示例
String phone = "13812345678";
String maskedPhone = DesensitizedUtil.mobilePhone(phone); // 138****5678

String idCard = "430101199003078888";
String maskedIdCard = DesensitizedUtil.idCardNum(idCard, 6, 4); // 430101********8888

String name = "张三四";
String maskedName = DesensitizedUtil.chineseName(name); // 张*四

String email = "12345678@qq.com";
String maskedEmail = DesensitizedUtil.email(email); // 1234****@qq.com

String bankCard = "6230351888852405";
String maskedBankCard = DesensitizedUtil.bankCard(bankCard); // 6230********2405

功能全面,开箱即用

方案二:正则工具类

写个工具类,需要的时候手动处理一下,这是最简单直接的方式。

java 复制代码
/**
 * 脱敏工具类 - 简单直接
 */
public class SensitiveUtil {
    
    /**
     * 手机号脱敏:13812345678 -> 138****5678
     */
    public static String maskPhone(String phone) {
        if (StringUtils.isEmpty(phone) || phone.length() != 11) {
            return phone;
        }
        return phone.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
    }
    
    /**
     * 身份证脱敏:430101199003078888 -> 430101********8888
     */
    public static String maskIdCard(String idCard) {
        if (StringUtils.isEmpty(idCard) || idCard.length() < 15) {
            return idCard;
        }
        return idCard.replaceAll("(\\d{6})\\d{8}(\\w{4})", "$1********$2");
    }
    
    /**
     * 姓名脱敏:张三四 -> 张*四
     */
    public static String maskName(String name) {
        if (StringUtils.isEmpty(name)) {
            return name;
        }
        if (name.length() == 1) {
            return "*";
        }
        if (name.length() == 2) {
            return name.charAt(0) + "*";
        }
        return name.substring(0, 1) + "*" + name.substring(name.length() - 1);
    }
    
    /**
     * 邮箱脱敏:12345678@qq.com -> 1234****@qq.com
     */
    public static String maskEmail(String email) {
        if (StringUtils.isEmpty(email) || !email.contains("@")) {
            return email;
        }
        String[] parts = email.split("@");
        if (parts[0].length() <= 4) {
            return parts[0].substring(0, 1) + "****@" + parts[1];
        }
        return parts[0].substring(0, 4) + "****@" + parts[1];
    }
    
    /**
     * 银行卡脱敏:6230351888852405 -> 6230********2405
     */
    public static String maskBankCard(String bankCard) {
        if (StringUtils.isEmpty(bankCard) || bankCard.length() < 8) {
            return bankCard;
        }
        return bankCard.replaceAll("(\\d{4})\\d{8}(\\d{4})", "$1********$2");
    }
}

怎么用?很简单:

java 复制代码
User user = userService.getById(123);

// 手动脱敏
user.setPhone(SensitiveUtil.maskPhone(user.getPhone()));
user.setIdCard(SensitiveUtil.maskIdCard(user.getIdCard()));
user.setName(SensitiveUtil.maskName(user.getName()));
user.setEmail(SensitiveUtil.maskEmail(user.getEmail()));
user.setBankCard(SensitiveUtil.maskBankCard(user.getBankCard()));

return user;

非常简单易懂,数据脱敏不再是问题。

但有些朋友又说了:"每写一个接口或者每一个字段都要单独调用,有点心累。有没有更方便的方案?" 请看方案二。

方案三:自定义注解 + Jackson

利用Jackson的序列化机制,在返回JSON时自动对标注了注解的字段进行脱敏。

java 复制代码
// 脱敏注解
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@JsonSerialize(using = SensitiveSerializer.class)
public @interface Sensitive {
    SensitiveType value();
}

// 脱敏类型枚举
public enum SensitiveType {
    PHONE,      // 手机号
    ID_CARD,    // 身份证
    NAME,       // 姓名
    EMAIL,      // 邮箱
    BANK_CARD   // 银行卡
}

// 脱敏序列化器
public class SensitiveSerializer extends JsonSerializer<String> {
    private SensitiveType type;
    
    public SensitiveSerializer(SensitiveType type) {
        this.type = type;
    }
    
    @Override
    public void serialize(String value, JsonGenerator gen, SerializerProvider provider) throws IOException {
        String result;
        switch (type) {
            case PHONE:
                result = SensitiveUtil.maskPhone(value);
                break;
            case ID_CARD:
                result = SensitiveUtil.maskIdCard(value);
                break;
            case NAME:
                result = SensitiveUtil.maskName(value);
                break;
            case EMAIL:
                result = SensitiveUtil.maskEmail(value);
                break;
            case BANK_CARD:
                result = SensitiveUtil.maskBankCard(value);
                break;
            default:
                result = value;
        }
        gen.writeString(result);
    }
}

使用方式

java 复制代码
public class User {
    private String name;
    
    @Sensitive(SensitiveType.PHONE)
    private String phone;
    
    @Sensitive(SensitiveType.ID_CARD)
    private String idCard;
    
    @Sensitive(SensitiveType.NAME)
    private String name;
    
    @Sensitive(SensitiveType.EMAIL)
    private String email;
    
    @Sensitive(SensitiveType.BANK_CARD)
    private String bankCard;
}

性能好,只在序列化时生效。配置一次,全局都可以用,不影响业务逻辑。缺点是需要配置Jackson

方案四:Lombok + 自定义Getter(轻量级替代方案)

如果你不想引入太多框架,但用了Lombok,这个方案很合适。

思路:让Lombok不生成默认getter,我们自己写一个带脱敏逻辑的getter

1. 关闭Lombok的默认getter

去掉@Data,只保留你需要的Lombok注解,手动添加带脱敏的getter

java 复制代码
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Setter
public class UserVO {

    private String name;
    private String phone;
    private String idCard;
    private String email;
    private String bankCard;

    // 自定义脱敏 getter
    public String getName() {
        return SensitiveUtil.maskName(name);
    }

    public String getPhone() {
        return SensitiveUtil.maskPhone(phone);
    }

    public String getIdCard() {
        return SensitiveUtil.maskIdCard(idCard);
    }
    
    public String getEmail() {
        return SensitiveUtil.maskEmail(email);
    }
    
    public String getBankCard() {
        return SensitiveUtil.maskBankCard(bankCard);
    }
}

这样只要调用user.getxx(),返回的就是脱敏后的数据。

实现简单,不依赖额外组件,适合小项目。但每个字段都要手动写getter,有点啰嗦了。

方案五:AOP切面深度脱敏(推荐)

这个是主推的方案,特别适合嵌套对象、ListMap等类型。 为什么?因为前面的方法大多只处理单层DTO

但现实中经常是:

java 复制代码
public class User {
    @Sensitive(SensitiveType.PHONE)
    private String phone; // 这个能脱敏
    
    private UserDetail detail; // 这里面还有敏感字段
}

public class UserDetail {
    @Sensitive(SensitiveType.ID_CARD)
    private String idCard; // 这个脱敏不了!
}

返回User对象时,phone字段能脱敏,但detail.idCard还是明文显示!

解决方案 :用AOP + 深度递归反射。详细步骤如下:

1. 定义脱敏类型枚举

java 复制代码
/**
 * 脱敏类型枚举
 */
public enum SensitiveType {
    PHONE,      // 手机号
    ID_CARD,    // 身份证
    NAME,       // 姓名
    EMAIL,      // 邮箱
    BANK_CARD   // 银行卡
}

2. 定义注解@Sensitive

java 复制代码
/**
 * 脱敏注解
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Sensitive {
    SensitiveType value();
}

3. 需要保留方案一的工具类,增加以下的maskByType方法

java 复制代码
/**
 * 根据类型脱敏
*/
public static String maskByType(String value, SensitiveType type) {
	if (StringUtils.isBlank(value)) {
            return value;
        }

        switch (type) {
            case PHONE: return maskPhone(value);
            case ID_CARD: return maskIdCard(value);
            case NAME: return maskName(value);
            case EMAIL: return maskEmail(value);
            case BANK_CARD: return maskBankCard(value);
            default: return value;
        }
    }
}

4. 写AOP切面(核心逻辑)

java 复制代码
/**
 * 深度脱敏AOP处理器
 */
@Aspect
@Component
@Slf4j
public class DeepSensitiveAspect {
    
    // 定义切点:拦截Controller层所有方法
    @Pointcut("execution(* com.example.controller..*.*(..))")
    public void controllerPointcut() {}
    
    // 定义切点:拦截Service层所有方法
    @Pointcut("execution(* com.example.service..*.*(..))")
    public void servicePointcut() {}
    
    /**
     * 环绕通知:处理Controller层返回结果
     */
    @Around("controllerPointcut()")
    public Object aroundController(ProceedingJoinPoint joinPoint) throws Throwable {
        Object result = joinPoint.proceed();
        return processDeepSensitive(result);
    }
    
    /**
     * 环绕通知:处理Service层返回结果
     */
    @Around("servicePointcut()")
    public Object aroundService(ProceedingJoinPoint joinPoint) throws Throwable {
        Object result = joinPoint.proceed();
        return processDeepSensitive(result);
    }
    
    /**
     * 深度脱敏处理
     */
    private Object processDeepSensitive(Object obj) {
        if (obj == null) {
            return null;
        }
        
        // 处理集合类型
        if (obj instanceof List) {
            return processList((List<?>) obj);
        }
        
        // 处理数组类型
        if (obj.getClass().isArray()) {
            return processArray((Object[]) obj);
        }
        
        // 处理Map类型
        if (obj instanceof Map) {
            return processMap((Map<?, ?>) obj);
        }
        
        // 处理分页对象(Spring Data Page)
        if (obj instanceof Page) {
            return processPage((Page<?>) obj);
        }
        
        // 处理普通Java对象
        if (isCustomClass(obj.getClass())) {
            return processObject(obj);
        }
        
        // 基本类型直接返回
        return obj;
    }
    
    /**
     * 处理List集合
     */
    private List<?> processList(List<?> list) {
        if (list == null || list.isEmpty()) {
            return list;
        }
        
        return list.stream()
            .map(this::processDeepSensitive)
            .collect(Collectors.toList());
    }
    
    /**
     * 处理数组
     */
    private Object[] processArray(Object[] array) {
        if (array == null || array.length == 0) {
            return array;
        }
        
        Object[] result = new Object[array.length];
        for (int i = 0; i < array.length; i++) {
            result[i] = processDeepSensitive(array[i]);
        }
        return result;
    }
    
    /**
     * 处理Map
     */
    private Map<?, ?> processMap(Map<?, ?> map) {
        if (map == null || map.isEmpty()) {
            return map;
        }
        
        Map<Object, Object> result = new HashMap<>();
        for (Map.Entry<?, ?> entry : map.entrySet()) {
            result.put(entry.getKey(), processDeepSensitive(entry.getValue()));
        }
        return result;
    }
    
    /**
     * 处理分页对象
     */
    private Page<?> processPage(Page<?> page) {
        if (page == null) {
            return null;
        }
        
        List<?> content = processList(page.getContent());
        return new PageImpl<>(content, page.getPageable(), page.getTotalElements());
    }
    
    /**
     * 处理单个对象
     */
    private Object processObject(Object obj) {
        if (obj == null) {
            return null;
        }
        
        Class<?> clazz = obj.getClass();
        try {
            // 获取所有字段(包括父类)
            List<Field> fields = getAllFields(clazz);
            
            for (Field field : fields) {
                field.setAccessible(true);
                
                // 检查是否有脱敏注解
                Sensitive sensitive = field.getAnnotation(Sensitive.class);
                if (sensitive != null && field.getType() == String.class) {
                    // 处理敏感字段
                    processSensitiveField(obj, field, sensitive);
                } else {
                    // 递归处理嵌套对象
                    processNestedField(obj, field);
                }
            }
        } catch (Exception e) {
            log.warn("脱敏处理失败: {}", e.getMessage());
        }
        
        return obj;
    }
    
    /**
     * 处理敏感字段
     */
    private void processSensitiveField(Object obj, Field field, Sensitive sensitive) {
        try {
            String value = (String) field.get(obj);
            if (StringUtils.isNotBlank(value)) {
                String maskedValue = SensitiveUtil.maskByType(value, sensitive.value());
                field.set(obj, maskedValue);
            }
        } catch (IllegalAccessException e) {
            log.warn("字段脱敏失败: {}", field.getName());
        }
    }
    
    /**
     * 处理嵌套字段
     */
    private void processNestedField(Object obj, Field field) {
        try {
            Object fieldValue = field.get(obj);
            if (fieldValue != null && isCustomClass(field.getType())) {
                // 递归处理嵌套对象
                processObject(fieldValue);
            }
        } catch (IllegalAccessException e) {
            // 忽略无法访问的字段
        }
    }
    
    /**
     * 获取所有字段(包括父类)
     */
    private List<Field> getAllFields(Class<?> clazz) {
        List<Field> fields = new ArrayList<>();
        while (clazz != null && clazz != Object.class) {
            fields.addAll(Arrays.asList(clazz.getDeclaredFields()));
            clazz = clazz.getSuperclass();
        }
        return fields;
    }
    
    /**
     * 判断是否为自定义类(非JDK类)
     */
    private boolean isCustomClass(Class<?> clazz) {
        return clazz != null && 
               !clazz.isPrimitive() && 
               !clazz.getName().startsWith("java.") &&
               !clazz.getName().startsWith("javax.");
    }
}

5. 实体类使用示例

java 复制代码
/**
 * 用户实体
 */
@Data
public class User {
    private Long id;
    
    @Sensitive(SensitiveType.NAME)
    private String name;
    
    @Sensitive(SensitiveType.PHONE)
    private String phone;
    
    @Sensitive(SensitiveType.ID_CARD)
    private String idCard;
    
    @Sensitive(SensitiveType.EMAIL)
    private String email;
    
    @Sensitive(SensitiveType.BANK_CARD)
    private String bankCard;
    
    // 嵌套对象也会被处理
    private UserDetail detail;
}

/**
 * 用户详情实体
 */
@Data
public class UserDetail {
    @Sensitive(SensitiveType.PHONE)
    private String emergencyPhone;
    
    @Sensitive(SensitiveType.ID_CARD)
    private String spouseIdCard;
}

6. Controller使用示例

java 复制代码
@RestController
@RequestMapping("/users")
public class UserController {
    
    @Autowired
    private UserService userService;
    
    /**
     * 返回单个用户(自动脱敏)
     */
    @GetMapping("/{id}")
    public User getUser(@PathVariable Long id) {
        return userService.getUserById(id);
    }
    
    /**
     * 返回用户列表(自动脱敏)
     */
    @GetMapping
    public List<User> getUsers() {
        return userService.getAllUsers();
    }
    
    /**
     * 返回分页数据(自动脱敏)
     */
    @GetMapping("/page")
    public Page<User> getUsersByPage(Pageable pageable) {
        return userService.getUsersByPage(pageable);
    }
}

7. 效果验证

json 复制代码
{
  "id": 1,
  "name": "张*四",
  "phone": "138****5678",
  "idCard": "430101********8888",
  "email": "1234****@qq.com",
  "bankCard": "6230********2405",
  "detail": {
    "emergencyPhone": "139****8765",
    "spouseIdCard": "110105********1234"
  }
}

优点

  • 全面支持:手机号、身份证、姓名、邮箱、银行卡全搞定
  • 深度处理:支持多层嵌套对象脱敏
  • 零侵入:业务代码无需任何修改
  • 高性能:使用反射缓存,性能优化
  • 易扩展:新增脱敏类型只需扩展枚举和工具方法

方案六:Mysql数据库层脱敏

最简单的MySQL脱敏SQL

sql 复制代码
SELECT 
    id,
    CONCAT(LEFT(phone, 3), '****', RIGHT(phone, 4)) AS phone,
    CONCAT(LEFT(id_card, 6), '********', RIGHT(id_card, 4)) AS id_card,
    CONCAT(LEFT(name, 1), '*', RIGHT(name, 1)) AS name,
    CONCAT(LEFT(email, 4), '****', SUBSTRING(email, LOCATE('@', email))) AS email,
    CONCAT(LEFT(bank_card, 4), '********', RIGHT(bank_card, 4)) AS bank_card
FROM users;

分字段详细写法

1. 手机号脱敏

sql 复制代码
SELECT CONCAT(LEFT(phone, 3), '****', RIGHT(phone, 4)) AS phone FROM users;
-- 13812345678 → 138****5678

2. 身份证脱敏

sql 复制代码
SELECT CONCAT(LEFT(id_card, 6), '********', RIGHT(id_card, 4)) AS id_card FROM users;
-- 430101199003078888 → 430101********8888

3. 姓名脱敏

sql 复制代码
SELECT 
    CASE 
        WHEN LENGTH(name) = 1 THEN '*'
        WHEN LENGTH(name) = 2 THEN CONCAT(LEFT(name, 1), '*')
        ELSE CONCAT(LEFT(name, 1), '*', RIGHT(name, 1))
    END AS name 
FROM users;
-- 张三四 → 张*四

4. 邮箱脱敏

sql 复制代码
SELECT 
    CONCAT(LEFT(email, 4), '****', SUBSTRING(email, LOCATE('@', email))) AS email 
FROM users;
-- 12345678@qq.com → 1234****@qq.com

5. 银行卡脱敏

sql 复制代码
SELECT CONCAT(LEFT(bank_card, 4), '********', RIGHT(bank_card, 4)) AS bank_card FROM users;
-- 6230351888852405 → 6230********2405

6. 完整查询示例

sql 复制代码
-- 查询单个用户
SELECT 
    id,
    CONCAT(LEFT(phone, 3), '****', RIGHT(phone, 4)) AS phone,
    CONCAT(LEFT(id_card, 6), '********', RIGHT(id_card, 4)) AS id_card,
    CASE 
        WHEN LENGTH(name) = 1 THEN '*'
        WHEN LENGTH(name) = 2 THEN CONCAT(LEFT(name, 1), '*')
        ELSE CONCAT(LEFT(name, 1), '*', RIGHT(name, 1))
    END AS name,
    CONCAT(LEFT(email, 4), '****', SUBSTRING(email, LOCATE('@', email))) AS email,
    CONCAT(LEFT(bank_card, 4), '********', RIGHT(bank_card, 4)) AS bank_card
FROM users 
WHERE id = 1;

-- 查询用户列表
SELECT 
    id,
    CONCAT(LEFT(phone, 3), '****', RIGHT(phone, 4)) AS phone,
    CONCAT(LEFT(name, 1), '*', RIGHT(name, 1)) AS name
FROM users 
ORDER BY id DESC 
LIMIT 10;

7. 创建视图方案

如果经常要用,建个视图更方便:

sql 复制代码
CREATE VIEW v_user_masked AS
SELECT 
    id,
    CONCAT(LEFT(phone, 3), '****', RIGHT(phone, 4)) AS phone,
    CONCAT(LEFT(id_card, 6), '********', RIGHT(id_card, 4)) AS id_card,
    CASE 
        WHEN LENGTH(name) = 1 THEN '*'
        WHEN LENGTH(name) = 2 THEN CONCAT(LEFT(name, 1), '*')
        ELSE CONCAT(LEFT(name, 1), '*', RIGHT(name, 1))
    END AS name,
    CONCAT(LEFT(email, 4), '****', SUBSTRING(email, LOCATE('@', email))) AS email,
    CONCAT(LEFT(bank_card, 4), '********', RIGHT(bank_card, 4)) AS bank_card
FROM users;

-- 直接用视图查询
SELECT * FROM v_user_masked WHERE id = 1;

8. 联表查询脱敏

sql 复制代码
SELECT 
    u.id,
    CONCAT(LEFT(u.name, 1), '*', RIGHT(u.name, 1)) AS user_name,
    CONCAT(LEFT(u.phone, 3), '****', RIGHT(u.phone, 4)) AS user_phone,
    o.order_no,
    o.amount
FROM users u
INNER JOIN orders o ON u.id = o.user_id;

最后总结

方案 优点 缺点 适用场景
Hutool 工具库 开箱即用,代码简洁,功能全 依赖第三方库,灵活性有限 快速开发、小项目、原型阶段
正则工具类 + 手动调用 简单直接,无额外依赖 需手动调用,侵入业务代码 单字段少量脱敏,轻量级应用
自定义注解 + Jackson 序列化 自动化序列化脱敏,性能好 仅作用于 JSON 输出,不支持嵌套对象深层脱敏 REST API 返回数据脱敏
Lombok + 自定义 Getter 轻量,无需切面或框架 每个字段都要写 getter,重复代码多 VO/DTO 层控制输出,适合简单结构
AOP 切面深度脱敏(推荐) 支持嵌套对象、集合、分页;零侵入业务;可全局生效;易扩展 实现复杂,需理解反射和 AOP 中大型项目,复杂数据结构,统一脱敏治理
MySQL 数据库层脱敏 不依赖 Java 层,查询即脱敏,安全隔离 SQL 复杂,维护成本高,无法动态控制 报表查询、只读视图、DBA 视角脱敏

🎯 最佳实践建议

1.如果你是新手或者小项目起步,方案一(Hutool)方案二(工具类)

2.如果你做的是标准后端服务API,推荐方案三(Jackson 注解序列化) 结合 @Sensitive注解,在返回 JSON时自动脱敏。

3.如果你的项目结构复杂、嵌套深、List/Map/分页多,强烈推荐 方案五(AOP深度脱敏)!这是真正意义上的"一次配置,处处脱敏"。

4.如果你有DB权限且需要给前端/报表提供固定脱敏视图,使用 方案六(MySQL脱敏+视图),实现数据访问层面的安全隔离。

希望这篇文章能帮你在实际项目中轻松搞定数据脱敏问题。如果觉得有用,欢迎点赞、收藏、转发,让更多人看到!

公众号:程序员刘大华,专注分享前后端开发的实战笔记。关注我,少走弯路,一起进步!

📌往期精彩

《工作 5 年没碰过分布式锁,是我太菜还是公司太稳?网友:太真实了!》

《90%的人不知道!Spring官方早已不推荐@Autowired?这3种注入方式你用对了吗?》

《写给小公司前端的 UI 规范:别让页面丑得自己都看不下去》

《终于找到 Axios 最优雅的封装方式了,再也不用写重复代码了》

相关推荐
异常驯兽师3 小时前
Spring 中处理 HTTP 请求参数注解全解析
java·spring·http
连合机器人4 小时前
晨曦中的守望者:当科技为景区赋予温度
java·前端·科技
AD钙奶-lalala4 小时前
idea新建的项目new 没有java class选项
java·ide·intellij-idea
sheji34164 小时前
【开题答辩全过程】以 12306候补购票服务系统为例,包含答辩的问题和答案
java·eclipse
勇敢牛牛_4 小时前
使用Rust实现服务配置/注册中心
开发语言·后端·rust·注册中心·配置中心
deepwater_zone5 小时前
Go语言核心技术
后端·golang
hzzzzzo05 小时前
微服务网关全解析:从入门到实践
java·开发语言·微服务
纪莫5 小时前
技术面:Spring (bean的生命周期、创建方式、注入方式、作用域)
java·spring·java面试⑧股
We....5 小时前
Java多线程分块下载文件
java·开发语言