SpringBoot常用脫敏方案小結(jié)
引言
1.1 編寫的目的
在數(shù)據(jù)脫敏功能的落地過程中,開發(fā)團隊往往面臨“選型難、對比雜、場景多”的困惑。目前系統(tǒng)梳理四種主流脫敏手段(Jackson 注解式、AOP 攔截式、DTO 手動式、數(shù)據(jù)庫sql脫敏)的核心機制、優(yōu)缺點及適用場景,幫助開發(fā)者在安全合規(guī)、研發(fā)效率、運行性能之間做出快速且正確的技術(shù)決策,同時規(guī)避常見的明文泄露、臟數(shù)據(jù)、性能陷阱等風(fēng)險,真正做到“敏感數(shù)據(jù)看不見,業(yè)務(wù)系統(tǒng)照樣轉(zhuǎn)”。
1.2 效果示意


脫敏方式介紹
具體的脫敏方向大致分為兩種
- 數(shù)據(jù)庫層面
- (應(yīng)用層)返回前端之前序列化處理
數(shù)據(jù)庫層面脫敏:
在數(shù)據(jù)庫層面進行脫敏通常意味著在 寫入數(shù)據(jù)庫之前 或 獲取數(shù)據(jù)庫數(shù)據(jù)時 對敏感數(shù)據(jù)進行處理,就像熟悉的MD5 密碼加密 也是數(shù)據(jù)庫脫敏的一種體現(xiàn)。
- 加密:對數(shù)據(jù)進行加密處理,使其在存儲時不可讀。
- 掩碼:隱藏部分?jǐn)?shù)據(jù),如電話號碼、身份證號等只顯示部分信息。
數(shù)據(jù)庫層面脫敏的優(yōu)點是可以集中管理,并且通常更安全,因為敏感數(shù)據(jù)不會被未經(jīng)處理就暴露給應(yīng)用程序。
應(yīng)用層面脫敏:
應(yīng)用層脫敏是在數(shù)據(jù)從數(shù)據(jù)庫抽出來,并且在發(fā)送給前端之前對其進行處理。這通常在業(yè)務(wù)邏輯層操作
- 脫敏對象的指定字段時調(diào)用自定義的工具類完成脫敏
3.1 Jackson + 自定義序列化器
概述
在將后端的對象返回前端之前, 會通過SpringMVC 的 默認(rèn)Jackson 序列化器來實現(xiàn)數(shù)據(jù)的json化處理, 會根據(jù)字段類型查找對應(yīng)的序列化器
利用這一擴展點,我們可以:
- 自定義一個通用脫敏序列化器(實現(xiàn)
JsonSerializer<String>+ContextualSerializer); - 在需要脫敏的字段上只加業(yè)務(wù)注解(如
@Desensitize(type = EMAIL)); - 序列化階段 Jackson 自動回調(diào)該序列化器,實時將明文替換為星號,再寫入 JSON。
全程零業(yè)務(wù)代碼侵入,規(guī)則集中維護,性能接近原生(序列化器被緩存),是對外 API 層最輕量、最統(tǒng)一的脫敏方案。
沒有注解的去選擇默認(rèn)的序列器(StringSerializer、IntSerializer...)
原理

BeanSerializer 拆包器

拓展
這種類型是以字段為粒度加注解, 那么只要在這個對象上加了注解, 就相當(dāng)于把對象的字段標(biāo)記了, 在哪調(diào)用都得走自定義的序列化邏輯
所以, 這里擴展一個 打標(biāo)簽 (標(biāo)記)可以指定字段在某個接口脫敏, 不影響該字段在其他地方的正常展示
打標(biāo)簽的含義
因為序列化是通過getter 方法來獲取字段的,所以可以在getter上做文章

addMixIn 的本質(zhì)是:把“Mixin 接口/類”上的所有注解,原封不動地嫁接給目標(biāo)實體類,但運行時仍然只出現(xiàn)目標(biāo)實體類BCompany,Mixin 本身不會被實例化也不會出現(xiàn)在 JSON 里。
public interface CompanyDesensMixin {
@Desensitize(type = DesensitizeType.EMAIL)
String getEmail();
}3.2 AOP + 標(biāo)記注解
概述
把“脫敏動作”從業(yè)務(wù)代碼里抽出來,做成一個橫切關(guān)注點;
在方法返回前端之前,立即調(diào)用統(tǒng)一脫敏處理器:通過反射按字段名遍歷返回對象,將敏感字段就地替換為星號。 使用方式只用在目標(biāo)字段上加一個自定義注解(包含脫敏類型),業(yè)務(wù)代碼零變動,即可對任意復(fù)雜嵌套對象、集合、Map 完成脫敏。
該方案以“方法”為最小粒度,一次性配置即可讓接口、日志、導(dǎo)出等多出口同時生效,但需承擔(dān) 反射 遍歷帶來的 CPU 開銷與循環(huán)引用風(fēng)險
原理

