SpringBoot動(dòng)態(tài)實(shí)現(xiàn)數(shù)據(jù)脫敏的實(shí)戰(zhàn)指南
一、數(shù)據(jù)脫敏:數(shù)據(jù)界的“猶抱琵琶半遮面”
想象一下這樣的場(chǎng)景:你的身份證號(hào)、手機(jī)號(hào)、銀行卡號(hào)這些隱私數(shù)據(jù),在系統(tǒng)中裸 奔——這簡(jiǎn)直比在公共場(chǎng)所穿皇帝的新衣還尷尬!數(shù)據(jù)脫敏就是給這些敏感數(shù)據(jù)穿上得體的衣服。
數(shù)據(jù)脫敏的幾種常見姿勢(shì):
- 靜態(tài)脫敏:像給照片打馬賽克,一勞永逸
- 動(dòng)態(tài)脫敏:像智能變色玻璃,看人下菜碟
- 前端脫敏:只在展示時(shí)害羞一下
- 后端脫敏:從出生就帶著面具
二、SpringBoot脫敏方案實(shí)戰(zhàn)
方案1:注解+序列化方案(給字段貼上“此處打碼”標(biāo)簽)
步驟1:先來個(gè)脫敏注解,像給敏感部位貼標(biāo)簽
import java.lang.annotation.*;
/**
* 脫敏注解:給敏感字段貼上“此處需要打碼”的標(biāo)簽
* 就像在數(shù)據(jù)身上貼了個(gè)“兒童不宜”的警示條
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Sensitive {
/**
* 脫敏類型:決定怎么打碼
*/
SensitiveType type();
}
/**
* 脫敏類型枚舉:各種打碼方式任君選擇
*/
public enum SensitiveType {
/** 中文名:張*三 */
CHINESE_NAME,
/** 身份證號(hào):110**********1234 */
ID_CARD,
/** 手機(jī)號(hào):138****1234 */
PHONE,
/** 郵箱:t***@163.com */
EMAIL,
/** 銀行卡號(hào):6217 **** **** 1234 */
BANK_CARD,
/** 地址:北京市海淀區(qū)**** */
ADDRESS
}
步驟2:實(shí)現(xiàn)脫敏序列化器,專業(yè)的“打碼師”
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import java.io.IOException;
/**
* 脫敏序列化器:專業(yè)的“馬賽克師傅”
* 負(fù)責(zé)給敏感數(shù)據(jù)穿上得體的衣服
*/
public class SensitiveSerializer extends JsonSerializer<String> {
private final SensitiveType type;
public SensitiveSerializer(SensitiveType type) {
this.type = type;
}
@Override
public void serialize(String value, JsonGenerator gen,
SerializerProvider serializers) throws IOException {
if (value == null) {
gen.writeNull();
return;
}
// 根據(jù)脫敏類型選擇不同的“打碼姿勢(shì)”
gen.writeString(maskData(value, type));
}
/**
* 核心脫敏邏輯:十八般武藝輪番上陣
*/
private String maskData(String data, SensitiveType type) {
if (data == null || data.isEmpty()) {
return data;
}
return switch (type) {
case CHINESE_NAME -> maskChineseName(data);
case ID_CARD -> maskIdCard(data);
case PHONE -> maskPhone(data);
case EMAIL -> maskEmail(data);
case BANK_CARD -> maskBankCard(data);
case ADDRESS -> maskAddress(data);
default -> data; // 默認(rèn)不脫敏,裸奔!
};
}
private String maskChineseName(String name) {
if (name.length() <= 1) return name;
if (name.length() == 2) return name.charAt(0) + "*";
return name.charAt(0) + "*" + name.charAt(name.length() - 1);
}
private String maskIdCard(String idCard) {
if (idCard.length() <= 8) return idCard;
return idCard.substring(0, 3) +
"*".repeat(Math.max(0, idCard.length() - 7)) +
idCard.substring(idCard.length() - 4);
}
private String maskPhone(String phone) {
if (phone.length() != 11) return phone;
return phone.substring(0, 3) + "****" + phone.substring(7);
}
private String maskEmail(String email) {
int atIndex = email.indexOf("@");
if (atIndex <= 1) return email;
return email.charAt(0) + "***" + email.substring(atIndex);
}
private String maskBankCard(String card) {
if (card.length() <= 8) return card;
return card.substring(0, 4) + " **** **** " +
card.substring(card.length() - 4);
}
private String maskAddress(String address) {
if (address.length() <= 4) return address;
return address.substring(0, address.length() - 4) + "****";
}
}
/**
* 注解序列化器:把注解和序列化器牽線搭橋
*/
public class SensitiveAnnotationIntrospector extends JacksonAnnotationIntrospector {
@Override
public Object findSerializer(Annotated am) {
Sensitive sensitive = am.getAnnotation(Sensitive.class);
if (sensitive != null) {
return new SensitiveSerializer(sensitive.type());
}
return super.findSerializer(am);
}
}
步驟3:配置Jackson,告訴它:“看這里,要打碼!”
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class JacksonConfig {
@Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.setAnnotationIntrospector(new SensitiveAnnotationIntrospector());
return mapper;
}
}
步驟4:在實(shí)體類上使用,貼上標(biāo)簽就自動(dòng)打碼
/**
* 用戶實(shí)體:敏感字段都穿上了“馬賽克小內(nèi)褲”
*/
@Data
public class UserDTO {
private Long id;
@Sensitive(type = SensitiveType.CHINESE_NAME)
private String username;
@Sensitive(type = SensitiveType.PHONE)
private String phone;
@Sensitive(type = SensitiveType.EMAIL)
private String email;
@Sensitive(type = SensitiveType.ID_CARD)
private String idCard;
@Sensitive(type = SensitiveType.BANK_CARD)
private String bankCard;
@Sensitive(type = SensitiveType.ADDRESS)
private String address;
// 這個(gè)字段沒注解,繼續(xù)裸奔
private String hobby;
}
步驟5:控制器測(cè)試一下效果
@RestController
@RequestMapping("/user")
public class UserController {
@GetMapping("/{id}")
public UserDTO getUser(@PathVariable Long id) {
// 模擬從數(shù)據(jù)庫(kù)查出的完整數(shù)據(jù)
UserDTO user = new UserDTO();
user.setId(id);
user.setUsername("張全蛋");
user.setPhone("13800138000");
user.setEmail("zhangquandan@example.com");
user.setIdCard("110101199001011234");
user.setBankCard("621700001234567890");
user.setAddress("北京市海淀區(qū)中關(guān)村大街1號(hào)");
user.setHobby("唱跳RAP籃球");
// 返回時(shí)自動(dòng)脫敏,就像自動(dòng)加了馬賽克
return user;
}
}
測(cè)試結(jié)果:
{
"id": 1,
"username": "張*蛋",
"phone": "138****8000",
"email": "z***@example.com",
"idCard": "110**********1234",
"bankCard": "6217 **** **** 7890",
"address": "北京市海淀區(qū)中關(guān)村大街****",
"hobby": "唱跳RAP籃球"
}
方案2:AOP切面方案(數(shù)據(jù)出門前的安檢員)
步驟1:定義脫敏策略接口
/**
* 脫敏策略:定義各種脫敏算法
* 就像不同的美顏濾鏡
*/
public interface SensitiveStrategy {
String mask(String data);
}
/**
* 策略工廠:根據(jù)類型選擇合適的濾鏡
*/
@Component
public class SensitiveStrategyFactory {
private final Map<SensitiveType, SensitiveStrategy> strategies = new HashMap<>();
public SensitiveStrategyFactory() {
// 注冊(cè)各種美顏濾鏡
strategies.put(SensitiveType.CHINESE_NAME, new ChineseNameStrategy());
strategies.put(SensitiveType.PHONE, new PhoneStrategy());
strategies.put(SensitiveType.ID_CARD, new IdCardStrategy());
// ... 其他策略
}
public SensitiveStrategy getStrategy(SensitiveType type) {
return strategies.getOrDefault(type, data -> data);
}
// 具體策略實(shí)現(xiàn)
private static class ChineseNameStrategy implements SensitiveStrategy {
@Override
public String mask(String data) {
if (data == null || data.length() <= 1) return data;
if (data.length() == 2) return data.charAt(0) + "*";
return data.charAt(0) + "*" + data.charAt(data.length() - 1);
}
}
private static class PhoneStrategy implements SensitiveStrategy {
@Override
public String mask(String data) {
if (data == null || data.length() != 11) return data;
return data.substring(0, 3) + "****" + data.substring(7);
}
}
// ... 其他策略實(shí)現(xiàn)
}
步驟2:AOP切面實(shí)現(xiàn)
@Aspect
@Component
@Slf4j
public class SensitiveAspect {
@Autowired
private SensitiveStrategyFactory strategyFactory;
/**
* 攔截所有Controller方法返回
* 就像在數(shù)據(jù)出門前設(shè)了個(gè)安檢門
*/
@Around("@annotation(org.springframework.web.bind.annotation.GetMapping) || " +
"@annotation(org.springframework.web.bind.annotation.PostMapping) || " +
"@annotation(org.springframework.web.bind.annotation.RequestMapping)")
public Object aroundController(ProceedingJoinPoint joinPoint) throws Throwable {
// 放行方法執(zhí)行
Object result = joinPoint.proceed();
// 給返回結(jié)果穿上衣服
return processSensitiveData(result);
}
/**
* 遞歸處理脫敏:連數(shù)據(jù)對(duì)象的子孫后代都不放過
*/
private Object processSensitiveData(Object obj) {
if (obj == null) return null;
// 如果是集合,給每個(gè)元素都穿上衣服
if (obj instanceof Collection) {
return processCollection((Collection<?>) obj);
}
// 如果是數(shù)組,也不放過
if (obj.getClass().isArray()) {
return processArray((Object[]) obj);
}
// 如果是Map,處理每個(gè)值
if (obj instanceof Map) {
return processMap((Map<?, ?>) obj);
}
// 如果是普通對(duì)象,深度掃描敏感字段
if (isCustomClass(obj.getClass())) {
return processObject(obj);
}
// 基本類型,直接返回
return obj;
}
private Object processObject(Object obj) {
Class<?> clazz = obj.getClass();
Object newObj;
try {
newObj = clazz.newInstance();
} catch (Exception e) {
log.warn("創(chuàng)建對(duì)象實(shí)例失敗: {}", clazz.getName());
return obj;
}
// 反射獲取所有字段
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
try {
Object value = field.get(obj);
// 如果有脫敏注解,穿上馬賽克
Sensitive sensitive = field.getAnnotation(Sensitive.class);
if (sensitive != null && value instanceof String) {
SensitiveStrategy strategy = strategyFactory.getStrategy(sensitive.type());
value = strategy.mask((String) value);
} else if (value != null) {
// 遞歸處理嵌套對(duì)象
value = processSensitiveData(value);
}
field.set(newObj, value);
} catch (Exception e) {
log.warn("處理字段 {} 失敗", field.getName(), e);
}
}
return newObj;
}
}
方案3:MyBatis攔截器方案(數(shù)據(jù)庫(kù)查詢時(shí)的美顏相機(jī))
/**
* MyBatis攔截器:在數(shù)據(jù)從數(shù)據(jù)庫(kù)出來時(shí)實(shí)時(shí)美顏
*/
@Intercepts({
@Signature(type = ResultSetHandler.class,
method = "handleResultSets",
args = {Statement.class})
})
@Component
@Slf4j
public class SensitiveInterceptor implements Interceptor {
@Autowired
private SensitiveStrategyFactory strategyFactory;
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 先執(zhí)行原方法獲取結(jié)果
Object result = invocation.proceed();
if (result == null) {
return null;
}
// 處理結(jié)果集
if (result instanceof List) {
for (Object obj : (List<?>) result) {
processObject(obj);
}
} else {
processObject(result);
}
return result;
}
private void processObject(Object obj) {
if (obj == null) return;
Class<?> clazz = obj.getClass();
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
Sensitive sensitive = field.getAnnotation(Sensitive.class);
if (sensitive != null) {
field.setAccessible(true);
try {
Object value = field.get(obj);
if (value instanceof String) {
SensitiveStrategy strategy = strategyFactory.getStrategy(sensitive.type());
String maskedValue = strategy.mask((String) value);
field.set(obj, maskedValue);
}
} catch (Exception e) {
log.error("脫敏處理失敗", e);
}
}
}
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
// 可以配置一些屬性
}
}
方案4:自定義消息轉(zhuǎn)換器方案(HTTP出口處的安檢機(jī))
/**
* 自定義HTTP消息轉(zhuǎn)換器:在數(shù)據(jù)離開系統(tǒng)前最后一道安檢
*/
@Component
public class SensitiveHttpMessageConverter extends MappingJackson2HttpMessageConverter {
@Autowired
private SensitiveStrategyFactory strategyFactory;
@Override
protected void writeInternal(Object object, Type type,
HttpOutputMessage outputMessage) throws IOException {
// 先脫敏再序列化
Object processedObject = processSensitiveData(object);
super.writeInternal(processedObject, type, outputMessage);
}
// 脫敏處理方法(同上,省略重復(fù)代碼)
private Object processSensitiveData(Object obj) {
// 實(shí)現(xiàn)同AOP方案中的processSensitiveData方法
// ...
}
}
方案5:數(shù)據(jù)庫(kù)層脫敏方案(給數(shù)據(jù)庫(kù)戴上口罩)
/**
* Hibernate事件監(jiān)聽器:數(shù)據(jù)入庫(kù)時(shí)自動(dòng)加密,出庫(kù)時(shí)自動(dòng)解密
*/
@Component
public class SensitiveEventListener implements
PostLoadEventListener, PreInsertEventListener, PreUpdateEventListener {
@Autowired
private EncryptionService encryptionService;
@Override
public void onPostLoad(PostLoadEvent event) {
Object entity = event.getEntity();
// 加載后解密
decryptEntity(entity);
}
@Override
public boolean onPreInsert(PreInsertEvent event) {
// 插入前加密
encryptEntity(event.getEntity());
return false;
}
@Override
public boolean onPreUpdate(PreUpdateEvent event) {
// 更新前加密
encryptEntity(event.getEntity());
return false;
}
private void encryptEntity(Object entity) {
if (entity == null) return;
Field[] fields = entity.getClass().getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(SensitiveEncrypt.class)) {
field.setAccessible(true);
try {
Object value = field.get(entity);
if (value instanceof String) {
String encrypted = encryptionService.encrypt((String) value);
field.set(entity, encrypted);
}
} catch (Exception e) {
log.error("加密字段失敗", e);
}
}
}
}
private void decryptEntity(Object entity) {
// 類似encryptEntity,調(diào)用encryptionService.decrypt
}
}
三、脫敏方案選擇指南:對(duì)癥下藥
1.注解+序列化方案
適用場(chǎng)景:REST API返回?cái)?shù)據(jù)脫敏
優(yōu)點(diǎn):簡(jiǎn)單優(yōu)雅,與業(yè)務(wù)解耦
缺點(diǎn):只對(duì)JSON序列化有效
2.AOP切面方案
適用場(chǎng)景:需要對(duì)Controller層統(tǒng)一處理
優(yōu)點(diǎn):集中管理,支持復(fù)雜邏輯
缺點(diǎn):性能開銷,可能誤傷
3.MyBatis攔截器方案
適用場(chǎng)景:數(shù)據(jù)庫(kù)查詢結(jié)果脫敏
優(yōu)點(diǎn):從源頭控制,一勞永逸
缺點(diǎn):影響所有查詢,不夠靈活
4.自定義消息轉(zhuǎn)換器方案
適用場(chǎng)景:全局HTTP響應(yīng)處理
優(yōu)點(diǎn):最徹底的出口控制
缺點(diǎn):可能與其他組件沖突
5.數(shù)據(jù)庫(kù)層方案
適用場(chǎng)景:存儲(chǔ)加密,展示脫敏
優(yōu)點(diǎn):最安全,防止數(shù)據(jù)泄露
缺點(diǎn):影響查詢性能,實(shí)現(xiàn)復(fù)雜
四、最佳實(shí)踐建議
1.分層防御:不要把所有雞蛋放在一個(gè)籃子里
數(shù)據(jù)安全防護(hù)體系:
存儲(chǔ)層:加密存儲(chǔ)(最后的底線)
業(yè)務(wù)層:邏輯脫敏(靈活控制)
展示層:展示脫敏(用戶體驗(yàn))
2.配置化脫敏:像調(diào)美顏強(qiáng)度一樣可配置
@Component
@ConfigurationProperties(prefix = "sensitive")
@Data
public class SensitiveProperties {
/**
* 是否開啟脫敏
*/
private boolean enabled = true;
/**
* 脫敏規(guī)則配置
*/
private Map<SensitiveType, Rule> rules = new HashMap<>();
@Data
public static class Rule {
/**
* 保留前幾位
*/
private Integer keepPrefix = 3;
/**
* 保留后幾位
*/
private Integer keepSuffix = 4;
/**
* 替換字符
*/
private Character maskChar = '*';
}
}
3.性能優(yōu)化:脫敏也要注意效率
@Component
public class SensitiveCache {
private final Cache<String, String> cache =
Caffeine.newBuilder()
.maximumSize(10000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.build();
public String maskWithCache(String data, SensitiveType type,
SensitiveStrategy strategy) {
String key = type.name() + ":" + data;
return cache.get(key, k -> strategy.mask(data));
}
}
4.監(jiān)控與日志:知道誰在什么時(shí)候脫敏
@Aspect
@Component
@Slf4j
public class SensitiveMonitorAspect {
@Around("@annotation(org.springframework.web.bind.annotation.RequestMapping)")
public Object monitorSensitive(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
Object result = joinPoint.proceed();
long cost = System.currentTimeMillis() - start;
// 記錄脫敏統(tǒng)計(jì)
log.info("脫敏處理完成,方法:{},耗時(shí):{}ms",
joinPoint.getSignature(), cost);
return result;
}
}
五、總結(jié):數(shù)據(jù)脫敏的智慧
數(shù)據(jù)脫敏就像給敏感數(shù)據(jù)穿上得體的衣服——既不能裸奔(安全風(fēng)險(xiǎn)),也不能裹成木乃伊(影響使用)。通過SpringBoot的各種方案,我們可以:
- 因地制宜:根據(jù)不同的場(chǎng)景選擇合適的脫敏方案
- 層層設(shè)防:構(gòu)建多層次的數(shù)據(jù)安全防護(hù)體系
- 靈活配置:像調(diào)節(jié)美顏相機(jī)一樣輕松調(diào)整脫敏策略
- 性能平衡:在安全和性能之間找到最佳平衡點(diǎn)
沒有一種方案是萬能的。就像穿衣服要分場(chǎng)合(泳池穿泳衣,會(huì)議室穿正裝),數(shù)據(jù)脫敏也要根據(jù)具體場(chǎng)景選擇最合適的方案。
最終目標(biāo):讓敏感數(shù)據(jù)既能保守秘密,又能履行職責(zé)。畢竟,數(shù)據(jù)的價(jià)值在于使用,而不是鎖在保險(xiǎn)柜里吃灰。脫敏就是讓數(shù)據(jù)在"安全"和"可用"之間優(yōu)雅地走鋼絲!
// 最后送大家一個(gè)萬能脫敏方法
public String universalMask(String data) {
return "****"; // 簡(jiǎn)單粗暴,但最安全?。ㄩ_玩笑的,別真用)
}
過多的脫敏會(huì)影響業(yè)務(wù),過少的脫敏又存在風(fēng)險(xiǎn)。找到那個(gè)剛剛好的平衡點(diǎn),才是數(shù)據(jù)脫敏的最高境界!
以上就是SpringBoot動(dòng)態(tài)實(shí)現(xiàn)數(shù)據(jù)脫敏的實(shí)戰(zhàn)指南的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot數(shù)據(jù)脫敏的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
全解析Spring Cloud之負(fù)載均衡之LoadBalance
這篇文章主要介紹了全解析Spring Cloud之負(fù)載均衡之LoadBalance的相關(guān)資料,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),感興趣的朋友一起看看吧2025-05-05
解決Properties屬性文件中的值有等號(hào)和換行的小問題
這篇文章主要介紹了解決Properties屬性文件中的值有等號(hào)有換行的小問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-08-08
Java ArrayList.add 的實(shí)現(xiàn)方法
這篇文章主要介紹了Java ArrayList.add 的實(shí)現(xiàn)方法,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-11-11
Java純代碼實(shí)現(xiàn)導(dǎo)出pdf
在項(xiàng)目開發(fā)中,產(chǎn)品的需求越來越奇葩啦,開始文件下載都是下載為excel的,做著做著需求竟然變了,要求能導(dǎo)出pdf,所以本文就來用Java實(shí)現(xiàn)導(dǎo)出pdf功能吧2023-12-12
Java實(shí)現(xiàn)簡(jiǎn)單的銀行管理系統(tǒng)的示例代碼
這篇文章主要介紹了如何利用Java實(shí)現(xiàn)簡(jiǎn)單的銀行管理系統(tǒng),可以實(shí)現(xiàn)存款,取款,查詢等功能,文中的示例代碼講解詳細(xì),感興趣的可以了解一下2022-09-09
Springboot啟動(dòng)后立即某個(gè)執(zhí)行方法的四種方式
spring項(xiàng)目如何在啟動(dòng)項(xiàng)目是執(zhí)行一些操作,在spring中能通過那些操作實(shí)現(xiàn)這個(gè)功能呢,下面這篇文章主要給大家介紹了關(guān)于Springboot啟動(dòng)后立即某個(gè)執(zhí)行方法的四種方式,需要的朋友可以參考下2022-06-06
Java中實(shí)現(xiàn)代碼優(yōu)化的技巧分享
這篇文章主要跟大家談?wù)剝?yōu)化這個(gè)話題,那么我們一起聊聊Java中如何實(shí)現(xiàn)代碼優(yōu)化這個(gè)問題,小編這里有幾個(gè)實(shí)用的小技巧分享給大家,需要的可以參考一下2022-08-08
Java虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)域匯總
這篇文章主要給大家介紹了關(guān)于Java虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)域的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用Java具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2019-08-08
Java 如何將前端傳來的數(shù)字轉(zhuǎn)化為日期
這篇文章主要介紹了Java 如何將前端傳來的數(shù)字轉(zhuǎn)化為日期,本文通過示例代碼給大家介紹的非常詳細(xì),需要的朋友可以參考下2023-06-06
使用@CachePut?更新數(shù)據(jù)庫(kù)和更新緩存
這篇文章主要介紹了使用@CachePut?更新數(shù)據(jù)庫(kù)和更新緩存方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-12-12

