SpringBoot實(shí)現(xiàn)返回值數(shù)據(jù)脫敏的步驟詳解
介紹
SpringBoot實(shí)現(xiàn)返回數(shù)據(jù)脫敏
有時,敏感數(shù)據(jù)返回時,需要進(jìn)行隱藏處理,但是如果一個字段一個字段的進(jìn)行硬編碼處理的話,不僅增加了工作量,而且后期需求變動的時候,更加是地獄般的工作量變更。
下面,通過身份證,姓名,密碼,手機(jī)號等等示例去演示脫敏的流程,當(dāng)然你也可以在此基礎(chǔ)上添加自己的實(shí)現(xiàn)方式
原理
- 項(xiàng)目使用的是SpringBoot,所以需要在序列化的時候,進(jìn)行脫敏處理,springboot內(nèi)置的序列化工具為jackson
- 通過實(shí)現(xiàn)com.fasterxml.jackson.databind.JsonSerializer進(jìn)行自定義序列化
- 通過重寫com.fasterxml.jackson.databind.ser.ContextualSerializer.createContextual獲取自定義注解的信息
實(shí)現(xiàn)
自定義注解類
@Target(ElementType.FIELD) //作用于字段上
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside // 表示自定義自己的注解Sensitive
@JsonSerialize(using = SensitiveInfoSerialize.class) // 該注解使用序列化的方式
public @interface Sensitive {
SensitizedType value();
}創(chuàng)建脫敏字段類型枚舉
public enum SensitizedType {
/**
* 用戶id
*/
USER_ID,
/**
* 中文名
*/
CHINESE_NAME,
/**
* 身份證號
*/
ID_CARD,
/**
* 座機(jī)號
*/
FIXED_PHONE,
/**
* 手機(jī)號
*/
MOBILE_PHONE,
/**
* 地址
*/
ADDRESS,
/**
* 電子郵件
*/
EMAIL,
/**
* 密碼
*/
PASSWORD,
/**
* 中國大陸車牌,包含普通車輛、新能源車輛
*/
CAR_LICENSE,
/**
* 銀行卡
*/
BANK_CARD,
/**
* IPv4地址
*/
IPV4,
/**
* IPv6地址
*/
IPV6,
/**
* 定義了一個first_mask的規(guī)則,只顯示第一個字符。
*/
FIRST_MASK
}脫敏工具類
import cn.hutool.core.util.CharUtil;
import cn.hutool.core.util.StrUtil;
/**
* @Auther: wu
* @Date: 2023/7/11
* @Description: com.wu.demo.common.my_sensitive
*/
public class SensitizedUtil {
public static String desensitized(CharSequence str, SensitizedType desensitizedType) {
if (StrUtil.isBlank(str)) {
return StrUtil.EMPTY;
}
String newStr = String.valueOf(str);
switch (desensitizedType) {
case USER_ID:
newStr = String.valueOf(userId());
break;
case CHINESE_NAME:
newStr = chineseName(String.valueOf(str));
break;
case ID_CARD:
newStr = idCardNum(String.valueOf(str), 3, 4);
break;
case FIXED_PHONE:
newStr = fixedPhone(String.valueOf(str));
break;
case MOBILE_PHONE:
newStr = mobilePhone(String.valueOf(str));
break;
case ADDRESS:
newStr = address(String.valueOf(str), 8);
break;
case EMAIL:
newStr = email(String.valueOf(str));
break;
case PASSWORD:
newStr = password(String.valueOf(str));
break;
case CAR_LICENSE:
newStr = carLicense(String.valueOf(str));
break;
case BANK_CARD:
newStr = bankCard(String.valueOf(str));
break;
case IPV4:
newStr = ipv4(String.valueOf(str));
break;
case IPV6:
newStr = ipv6(String.valueOf(str));
break;
case FIRST_MASK:
newStr = firstMask(String.valueOf(str));
break;
default:
}
return newStr;
}
/**
* 【用戶id】不對外提供userId
*
* @return 脫敏后的主鍵
*/
public static Long userId() {
return 0L;
}
/**
* 定義了一個first_mask的規(guī)則,只顯示第一個字符。<br>
* 脫敏前:123456789;脫敏后:1********。
*
* @param str 字符串
* @return 脫敏后的字符串
*/
public static String firstMask(String str) {
if (StrUtil.isBlank(str)) {
return StrUtil.EMPTY;
}
return StrUtil.hide(str, 1, str.length());
}
/**
* 【中文姓名】只顯示第一個漢字,其他隱藏為2個星號,比如:李**
*
* @param fullName 姓名
* @return 脫敏后的姓名
*/
public static String chineseName(String fullName) {
return firstMask(fullName);
}
/**
* 【身份證號】前1位 和后2位
*
* @param idCardNum 身份證
* @param front 保留:前面的front位數(shù);從1開始
* @param end 保留:后面的end位數(shù);從1開始
* @return 脫敏后的身份證
*/
public static String idCardNum(String idCardNum, int front, int end) {
//身份證不能為空
if (StrUtil.isBlank(idCardNum)) {
return StrUtil.EMPTY;
}
//需要截取的長度不能大于身份證號長度
if ((front + end) > idCardNum.length()) {
return StrUtil.EMPTY;
}
//需要截取的不能小于0
if (front < 0 || end < 0) {
return StrUtil.EMPTY;
}
return StrUtil.hide(idCardNum, front, idCardNum.length() - end);
}
/**
* 【固定電話 前四位,后兩位
*
* @param num 固定電話
* @return 脫敏后的固定電話;
*/
public static String fixedPhone(String num) {
if (StrUtil.isBlank(num)) {
return StrUtil.EMPTY;
}
return StrUtil.hide(num, 4, num.length() - 2);
}
/**
* 【手機(jī)號碼】前三位,后4位,其他隱藏,比如135****2210
*
* @param num 移動電話;
* @return 脫敏后的移動電話;
*/
public static String mobilePhone(String num) {
if (StrUtil.isBlank(num)) {
return StrUtil.EMPTY;
}
return StrUtil.hide(num, 3, num.length() - 4);
}
/**
* 【地址】只顯示到地區(qū),不顯示詳細(xì)地址,比如:北京市海淀區(qū)****
*
* @param address 家庭住址
* @param sensitiveSize 敏感信息長度
* @return 脫敏后的家庭地址
*/
public static String address(String address, int sensitiveSize) {
if (StrUtil.isBlank(address)) {
return StrUtil.EMPTY;
}
int length = address.length();
return StrUtil.hide(address, length - sensitiveSize, length);
}
/**
* 【電子郵箱】郵箱前綴僅顯示第一個字母,前綴其他隱藏,用星號代替,@及后面的地址顯示,比如:d**@126.com
*
* @param email 郵箱
* @return 脫敏后的郵箱
*/
public static String email(String email) {
if (StrUtil.isBlank(email)) {
return StrUtil.EMPTY;
}
int index = StrUtil.indexOf(email, '@');
if (index <= 1) {
return email;
}
return StrUtil.hide(email, 1, index);
}
/**
* 【密碼】密碼的全部字符都用*代替,比如:******
*
* @param password 密碼
* @return 脫敏后的密碼
*/
public static String password(String password) {
if (StrUtil.isBlank(password)) {
return StrUtil.EMPTY;
}
return StrUtil.repeat('*', password.length());
}
/**
* 【中國車牌】車牌中間用*代替
* eg1:null -》 ""
* eg1:"" -》 ""
* eg3:蘇D40000 -》 蘇D4***0
* eg4:陜A12345D -》 陜A1****D
* eg5:京A123 -》 京A123 如果是錯誤的車牌,不處理
*
* @param carLicense 完整的車牌號
* @return 脫敏后的車牌
*/
public static String carLicense(String carLicense) {
if (StrUtil.isBlank(carLicense)) {
return StrUtil.EMPTY;
}
// 普通車牌
if (carLicense.length() == 7) {
carLicense = StrUtil.hide(carLicense, 3, 6);
} else if (carLicense.length() == 8) {
// 新能源車牌
carLicense = StrUtil.hide(carLicense, 3, 7);
}
return carLicense;
}
/**
* 銀行卡號脫敏
* eg: 1101 **** **** **** 3256
*
* @param bankCardNo 銀行卡號
* @return 脫敏之后的銀行卡號
* @since 5.6.3
*/
public static String bankCard(String bankCardNo) {
if (StrUtil.isBlank(bankCardNo)) {
return bankCardNo;
}
bankCardNo = StrUtil.trim(bankCardNo);
if (bankCardNo.length() < 9) {
return bankCardNo;
}
final int length = bankCardNo.length();
final int midLength = length - 8;
final StringBuilder buf = new StringBuilder();
buf.append(bankCardNo, 0, 4);
for (int i = 0; i < midLength; ++i) {
if (i % 4 == 0) {
buf.append(CharUtil.SPACE);
}
buf.append('*');
}
buf.append(CharUtil.SPACE).append(bankCardNo, length - 4, length);
return buf.toString();
}
/**
* IPv4脫敏,如:脫敏前:192.0.2.1;脫敏后:192.*.*.*。
*
* @param ipv4 IPv4地址
* @return 脫敏后的地址
*/
public static String ipv4(String ipv4) {
return StrUtil.subBefore(ipv4, '.', false) + ".*.*.*";
}
/**
* IPv4脫敏,如:脫敏前:2001:0db8:86a3:08d3:1319:8a2e:0370:7344;脫敏后:2001:*:*:*:*:*:*:*
*
* @param ipv6 IPv4地址
* @return 脫敏后的地址
*/
public static String ipv6(String ipv6) {
return StrUtil.subBefore(ipv6, ':', false) + ":*:*:*:*:*:*:*";
}
}上述枚舉類和脫敏工具類,我使用了hutool中的代碼,如果hutool滿足你的需求,可以直接把上述自定義注解類和自定義序列化類使用到的SensitizedType類直接替換為hutool中的cn.hutool.core.util.DesensitizedUtil.DesensitizedType的枚舉類,
添加自定義序列化實(shí)現(xiàn)類
public class SensitiveInfoSerialize extends JsonSerializer<String> implements ContextualSerializer {
private SensitizedType sensitizedType;
/**
* 步驟一
* 方法來源于ContextualSerializer,獲取屬性上的注解屬性,同時返回一個合適的序列化器
*/
@Override
public JsonSerializer<?> createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) throws JsonMappingException {
// 獲取自定義注解
Sensitive annotation = beanProperty.getAnnotation(Sensitive.class);
// 注解不為空,且標(biāo)注的字段為String
if(Objects.nonNull(annotation) && Objects.equals(String.class, beanProperty.getType().getRawClass())){
this.sensitizedType = annotation.value();
//自定義情況,返回本序列化器,將順利進(jìn)入到該類中的serialize方法中
return this;
}
// 注解為空,字段不為String,尋找合適的序列化器進(jìn)行處理
return serializerProvider.findValueSerializer(beanProperty.getType(), beanProperty);
}
/**
* 步驟二
* 方法來源于JsonSerializer<String>:指定返回類型為String類型,serialize()將修改后的數(shù)據(jù)返回
*/
@Override
public void serialize(String str, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
if(Objects.isNull(sensitizedType)){
// 定義策略為空,返回原字符串
jsonGenerator.writeString(str);
}else {
// 定義策略不為空,返回策略處理過的字符串
jsonGenerator.writeString(SensitizedUtil.desensitized(str,sensitizedType));
}
}
}測試驗(yàn)證
在需要的脫敏的實(shí)體類字段上加上相應(yīng)的注解
@Data
public class SensitiveBody {
private String name;
@Sensitive(SensitizedType.MOBILE_PHONE)
private String mobile;
@Sensitive(SensitizedType.ID_CARD)
private String idCard;
} @ApiOperation(value = "脫敏測試處理")
@GetMapping("sensitiveTest")
public AjaxResult sensitiveTest(){
SensitiveBody body = new SensitiveBody();
body.setMobile("13041064026");
body.setIdCard("411126189912355689");
body.setName("Tom");
return AjaxResult.success(body);
}
到此這篇關(guān)于SpringBoot實(shí)現(xiàn)返回值數(shù)據(jù)脫敏的步驟詳解的文章就介紹到這了,更多相關(guān)SpringBoot返回值數(shù)據(jù)脫敏內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Springboot敏感字段脫敏的實(shí)現(xiàn)思路
- SpringBoot?自定義注解之脫敏注解詳解
- SpringBoot使用jasypt實(shí)現(xiàn)數(shù)據(jù)庫信息脫敏的方法詳解
- Springboot+Hutool自定義注解實(shí)現(xiàn)數(shù)據(jù)脫敏
- 淺析如何在SpringBoot中實(shí)現(xiàn)數(shù)據(jù)脫敏
- SpringBoot利用自定義注解實(shí)現(xiàn)隱私數(shù)據(jù)脫敏(加密顯示)的解決方案
- SpringBoot數(shù)據(jù)脫敏的實(shí)現(xiàn)示例
- SpringBoot實(shí)現(xiàn)接口返回數(shù)據(jù)脫敏的代碼示例
- SpringBoot實(shí)現(xiàn)數(shù)據(jù)加密脫敏的示例代碼
- SpringBoot敏感數(shù)據(jù)脫敏的處理方式
- SpringBoot常用脫敏方案小結(jié)
相關(guān)文章
一行命令實(shí)現(xiàn)Java項(xiàng)目啟動停止和重啟方式
文章介紹了一個支持啟動、停止和重啟項(xiàng)目的功能,默認(rèn)不入?yún)?同時允許用戶自定義Java路徑和JVM參數(shù),并可通過腳本進(jìn)行操作2025-09-09
SpringCloud對服務(wù)內(nèi)某個client進(jìn)行單獨(dú)配置的操作步驟
我們的微服務(wù)項(xiàng)目用的是springCloud,某個微服務(wù)接口因?yàn)閿?shù)據(jù)處理量大,出現(xiàn)了接口超時的情況,我們需要單獨(dú)修改這一個feignClient的超時時間,所以本文介紹了SpringCloud對服務(wù)內(nèi)某個client進(jìn)行單獨(dú)配置的操作步驟,需要的朋友可以參考下2023-10-10
Java字符串拼接的五種方法及性能比較分析(從執(zhí)行100次到90萬次)
字符串拼接一般使用“+”,但是“+”不能滿足大批量數(shù)據(jù)的處理,Java中有以下五種方法處理字符串拼接及性能比較分析,感興趣的可以了解一下2021-12-12
關(guān)于Poi讀取Excel引發(fā)內(nèi)存溢出問題的解決方法
這篇文章主要給大家介紹了關(guān)于Poi讀取Excel引發(fā)內(nèi)存溢出問題的解決方法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面跟著小編來一起學(xué)習(xí)學(xué)習(xí)吧。2017-08-08
解決SpringBoot啟動報錯:Failed?to?load?property?source?from?l
這篇文章主要介紹了解決SpringBoot啟動報錯:Failed?to?load?property?source?from?location?'classpath:/application.yml'問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2025-04-04
Spring?Boot開發(fā)RESTful接口與http協(xié)議狀態(tài)表述
這篇文章主要為大家介紹了Spring?Boot開發(fā)RESTful接口與http協(xié)議狀態(tài)表述,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步2022-03-03
SpringBoot實(shí)現(xiàn)Excel異步導(dǎo)出的完整實(shí)戰(zhàn)方案
在企業(yè)級后端系統(tǒng)中,Excel導(dǎo)出是一個幾乎繞不過去的功能點(diǎn),無論是報表系統(tǒng)還是后臺管理系統(tǒng),下面小編就為大家介紹一個SpringBoot異步導(dǎo)出Excel的完整實(shí)戰(zhàn)方案吧2025-11-11

