SpringBoot中實現(xiàn)數(shù)據(jù)脫敏的六種常用方案
前言
在日常的開發(fā)開發(fā)工作中,我相信各位老鐵肯定遇到過這種需求: “手機號中間四位得用*顯示”、“身份證中間八位要隱藏”、“用戶郵箱前綴脫敏”…… 例如:
- 手機號:
13812345678→138****5678 - 身份證:
430101199003078888→430101********8888 - 姓名:
張三四→張*四 - 郵箱號:
12345678@qq.com→1234****@qq.com - 銀行卡:
6230351888852405→6230********2405
既要展示部分?jǐn)?shù)據(jù),又要保證敏感信息不泄露。這就是所謂的數(shù)據(jù)脫敏。
今天給大家分享6種我在項目中常用的脫敏方案,SpringBoot項目拿來即用,可以直接復(fù)制粘貼!
方案一:Hutool工具庫(懶人必備?。?/h2>
不想造輪子可以直接用現(xiàn)成的!
// 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;
非常簡單易懂,數(shù)據(jù)脫敏不再是問題。
但有些朋友又說了:“每寫一個接口或者每一個字段都要單獨調(diào)用,有點心累。有沒有更方便的方案?” 請看方案二。
方案三:自定義注解 + Jackson
利用Jackson的序列化機制,在返回JSON時自動對標(biāo)注了注解的字段進(jìn)行脫敏。
// 脫敏注解
@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;
}
性能好,只在序列化時生效。配置一次,全局都可以用,不影響業(yè)務(wù)邏輯。缺點是需要配置Jackson。
方案四:Lombok + 自定義Getter(輕量級替代方案)
如果你不想引入太多框架,但用了Lombok,這個方案很合適。
思路:讓Lombok不生成默認(rèn)getter,我們自己寫一個帶脫敏邏輯的getter。
1. 關(guān)閉Lombok的默認(rèn)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);
}
}
這樣只要調(diào)用user.getxx(),返回的就是脫敏后的數(shù)據(jù)。
實現(xiàn)簡單,不依賴額外組件,適合小項目。但每個字段都要手動寫getter,有點啰嗦了。
方案五:AOP切面深度脫敏(推薦)
這個是主推的方案,特別適合嵌套對象、List、Map等類型。 為什么?因為前面的方法大多只處理單層DTO。
但現(xiàn)實中經(jīng)常是:
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 + 深度遞歸反射。詳細(xì)步驟如下:
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方法
/**
* 根據(jù)類型脫敏
*/
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() {}
/**
* 環(huán)繞通知:處理Controller層返回結(jié)果
*/
@Around("controllerPointcut()")
public Object aroundController(ProceedingJoinPoint joinPoint) throws Throwable {
Object result = joinPoint.proceed();
return processDeepSensitive(result);
}
/**
* 環(huán)繞通知:處理Service層返回結(jié)果
*/
@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);
}
// 處理數(shù)組類型
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());
}
/**
* 處理數(shù)組
*/
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();
}
/**
* 返回分頁數(shù)據(jù)(自動脫敏)
*/
@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"
}
}
優(yōu)點:
- 全面支持:手機號、身份證、姓名、郵箱、銀行卡全搞定
- 深度處理:支持多層嵌套對象脫敏
- 零侵入:業(yè)務(wù)代碼無需任何修改
- 高性能:使用反射緩存,性能優(yōu)化
- 易擴展:新增脫敏類型只需擴展枚舉和工具方法
方案六:Mysql數(shù)據(jù)庫層脫敏
最簡單的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;
分字段詳細(xì)寫法
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. 創(chuàng)建視圖方案
如果經(jīng)常要用,建個視圖更方便:
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. 聯(lián)表查詢脫敏
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;
最后總結(jié)
| 方案 | 優(yōu)點 | 缺點 | 適用場景 |
|---|---|---|---|
| Hutool 工具庫 | 開箱即用,代碼簡潔,功能全 | 依賴第三方庫,靈活性有限 | 快速開發(fā)、小項目、原型階段 |
| 正則工具類 + 手動調(diào)用 | 簡單直接,無額外依賴 | 需手動調(diào)用,侵入業(yè)務(wù)代碼 | 單字段少量脫敏,輕量級應(yīng)用 |
| 自定義注解 + Jackson 序列化 | 自動化序列化脫敏,性能好 | 僅作用于 JSON 輸出,不支持嵌套對象深層脫敏 | REST API 返回數(shù)據(jù)脫敏 |
| Lombok + 自定義 Getter | 輕量,無需切面或框架 | 每個字段都要寫 getter,重復(fù)代碼多 | VO/DTO 層控制輸出,適合簡單結(jié)構(gòu) |
| AOP 切面深度脫敏(推薦) | 支持嵌套對象、集合、分頁;零侵入業(yè)務(wù);可全局生效;易擴展 | 實現(xiàn)復(fù)雜,需理解反射和 AOP | 中大型項目,復(fù)雜數(shù)據(jù)結(jié)構(gòu),統(tǒng)一脫敏治理 |
| MySQL 數(shù)據(jù)庫層脫敏 | 不依賴 Java 層,查詢即脫敏,安全隔離 | SQL 復(fù)雜,維護(hù)成本高,無法動態(tài)控制 | 報表查詢、只讀視圖、DBA 視角脫敏 |
最佳實踐建議
1.如果你是新手或者小項目起步,方案一(Hutool) 或 方案二(工具類)。
2.如果你做的是標(biāo)準(zhǔn)后端服務(wù)API,推薦方案三(Jackson 注解序列化) 結(jié)合 @Sensitive注解,在返回 JSON時自動脫敏。
3.如果你的項目結(jié)構(gòu)復(fù)雜、嵌套深、List/Map/分頁多,強烈推薦 方案五(AOP深度脫敏)!這是真正意義上的“一次配置,處處脫敏”。
4.如果你有DB權(quán)限且需要給前端/報表提供固定脫敏視圖,使用 方案六(MySQL脫敏+視圖),實現(xiàn)數(shù)據(jù)訪問層面的安全隔離。
以上就是SpringBoot中實現(xiàn)數(shù)據(jù)脫敏的六種常用方案的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot數(shù)據(jù)脫敏方案的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
MyBatis/mybatis-plus項目打印SQL的方法實現(xiàn)
SpringBoot項目中,經(jīng)常需要打印SQL語句及其參數(shù),本文就來介紹一下MyBatis/mybatis-plus項目打印SQL的方法實現(xiàn),具有一定的參考價值,感興趣的可以了解一下2024-07-07
Java+OpenCV調(diào)用攝像頭實現(xiàn)拍照功能
隨著我們對環(huán)境、Mat基本使用越來越熟練、Java Swing也逐步熟悉了起來。本文將通過OpenCV驅(qū)動攝像頭實現(xiàn)識臉和拍照功能,需要的可以參考一下2022-03-03
一文掌握J(rèn)ava 數(shù)據(jù)結(jié)構(gòu)超詳細(xì)筆記(收藏版)
本文給大家介紹Java數(shù)據(jù)結(jié)構(gòu),包括數(shù)組、鏈表、List、Set、Map及迭代器,對比了其內(nèi)存特性、操作效率和應(yīng)用場景,并介紹了哈希及沖突解決方法,感興趣的朋友跟隨小編一起看看吧2025-09-09
Spring?Boot實現(xiàn)WebSocket實時通信
本文主要介紹了Spring?Boot實現(xiàn)WebSocket實時通信,包含實現(xiàn)實時消息傳遞和群發(fā)消息等功能,具有一定的參考價值,感興趣的可以了解一下2024-05-05
SpringBoot應(yīng)用是否存在MySQL連接泄漏問題的排查方法
這篇文章主要介紹了排查SpringBoot應(yīng)用MySQL連接泄漏的方法,需檢查連接數(shù)、空閑連接,配置連接池參數(shù),確保資源關(guān)閉,并使用監(jiān)控工具如Actuator和Prometheus分析,需要的朋友可以參考下2025-06-06
Java連接Sql數(shù)據(jù)庫經(jīng)常用到的操作
這篇文章主要介紹了Java連接Sql數(shù)據(jù)庫經(jīng)常用到的操作的相關(guān)資料,需要的朋友可以參考下2016-02-02

