本文目录导读:
构建一个全栈项目的数据脱敏方案,需要从前端展示、后端接口、数据库存储以及日志审计等多个层面进行系统性设计,核心目标是:在保证开发和测试效率的同时,确保敏感数据(如手机号、身份证号、银行卡号、密码等)在非必需场景下不可见或不可逆。
以下是一个分层、可落地的全栈数据脱敏方案。
总体原则:分层脱敏,按需解密
- 存储层:静态脱敏或加密存储(最安全)。
- 服务层:接口返回前动态脱敏(最常用)。
- 日志层:彻底屏蔽或自动替换(常被忽略)。
- 前端层:仅做展示和脱敏(如手机号
138****1234),但这只是辅助,绝不可依赖前端做安全脱敏。
表现层(前端)脱敏策略
原则:后端已经脱敏,前端只负责展示;或前端对后端已脱敏的数据进行二次格式化(如增加星号)。
常见做法:
- 后端返回已脱敏字符串:最简单,前端直接展示。
- 前端自定义指令/过滤器(如
v-mask="phone"):适用于仅展示场景(如列表页),但隐患是如果黑客拿到原始 JSON 数据,前端代码无保护作用。 - 后端返回原始数据,前端用自定义渲染:只推荐用在用户本人详情页(有权限校验),且需要配合前端水印防截屏。
服务层(后端)脱敏方案(核心)
这是最核心、最可控的环节,推荐使用 注解 + AOP / 拦截器 / 序列化器 的方式。
方案 1:注解 + 返回体序列化拦截(最推荐)
思路:在 DTO(数据传输对象) 或 VO(视图对象) 的字段上标注脱敏类型,在 JSON 序列化阶段自动替换。
Java (Spring Boot) 示例:
-
定义注解
@Sensitive:@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface Sensitive { SensitiveType type(); // 如 PHONE, ID_CARD, BANK_CARD } public enum SensitiveType { PHONE, ID_CARD, BANK_CARD, PASSWORD } -
定义脱敏工具类:
public class DesensitizedUtil { public static String phone(String phone) { return phone.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2"); } public static String idCard(String id) { return id.replaceAll("(\\d{4})\\d{10}(\\w{4})", "$1**********$2"); } public static String password(String pwd) { return "******"; } } -
自定义 Jackson 序列化器:
@JacksonAnnotationsInside @JsonSerialize(using = SensitiveSerializer.class) public @interface Sensitive { SensitiveType type(); } public class SensitiveSerializer extends JsonSerializer<String> { @Override public void serialize(String value, JsonGenerator gen, SerializerProvider provider) { // 获取当前字段上的注解 // 根据注解类型调用 DesensitizedUtil gen.writeString(desensitizedValue); } } -
在 VO 中使用:
public class UserVO { private Long id; private String name; @Sensitive(type = SensitiveType.PHONE) private String phone; @Sensitive(type = SensitiveType.PASSWORD) private String password; }
优势:无侵入、统一管理、性能高、切换方便。
方案 2:MyBatis / ORM 层脱敏(不推荐用于展示)
在从数据库读取后、返回前脱敏,优点是可控制粒度到 SQL 级别,缺点是耦合太深,通常用于数据导出场景,推荐给方案 1。
方案 3:审计日志与异常日志脱敏
常见遗漏:
log.info("用户信息: {}", user)→ 可能直接打印了身份证号。- Elasticsearch 收集的日志。
- 全局异常拦截器返回的堆栈信息。
解决方案:
// 方式一:在日志框架配置中,使用 MaskingLayout(如 Logback)
// 或使用 Aspect 切面拦截所有 log 方法,对敏感参数进行替换
@Aspect
@Component
public class LogDesensitizationAspect {
@Around("execution(* *.log(..)) && args(logger,..)")
public Object protectSensitiveInLog(ProceedingJoinPoint pjp) {
Object[] args = pjp.getArgs();
// 遍历参数,替换敏感字符串模式(如 3-4-4 电话模式)
return pjp.proceed(protectedArgs);
}
}
数据存储层脱敏方案
静态脱敏(用于开发/测试环境)
从生产库导出数据到测试库时,自动替换所有敏感字段为假数据(如所有手机号变为 13800000001),保证开发测试可用而数据不可溯源。
动态脱敏(生产中常用)
加密存储 + 按需解密:
- 存储:在 DAO 层插入前加密(AES / 国密 SM4)。
- 查询:只有特定接口(如用户详情、金融交易)才调用解密方法;普通列表查询直接返回密文或脱敏字符串。
Java 示例(使用 MyBatis TypeHandler):
@MappedTypes(String.class)
@MappedJdbcTypes(JdbcType.VARCHAR)
public class EncryptTypeHandler extends BaseTypeHandler<String> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, String parameter) {
ps.setString(i, AES.encrypt(parameter)); // 加密存入数据库
}
@Override
public String getNullableResult(ResultSet rs, String columnName) {
String raw = rs.getString(columnName);
return raw == null ? null : AES.decrypt(raw); // 自动解密
}
}
注意:该方案对查询性能有影响(如无法用 LIKE 查询脱敏字段),需配合索引分词或等值查询。
不同角色的脱敏策略(权限分级)
同一个数据,不同角色看到的内容不同:
| 用户角色 | 手机号 | 身份证号 |
|---|---|---|
| 普通用户(本人) | 完整 | 隐藏前6位 |
| 客服 | 完整前后四位 | 隐藏中间8位 |
| 管理员 | 完整 | 完整 |
| 其他用户 | 隐藏中间四位 | 全部隐藏 |
实现方式:在后端根据 SecurityContext 中的角色信息,使用策略工厂动态决定脱敏规则。
全栈协同实战流程(以订单详情为例)
- 前端:用户点击“查看完整手机号”。
- 后端:检查权限(用户本人且已通过二次验证),调用加密服务解密数据库中的
phone字段。 - 服务端:记录该操作日志(已脱敏),并将解密后的完整数据返回给前端。
- 前端:展示完整手机号,同时添加数字水印。
- 数据库:
phone字段始终存储加密密文,普通查询只返回脱敏后的 。
常见坑与最佳实践
- 不要在前端做真正脱敏:前端代码是公开的,任何前端脱敏都可被绕过(查看网络请求即可看到原始数据)。
- 统一脱敏库:所有脱敏逻辑统一到后端的一个模块,便于审计和修改。
- 日志是最大的泄露点:务必配置日志脱敏过滤器,禁止打印敏感字段。
- 图片/文件:身份证照片、银行卡照片等文件也需脱敏水印处理。
- API 文档:避免在 Swagger / OpenAPI 文档中暴露敏感字段示例值。
全栈数据脱敏的最佳实践是:
数据存储加密 + 日志脱敏 + 后端通过注解/序列化器动态返回部分脱敏数据 + 前端只做展示格式美化。
具体选择哪种方案,取决于你的合规需求(如等保三级、GDPR、PCI-DSS)和对性能的要求。
如果需要针对特定编程语言(如 Node.js / Python / Go)或特定框架的代码实现,可以进一步说明。