SpringBoot中实现数据脱敏的六种常用方案
作者:刘大华
前言
在日常的开发开发工作中,我相信各位老铁肯定遇到过这种需求: “手机号中间四位得用*显示”、“身份证中间八位要隐藏”、“用户邮箱前缀脱敏”…… 例如:
- 手机号:
13812345678
→138****5678
- 身份证:
430101199003078888
→430101********8888
- 姓名:
张三四
→张*四
- 邮箱号:
12345678@qq.com
→1234****@qq.com
- 银行卡:
6230351888852405
→6230********2405
既要展示部分数据,又要保证敏感信息不泄露。这就是所谓的数据脱敏。
今天给大家分享6种我在项目中常用的脱敏方案,SpringBoot
项目拿来即用,可以直接复制粘贴!
方案一:Hutool工具库(懒人必备!)
不想造轮子可以直接用现成的!
// 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
功能全面,开箱即用
方案二:正则工具类
写个工具类,需要的时候手动处理一下,这是最简单直接的方式。
/** * 脱敏工具类 - 简单直接 */ 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"); } }
怎么用?很简单:
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
时自动对标注了注解的字段进行脱敏。
// 脱敏注解 @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); } }
使用方式:
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
@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
。
但现实中经常是:
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. 定义脱敏类型枚举
/** * 脱敏类型枚举 */ public enum SensitiveType { PHONE, // 手机号 ID_CARD, // 身份证 NAME, // 姓名 EMAIL, // 邮箱 BANK_CARD // 银行卡 }
2. 定义注解@Sensitive
/** * 脱敏注解 */ @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface Sensitive { SensitiveType value(); }
3. 需要保留方案一的工具类,增加以下的maskByType方法
/** * 根据类型脱敏 */ 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切面(核心逻辑)
/** * 深度脱敏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. 实体类使用示例
/** * 用户实体 */ @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使用示例
@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. 效果验证
{ "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
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. 手机号脱敏
SELECT CONCAT(LEFT(phone, 3), '****', RIGHT(phone, 4)) AS phone FROM users; -- 13812345678 → 138****5678
2. 身份证脱敏
SELECT CONCAT(LEFT(id_card, 6), '********', RIGHT(id_card, 4)) AS id_card FROM users; -- 430101199003078888 → 430101********8888
3. 姓名脱敏
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. 邮箱脱敏
SELECT CONCAT(LEFT(email, 4), '****', SUBSTRING(email, LOCATE('@', email))) AS email FROM users; -- 12345678@qq.com → 1234****@qq.com
5. 银行卡脱敏
SELECT CONCAT(LEFT(bank_card, 4), '********', RIGHT(bank_card, 4)) AS bank_card FROM users; -- 6230351888852405 → 6230********2405
6. 完整查询示例
-- 查询单个用户 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. 创建视图方案
如果经常要用,建个视图更方便:
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. 联表查询脱敏
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脱敏+视图),实现数据访问层面的安全隔离。
以上就是SpringBoot中实现数据脱敏的六种常用方案的详细内容,更多关于SpringBoot数据脱敏方案的资料请关注脚本之家其它相关文章!