隐藏用户敏感信息

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

你是否遇到过这些场景?

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

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

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

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

本文将带你构建一套覆盖存储、查询、传输、日志的全链路脱敏方案,用 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 或自定义上下文传递机制。


八、结语

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

相关推荐
Code_Solitude7 分钟前
C语言:关于二维数组的作业总结
java·c语言·前端
Wang's Blog9 分钟前
Java 项目实战: 外卖平台优化-Nginx六种负载均衡策略对比与选型
java·nginx·负载均衡
Wang's Blog21 分钟前
Java 项目实战: 外卖平台优化-YApi接口管理平台与文档导入导出
java·开发语言·yapi
专业程序开发源26 分钟前
django新闻推荐系统70655-计算机课程设计、毕业设计
java·javascript·spring boot·后端·python·django·课程设计
vx_Biye_Design42 分钟前
springboot一站式旅游管理平台81037-计算机课程设计、毕业设计
java·vue.js·spring boot·后端·课程设计·express·旅游
写后端的胖头鱼1 小时前
时间复杂度 & 空间复杂度
java·数据结构·算法·时间复杂度·空间复杂度
嵌入式学习菌1 小时前
ESP32 ModbusTCP 分片缓存
java·后端·spring
vx_Biye_Design1 小时前
springboot宠物寄养服务预约与监管系统82684-计算机课程设计、毕业设计
java·vue.js·spring boot·elasticsearch·课程设计·express·宠物
程序员小杰@1 小时前
Spring Boot 常用注解分类速记
java·spring boot·后端
vx_Biye_Design1 小时前
springboot小学生英语学习APP62773-计算机课程设计、毕业设计
java·vue.js·spring boot·后端·python·学习·课程设计