隐藏用户敏感信息

一、前言:为什么必须隐藏敏感信息?

你是否遇到过这些场景?

  • 用户反馈:"我在订单页面看到了别人的手机号!"
  • 安全审计报告指出:"接口返回了完整身份证号"
  • 测试环境数据库被泄露,包含大量明文用户信息

根据《个人信息保护法》和《网络安全法》,手机号、身份证、银行卡号、住址等属于敏感个人信息 ,必须进行最小化展示与传输

但很多开发者只做了"前端打码",却忽略了:

  • ❌ 数据库明文存储
  • ❌ 日志打印完整信息
  • ❌ 接口返回未脱敏
  • ❌ 管理后台直接暴露原始数据

本文将带你构建一套覆盖存储、查询、传输、日志的全链路脱敏方案,用 Spring Boot 实现安全又优雅的数据保护!


二、敏感信息分类与脱敏规则

敏感类型 示例 脱敏规则(通用)
手机号 13812345678 138****5678
身份证号 11010119900307XXXX 110101********07XXXX
银行卡号 6222080402564890018 6222 **** **** 0018
姓名 张三丰 张*丰***
邮箱 mailto:zhangsan@example.com zh******@example.com
地址 北京市朝阳区建国路88号 北京市朝阳区******

📌 原则保留必要识别信息 + 隐藏关键部分


三、方案一:接口层脱敏(推荐!灵活可控)

核心思路:通过自定义注解 + Jackson 序列化器,在 JSON 输出时自动脱敏。

1. 定义脱敏类型枚举
java 复制代码
public enum SensitiveType {
    MOBILE,        // 手机号
    ID_CARD,       // 身份证
    BANK_CARD,     // 银行卡
    EMAIL,         // 邮箱
    NAME,          // 姓名
    ADDRESS        // 地址
}
2. 创建脱敏注解
java 复制代码
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@JacksonAnnotationsInside
@JsonSerialize(using = SensitiveJsonSerializer.class)
public @interface Sensitive {
    SensitiveType value();
}
3. 实现自定义序列化器
java 复制代码
public class SensitiveJsonSerializer extends JsonSerializer<String> {

    @Override
    public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) 
            throws IOException {
        if (value == null) {
            gen.writeNull();
            return;
        }

        // 从上下文获取注解(需配合自定义模块)
        // 此处简化:通过字段名或传参判断,实际可通过反射获取注解
        // 更佳做法见下文"进阶实现"
        String masked = mask(value, getSensitiveTypeFromContext());
        gen.writeString(masked);
    }

    private String mask(String value, SensitiveType type) {
        switch (type) {
            case MOBILE:
                if (value.length() == 11) {
                    return value.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
                }
                break;
            case ID_CARD:
                if (value.length() >= 18) {
                    return value.replaceAll("(\\d{6})\\d{8}(\\w{4})", "$1********$2");
                }
                break;
            case BANK_CARD:
                if (value.length() >= 16) {
                    return value.replaceAll(".{4}(?=.{4})", "**** ");
                }
                break;
            case EMAIL:
                if (value.contains("@")) {
                    String[] parts = value.split("@");
                    String prefix = parts[0];
                    if (prefix.length() > 2) {
                        return prefix.substring(0, 2) + "******@" + parts[1];
                    }
                }
                break;
            case NAME:
                if (value.length() == 2) {
                    return value.substring(0, 1) + "*";
                } else if (value.length() > 2) {
                    return value.substring(0, 1) + "**";
                }
                break;
            case ADDRESS:
                if (value.length() > 6) {
                    return value.substring(0, 6) + "******";
                }
                break;
        }
        return value;
    }

    // 简化:实际应通过字段元数据获取类型
    private SensitiveType getSensitiveTypeFromContext() {
        // 进阶实现见下文
        return SensitiveType.MOBILE;
    }
}

⚠️ 上述简化版无法动态识别字段类型。下面提供生产级实现


4. 生产级方案:通过自定义 Jackson 模块 + 反射

java 复制代码
// 自定义序列化器(支持动态获取注解)
public class SensitiveJsonSerializer extends JsonSerializer<String> 
        implements ContextualSerializer {

    private SensitiveType sensitiveType;

    public SensitiveJsonSerializer() {}

    public SensitiveJsonSerializer(SensitiveType sensitiveType) {
        this.sensitiveType = sensitiveType;
    }

    @Override
    public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) 
            throws IOException {
        if (value == null) {
            gen.writeNull();
            return;
        }
        gen.writeString(mask(value, sensitiveType));
    }

    @Override
    public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) 
            throws JsonMappingException {
        if (property != null) {
            Sensitive annotation = property.getAnnotation(Sensitive.class);
            if (annotation != null) {
                return new SensitiveJsonSerializer(annotation.value());
            }
        }
        return this;
    }

    // mask 方法同上...
}
5. 在实体类中使用注解
java 复制代码
public class UserDTO {
    private Long id;
    
