前言
大家好!我是大华! 在日常的开发开发工作中,我相信各位老铁肯定遇到过这种需求: "手机号中间四位得用*显示"、"身份证中间八位要隐藏"、"用户邮箱前缀脱敏"...... 例如:
- 手机号:
13812345678
→138****5678
- 身份证:
430101199003078888
→430101********8888
- 姓名:
张三四
→张*四
- 邮箱号:
12345678@qq.com
→1234****@qq.com
- 银行卡:
6230351888852405
→6230********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切面深度脱敏(推荐)
这个是主推的方案,特别适合嵌套对象、List
、Map
等类型。 为什么?因为前面的方法大多只处理单层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种注入方式你用对了吗?》