相關(guān)代碼
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Sensitive {
SensitiveType value();
}/**
* 脫敏類型枚舉
*/
public enum SensitiveType {
PHONE, // 手機號
ID_CARD, // 身份證
NAME, // 姓名
EMAIL, // 郵箱
BANK_CARD // 銀行卡
}/**
* 深度脫敏AOP處理器
*/
@Aspect
@Component
@Slf4j
public class DeepSensitiveAspect {
// 定義切點:攔截Controller層所有方法
@Pointcut("execution(* com.jing.springbootdemo.web.BasicController.user())")
public void controllerPointcut() {}
/**
* 環(huán)繞通知:處理Controller層返回結(jié)果
*/
@Around("controllerPointcut()")
public Object aroundController(ProceedingJoinPoint joinPoint) throws Throwable {
Object result = joinPoint.proceed();
long start = System.nanoTime();
Object res = processDeepSensitive(result);
long end = System.nanoTime();
long cost = TimeUnit.NANOSECONDS.toMillis(end - start);
log.info("Controller 通知總耗時 = {} ms | 簽名 = {}", cost, joinPoint.getSignature().toShortString());
return res;
}
/**
* 遞歸處理單個對象所有字段
*/
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;
}
}這里需要寫一個遞歸處理對象,Map,集合(反射). 如果是多字段/深度嵌套的對象, 多層反射
3.3 DTO 手動脫敏
概述
沒有任何注解、沒有任何框架,就是最原始的 get/set 時期的做法,最原始最靈活
代碼
public UserDTO toDTO(UserEntity entity) {
UserDTO dto = new UserDTO();
// 1. 普通字段
dto.setUserName(desensitizeName(entity.getRealName()));
// 2. 身份證號
dto.setIdCard(StrUtil.hide(entity.getIdCard(), 1, 17));
// 3. 手機
dto.setMobile(DesensitizedUtil.mobilePhone(entity.getMobile()));
// 4. 郵箱
dto.setEmail(DesensitizedUtil.email(entity.getEmail()));
return dto;
}
private String desensitizeName(String fullName) {
if (fullName == null || fullName.length() < 2) return fullName;
return fullName.charAt(0) + "*" + fullName.substring(fullName.length() - 1);
}這種靈活度最高,但是如果項目多處用到數(shù)據(jù)脫敏, 就要寫很多重復(fù)的代碼. 且及其不好維護
3.4 數(shù)據(jù)庫脫敏
概述
數(shù)據(jù)庫脫敏是指在數(shù)據(jù)離開數(shù)據(jù)庫之前,通過 SQL 內(nèi)置函數(shù)、視圖、存儲過程或商業(yè)插件,對敏感字段進行實時變形,使返回給應(yīng)用、報表或第三方接口的數(shù)據(jù)不再包含完整明文,從而防止泄露、滿足合規(guī)的一種服務(wù)端級保護手段。
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;通過SQL層面直接脫敏, SQL 復(fù)雜,維護成本高,無法動態(tài)控制
| 方式 | 層面 | 改動 | 性能 | 靈活度 |
| jackson 自定義注解 | 實現(xiàn)自定義的序列化器, 走原有springMVC的流程,無侵入脫敏只用加注解, 添加脫敏方式只需要加枚舉 | 自定義 序列化器和 注解 | 走原生,不用拷貝,不用反射, 效率高 | 以字段為粒度, 也可以通過給某個 方法里的指定字段脫敏,靈活度高 |
| aop+標(biāo)注注解 | 以反射的方式, 在脫敏的方法上添加脫敏注解 | 不修改原有的實體,只用在需要脫敏的方法上添加標(biāo)記注解 | 以反射方式進行, 遇見嵌套對象, 需要將所有字段循環(huán)反射判斷是否需要脫敏, 多字段對象,影響性能 | 以方法為粒度, 通過調(diào)用工具類來脫敏, 工具類來決定哪個字段要脫敏, 靈活度不高 |
| DTO 手動脫敏 | 代碼業(yè)務(wù)層面, 需要脫敏的位置,添加 pojo -> dto 的對象轉(zhuǎn)換 | 需要給需要轉(zhuǎn)化的對象新建dto對象 | 無反射,純手動調(diào)用脫敏工具,效率高 | 靈活度最高,但是需要大量修改原有 的代碼和編寫重復(fù)代碼, 維護成本高 |
| 數(shù)據(jù)庫層面 | 通過sql直接將數(shù)據(jù)解決按 | 需要脫敏的地方添加sql, 添加mapper | 每行通過sql的字符串截斷方法, 不能走索引 | 靈活度差, 該字段就要改sql, 需要脫敏 就要去改sql, 復(fù)用性最低 |
性能測試
綜上所述, Jackson 與 AOP 反射切入的方式 比較合理, 適用于當(dāng)前業(yè)務(wù)需求;
下面寫兩個這兩種類型的 Demo 來進行性能測試
測試結(jié)論
執(zhí)行結(jié)果