    @Sensitive(SensitiveType.NAME)
    private String name;
    
    @Sensitive(SensitiveType.MOBILE)
    private String phone;
    
    @Sensitive(SensitiveType.ID_CARD)
    private String idCard;
    
    @Sensitive(SensitiveType.EMAIL)
    private String email;
    
    // getter/setter...
}
6. 控制器返回自动脱敏
java 复制代码
@GetMapping("/user/{id}")
public UserDTO getUser(@PathVariable Long id) {
    User user = userService.findById(id);
    UserDTO dto = convertToDTO(user);
    return dto; // 返回时自动脱敏!
}

效果

java 复制代码
{
  "id": 1001,
  "name": "张**",
  "phone": "138****5678",
  "idCard": "110101********07XXXX",
  "email": "zh******@example.com"
}

四、方案二:数据库存储脱敏(高安全场景)

对于极高敏感字段(如身份证、银行卡),建议加密存储

1. 存储时加密(AES / SM4)

java 复制代码
// 保存前加密
user.setIdCard(AesUtil.encrypt(rawIdCard, key));

// 查询后解密(仅限内部系统)
String realIdCard = AesUtil.decrypt(encryptedIdCard, key);

🔒 注意:加密后无法按原文查询(如"查身份证尾号为1234的用户"),需权衡。

2. 使用数据库透明加密(TDE)

  • MySQL Enterprise TDE
  • Oracle TDE
  • 华为云/阿里云 RDS 加密

五、其他环节脱敏

1. 日志脱敏

避免 log.info("用户信息: {}", user) 打印完整对象。

方案

  • 重写 toString() 方法(不推荐,影响调试)
  • 使用 MDC + 日志过滤器
  • 最佳:日志中只打印脱敏后的 DTO
java 复制代码
log.info("处理用户 [{}] 的请求", userDTO.getPhone()); // 已脱敏

2. 管理后台权限控制

  • 普通客服:只能看到 138****5678
  • 高级管理员:可申请查看完整信息(需审批 + 操作留痕)

六、避坑指南:常见误区

❌ 误区 1:只在前端脱敏

风险 :F12 查看网络请求,原始数据暴露
正解脱敏必须在服务端完成

❌ 误区 2:脱敏规则硬编码在业务逻辑中

后果 :重复代码多,难以统一修改
正解使用注解 + 序列化器集中管理

❌ 误区 3:测试环境使用真实用户数据

风险 :测试库泄露 = 用户信息泄露
正解测试数据必须脱敏或伪造


七、进阶:动态脱敏(按角色/场景)

有时需要:

  • 普通用户:138****5678
  • 本人查看:13812345678
  • 客服:138****5678
  • 财务:13812345678(需授权)

实现思路

  1. 在序列化器中注入当前用户上下文(如 UserContext.getCurrentUserId()
  2. 判断是否为本人或有权限
  3. 动态决定是否脱敏
java 复制代码
if (isOwner(fieldValue, currentUserId) || hasPermission()) {
    gen.writeString(value); // 不脱敏
} else {
    gen.writeString(mask(value, type)); // 脱敏
}

💡 需结合 Spring Security 或自定义上下文传递机制。


八、结语

感谢您的阅读!如果你有任何疑问或想要分享的经验,请在评论区留言交流!

相关推荐
wangmengxxw2 小时前
SpringAi-MCP技术
java·大模型·springai·mcp
@老蝴2 小时前
MySQL数据库 - 事务
java·数据库·mysql
木井巳2 小时前
【Java】深入理解Java语言的重要概念
java·开发语言
yangminlei2 小时前
MyBatis插件开发-实现SQL执行耗时监控
java·开发语言·tomcat
what丶k2 小时前
Java连接人大金仓数据库(KingbaseES)全指南:从环境搭建到实战优化
java·开发语言·数据库
沛沛老爹2 小时前
从Web到AI:多模态Agent Skills开发实战——JavaScript+Python全栈赋能视觉/语音能力
java·开发语言·javascript·人工智能·python·安全架构
0x532 小时前
JAVA|智能仿真并发项目-进程与线程
java·开发语言·jvm
xiaolyuh1232 小时前
Spring Boot 深度解析
java·spring boot·后端
黎雁·泠崖2 小时前
Java静态方法:用法+工具类设计+ArrayUtil实战
java·开发语言