可以看到Jackson的序列化性能是AOP方式的3~4倍
安全層面
jackson方式:
- 無鎖、無并發(fā)問題
- 自定義的序列化器由springWeb自己管理
aop+標(biāo)注注解:
- 對象如果層級嵌套多/字段多,需要手動寫遞歸,占用大量??臻g
DTO 手動脫敏:
最原始,最安全(內(nèi)存全是***,無內(nèi)存殘留數(shù)據(jù))
數(shù)據(jù)庫層面脫敏:
將脫敏邏輯轉(zhuǎn)移到 SQL 查詢出口,與業(yè)務(wù)邏輯完全解耦,依賴于SQL實現(xiàn)是安全等級最高的實現(xiàn)方式。
項目場景
對于有種情況, 在整個列表頁面, 郵箱這種信息不想被一眼看到, 然后詳情信息又想看見. 對于后端的實現(xiàn), 用戶這個數(shù)據(jù)模型肯定是一份, 不可能列表 和 詳情頁面各一個數(shù)據(jù)模型(對象).
這邊肯定優(yōu)先選擇可以區(qū)分接口的方案
選擇方案
jackson+自定義注解方式
- 自定義序列化處理器,springMVC 轉(zhuǎn)化時(遇見自定義的注解) 直接調(diào)用自己的序列化處理器
- 只用在需要的脫敏字段上加注解
- 增加注解規(guī)則,只要更改注解工具
- 后期改字段名稱,直接改名稱
到此這篇關(guān)于SpringBoot常用脫敏方案小結(jié)的文章就介紹到這了,更多相關(guān)SpringBoot 常用脫敏內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Springboot敏感字段脫敏的實現(xiàn)思路
- SpringBoot?自定義注解之脫敏注解詳解
- SpringBoot使用jasypt實現(xiàn)數(shù)據(jù)庫信息脫敏的方法詳解
- Springboot+Hutool自定義注解實現(xiàn)數(shù)據(jù)脫敏
- SpringBoot實現(xiàn)返回值數(shù)據(jù)脫敏的步驟詳解
- 淺析如何在SpringBoot中實現(xiàn)數(shù)據(jù)脫敏
- SpringBoot利用自定義注解實現(xiàn)隱私數(shù)據(jù)脫敏(加密顯示)的解決方案
- SpringBoot數(shù)據(jù)脫敏的實現(xiàn)示例
- SpringBoot實現(xiàn)接口返回數(shù)據(jù)脫敏的代碼示例
- SpringBoot實現(xiàn)數(shù)據(jù)加密脫敏的示例代碼
- SpringBoot敏感數(shù)據(jù)脫敏的處理方式
相關(guān)文章
Java中的反射,枚舉及l(fā)ambda表達(dá)式的使用詳解
這篇文章主要為大家詳細(xì)介紹了Java的反射,枚舉及l(fā)ambda表達(dá)式,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下,希望能夠給你帶來幫助2022-03-03
SpringBoot根據(jù)參數(shù)動態(tài)調(diào)用接口實現(xiàn)類方法
在?Spring?Boot?開發(fā)中,我們經(jīng)常會遇到根據(jù)不同參數(shù)調(diào)用接口不同實現(xiàn)類方法的需求,本文將詳細(xì)介紹如何實現(xiàn)這一功能,有需要的小伙伴可以參考下2025-02-02
使用自定義參數(shù)解析器同一個參數(shù)支持多種Content-Type
這篇文章主要介紹了使用自定義參數(shù)解析器同一個參數(shù)支持多種Content-Type的操作,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-08-08
Java代碼實現(xiàn)對properties文件有序的讀寫的示例
本篇文章主要介紹了Java代碼實現(xiàn)對properties文件有序的讀寫的示例,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-11-11
SpringCloud通過Feign傳遞List類型參數(shù)方式
這篇文章主要介紹了SpringCloud通過Feign傳遞List類型參數(shù)方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-03-03
Java初級必看的數(shù)據(jù)類型與常量變量知識點
這篇文章主要給大家介紹了關(guān)于Java初級必看的數(shù)據(jù)類型與常量變量知識點的相關(guān)資料,需要的朋友可以參考下2023-11-11
java input 調(diào)用手機相機和本地照片上傳圖片到服務(wù)器然后壓縮的方法
今天小編就為大家分享一篇java input 實現(xiàn)調(diào)用手機相機和本地照片上傳圖片到服務(wù)器然后壓縮的方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-08-08

