1 起因
最近在寫一個功能,對用戶敏感的數(shù)據(jù)進(jìn)行脫敏,在網(wǎng)上看一圈基本上都是全局范圍的,我覺得應(yīng)該更加靈活,在不同場景,不同業(yè)務(wù)下進(jìn)行脫敏更加合適。
JsonSerializer介紹就參考這位大佬的
https://juejin.cn/post/6872636051237240846
aop介紹參考這位大佬的
https://juejin.cn/post/6844903575441637390
基于 Spring Boot + MyBatis Plus + Vue & Element 實現(xiàn)的后臺管理系統(tǒng) + 用戶小程序,支持 RBAC 動態(tài)權(quán)限、多租戶、數(shù)據(jù)權(quán)限、工作流、三方登錄、支付、短信、商城等功能
- 項目地址:https://github.com/YunaiV/ruoyi-vue-pro
- 視頻教程:https://doc.iocoder.cn/video/
2 初步嘗試
枚舉類
/**
*敏感信息枚舉類
*
**/
publicenumPrivacyTypeEnum{
/**
*自定義
*/
CUSTOMER,
/**
*用戶名,張*三,李*
*/
CHINESE_NAME,
/**
*身份證號,110110********1234
*/
ID_CARD,
/**
*座機(jī)號,****1234
*/
FIXED_PHONE,
/**
*手機(jī)號,176****1234
*/
MOBILE_PHONE,
/**
*地址,北京********
*/
ADDRESS,
/**
*電子郵件,s*****o@xx.com
*/
EMAIL,
/**
*銀行卡,622202************1234
*/
BANK_CARD,
/**
*密碼,永遠(yuǎn)是******,與長度無關(guān)
*/
PASSWORD,
/**
*密鑰,永遠(yuǎn)是******,與長度無關(guān)
*/
KEY
}
注解
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})//作用于字段上
@JacksonAnnotationsInside//表示自定義自己的注解PrivacyEncrypt
@JsonSerialize(using=PrivacySerialize.class)//該注解使用序列化的方式
public@interfacePrivacyEncrypt{
/**
*脫敏數(shù)據(jù)類型,非Customer時,將忽略refixNoMaskLen和suffixNoMaskLen和maskStr
*/
PrivacyTypeEnumtype()defaultPrivacyTypeEnum.CUSTOMER;
/**
*前置不需要打碼的長度
*/
intprefixNoMaskLen()default0;
/**
*后置不需要打碼的長度
*/
intsuffixNoMaskLen()default0;
/**
*用什么打碼
*/
StringmaskStr()default"*";
}
序列化類
publicclassPrivacySerializeextendsJsonSerializer<String>implementsContextualSerializer{
publicstaticfinalLoggerlogger=LoggerFactory.getLogger(PrivacySerialize.class);
privatePrivacyTypeEnumtype;
privateIntegerprefixNoMaskLen;
privateIntegersuffixNoMaskLen;
privateStringmaskStr;
publicPrivacySerialize(PrivacyTypeEnumtype,IntegerprefixNoMaskLen,IntegersuffixNoMaskLen,StringmaskStr){
this.type=type;
this.prefixNoMaskLen=prefixNoMaskLen;
this.suffixNoMaskLen=suffixNoMaskLen;
this.maskStr=maskStr;
}
publicPrivacySerialize(){
}
@Override
publicvoidserialize(Stringorigin,JsonGeneratorjsonGenerator,SerializerProviderserializerProvider)throwsIOException{
if(StringUtils.isNotBlank(origin)&&null!=type){
switch(type){
caseCHINESE_NAME:
jsonGenerator.writeString(DesensitizedUtils.chineseName(origin));
break;
caseID_CARD:
jsonGenerator.writeString(DesensitizedUtils.idCardNum(origin));
break;
caseFIXED_PHONE:
jsonGenerator.writeString(DesensitizedUtils.fixedPhone(origin));
break;
caseMOBILE_PHONE:
jsonGenerator.writeString(DesensitizedUtils.mobilePhone(origin));
break;
caseADDRESS:
jsonGenerator.writeString(DesensitizedUtils.address(origin));
break;
caseEMAIL:
jsonGenerator.writeString(DesensitizedUtils.email(origin));
break;
caseBANK_CARD:
jsonGenerator.writeString(DesensitizedUtils.bankCard(origin));
break;
casePASSWORD:
jsonGenerator.writeString(DesensitizedUtils.password(origin));
break;
caseKEY:
jsonGenerator.writeString(DesensitizedUtils.key(origin));
break;
caseCUSTOMER:
jsonGenerator.writeString(DesensitizedUtils.desValue(origin,prefixNoMaskLen,suffixNoMaskLen,maskStr));
break;
default:
thrownewIllegalArgumentException("Unknowsensitivetypeenum"+type);
}
}else{
jsonGenerator.writeString("");
}
}
@Override
publicJsonSerializer>createContextual(SerializerProviderserializerProvider,BeanPropertybeanProperty)throwsJsonMappingException{
if(beanProperty!=null){
if(Objects.equals(beanProperty.getType().getRawClass(),String.class)){
PrivacyEncryptencrypt=beanProperty.getAnnotation(PrivacyEncrypt.class);
if(encrypt==null){
encrypt=beanProperty.getContextAnnotation(PrivacyEncrypt.class);
}
if(encrypt!=null){
returnnewPrivacySerialize(encrypt.type(),encrypt.prefixNoMaskLen(),
encrypt.suffixNoMaskLen(),encrypt.maskStr());
}
}
returnserializerProvider.findValueSerializer(beanProperty.getType(),beanProperty);
}
returnserializerProvider.findNullValueSerializer(null);
}
}
脫敏工具類
/**
*脫敏工具類
*
**/
publicclassDesensitizedUtils{
/**
*對字符串進(jìn)行脫敏操作
*@paramorigin原始字符串
*@paramprefixNoMaskLen左側(cè)需要保留幾位明文字段
*@paramsuffixNoMaskLen右側(cè)需要保留幾位明文字段
*@parammaskStr用于遮罩的字符串,如'*'
*@return脫敏后結(jié)果
*/
publicstaticStringdesValue(Stringorigin,intprefixNoMaskLen,intsuffixNoMaskLen,StringmaskStr){
if(origin==null){
returnnull;
}
StringBuildersb=newStringBuilder();
for(inti=0,n=origin.length();iif(icontinue;
}
if(i>(n-suffixNoMaskLen-1)){
sb.append(origin.charAt(i));
continue;
}
sb.append(maskStr);
}
returnsb.toString();
}
/**
*【中文姓名】只顯示最后一個漢字,其他隱藏為星號,比如:**夢
*@paramfullName姓名
*@return結(jié)果
*/
publicstaticStringchineseName(StringfullName){
if(fullName==null){
returnnull;
}
returndesValue(fullName,0,1,"*");
}
/**
*【身份證號】顯示前六位,四位,其他隱藏。共計18位或者15位,比如:340304*******1234
*@paramid身份證號碼
*@return結(jié)果
*/
publicstaticStringidCardNum(Stringid){
returndesValue(id,6,4,"*");
}
/**
*【固定電話】后四位,其他隱藏,比如****1234
*@paramnum固定電話
*@return結(jié)果
*/
publicstaticStringfixedPhone(Stringnum){
returndesValue(num,0,4,"*");
}
/**
*【手機(jī)號碼】前三位,后四位,其他隱藏,比如135****6810
*@paramnum手機(jī)號碼
*@return結(jié)果
*/
publicstaticStringmobilePhone(Stringnum){
returndesValue(num,3,4,"*");
}
/**
*【地址】只顯示到地區(qū),不顯示詳細(xì)地址,比如:北京市海淀區(qū)****
*@paramaddress地址
*@return結(jié)果
*/
publicstaticStringaddress(Stringaddress){
returndesValue(address,6,0,"*");
}
/**
*【電子郵箱郵箱前綴僅顯示第一個字母,前綴其他隱藏,用星號代替,@及后面的地址顯示,比如:d**@126.com
*@paramemail電子郵箱
*@return結(jié)果
*/
publicstaticStringemail(Stringemail){
returnemail.replaceAll("(w?)(w+)(w)(@w+.[a-z]+(.[a-z]+)?)","$1****$3$4");
}
/**
*【銀行卡號】前六位,后四位,其他用星號隱藏每位1個星號,比如:622260**********1234
*@paramcardNum銀行卡號
*@return結(jié)果
*/
publicstaticStringbankCard(StringcardNum){
returndesValue(cardNum,6,4,"*");
}
/**
*【密碼】密碼的全部字符都用*代替,比如:******
*@parampassword密碼
*@return結(jié)果
*/
publicstaticStringpassword(Stringpassword){
if(password==null){
returnnull;
}
return"******";
}
/**
*【密鑰】密鑰除了最后三位,全部都用*代替,比如:***xdS脫敏后長度為6,如果明文長度不足三位,則按實際長度顯示,剩余位置補(bǔ)*
*@paramkey密鑰
*@return結(jié)果
*/
publicstaticStringkey(Stringkey){
if(key==null){
returnnull;
}
intviewLength=6;
StringBuildertmpKey=newStringBuilder(desValue(key,0,3,"*"));
if(tmpKey.length()>viewLength){
returntmpKey.substring(tmpKey.length()-viewLength);
}
elseif(tmpKey.length()intbuffLength=viewLength-tmpKey.length();
for(inti=0;i0,"*");
}
returntmpKey.toString();
}
else{
returntmpKey.toString();
}
}
}
注解使用
的確實現(xiàn)了數(shù)據(jù)脫敏,但是有個問題現(xiàn)在的脫敏針對的是 只要對該實體類進(jìn)行了使用返回的接口,中的數(shù)據(jù)都會進(jìn)行脫敏,在有些場景下是不需要的,所以說要進(jìn)行改進(jìn)。
基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 實現(xiàn)的后臺管理系統(tǒng) + 用戶小程序,支持 RBAC 動態(tài)權(quán)限、多租戶、數(shù)據(jù)權(quán)限、工作流、三方登錄、支付、短信、商城等功能
3 第二版改進(jìn)
我的思路是在該實體類中在繼承一個 父類其中定義一個字段,使其作為是否進(jìn)行脫敏的開關(guān),并且該實體類字段不參與序列化 脫敏控制類
publicclassDataMaskKeyimplementsSerializable{
//不進(jìn)行序列化,設(shè)置key來進(jìn)行過濾的把控,默認(rèn)不開啟
privatetransientBooleanisPrivacyKey=false;
publicBooleangetPrivacyKey(){
returnisPrivacyKey;
}
publicvoidsetPrivacyKey(BooleanprivacyKey){
isPrivacyKey=privacyKey;
}
}
更新之后的序列化類
思路就是通過反射獲取,該成員的屬性,因為不知道會繼承多少,所以要進(jìn)行遞歸查找需要的字段
publicclassPrivacySerializeextendsJsonSerializer<String>implementsContextualSerializer{
publicstaticfinalLoggerlogger=LoggerFactory.getLogger(PrivacySerialize.class);
privatePrivacyTypeEnumtype;
privateIntegerprefixNoMaskLen;
privateIntegersuffixNoMaskLen;
privateStringmaskStr;
publicPrivacySerialize(PrivacyTypeEnumtype,IntegerprefixNoMaskLen,IntegersuffixNoMaskLen,StringmaskStr){
this.type=type;
this.prefixNoMaskLen=prefixNoMaskLen;
this.suffixNoMaskLen=suffixNoMaskLen;
this.maskStr=maskStr;
}
publicPrivacySerialize(){
}
@Override
publicvoidserialize(Stringorigin,JsonGeneratorjsonGenerator,SerializerProviderserializerProvider)throwsIOException{
booleanflag=false;
//反射獲取對象
ObjectcurrentValue=jsonGenerator.getOutputContext().getCurrentValue();
//反射獲取class
Class>aClass=jsonGenerator.getOutputContext().getCurrentValue().getClass();
ListfieldList=getFieldList(aClass);
for(Fieldfield:fieldList){
//開始反射獲取
Stringname=field.getName();
if("isPrivacyKey".equals(name)){
try{
//進(jìn)行重新賦值
flag=(boolean)field.get(currentValue);
}catch(IllegalAccessExceptione){
e.printStackTrace();
}
}
}
//反射進(jìn)行進(jìn)行開關(guān)判定
if(flag){
//logger.info("進(jìn)行脫敏處理");
if(StringUtils.isNotBlank(origin)&&null!=type){
switch(type){
caseCHINESE_NAME:
jsonGenerator.writeString(DesensitizedUtils.chineseName(origin));
break;
caseID_CARD:
jsonGenerator.writeString(DesensitizedUtils.idCardNum(origin));
break;
caseFIXED_PHONE:
jsonGenerator.writeString(DesensitizedUtils.fixedPhone(origin));
break;
caseMOBILE_PHONE:
jsonGenerator.writeString(DesensitizedUtils.mobilePhone(origin));
break;
caseADDRESS:
jsonGenerator.writeString(DesensitizedUtils.address(origin));
break;
caseEMAIL:
jsonGenerator.writeString(DesensitizedUtils.email(origin));
break;
caseBANK_CARD:
jsonGenerator.writeString(DesensitizedUtils.bankCard(origin));
break;
casePASSWORD:
jsonGenerator.writeString(DesensitizedUtils.password(origin));
break;
caseKEY:
jsonGenerator.writeString(DesensitizedUtils.key(origin));
break;
caseCUSTOMER:
jsonGenerator.writeString(DesensitizedUtils.desValue(origin,prefixNoMaskLen,suffixNoMaskLen,maskStr));
break;
default:
thrownewIllegalArgumentException("Unknowsensitivetypeenum"+type);
}
}else{
jsonGenerator.writeString("");
}
}else{
//logger.info("不進(jìn)行脫敏處理");
jsonGenerator.writeString(origin);
}
}
@Override
publicJsonSerializer>createContextual(SerializerProviderserializerProvider,BeanPropertybeanProperty)throwsJsonMappingException{
if(beanProperty!=null){
if(Objects.equals(beanProperty.getType().getRawClass(),String.class)){
PrivacyEncryptencrypt=beanProperty.getAnnotation(PrivacyEncrypt.class);
if(encrypt==null){
encrypt=beanProperty.getContextAnnotation(PrivacyEncrypt.class);
}
if(encrypt!=null){
returnnewPrivacySerialize(encrypt.type(),encrypt.prefixNoMaskLen(),
encrypt.suffixNoMaskLen(),encrypt.maskStr());
}
}
returnserializerProvider.findValueSerializer(beanProperty.getType(),beanProperty);
}
returnserializerProvider.findNullValueSerializer(null);
}
privateListgetFieldList(Class>clazz) {
if(null==clazz){
returnnull;
}
ListfieldList=newArrayList<>();
//遞歸查找需求的字段
Class>aClass=ClassRecursionUtils.getClass(clazz,"isPrivacyKey");
Field[]declaredFields=aClass.getDeclaredFields();
for(Fieldfield:declaredFields){
if(field!=null){
//設(shè)置屬性的可訪問性
field.setAccessible(true);
//過濾靜態(tài)
if(Modifier.isStatic(field.getModifiers())){
continue;
}
Stringname=field.getName();
//過濾非布爾類型
Class>type=field.getType();
//并且只添加isPrivacyKey
if(type.isAssignableFrom(Boolean.class)&&"isPrivacyKey".equals(name)){
fieldList.add(field);
}
}
}
returnfieldList;
}
}
遞歸工具類
publicclassClassRecursionUtils{
publicstaticClass>getClass(Class>c,StringfieldName){
if(c!=null&&!hasField(c,fieldName)){
returngetClass(c.getSuperclass(),fieldName);
}
returnc;
}
publicstaticbooleanhasField(Class>c,StringfieldName){
Field[]fields=c.getDeclaredFields();
for(Fieldf:fields){
if(fieldName.equals(f.getName())){
returntrue;
}
}
returnfalse;
}
}
現(xiàn)在只需要在進(jìn)行實體類 封裝數(shù)據(jù)時,在進(jìn)行手動set即可
4 最終方案
在上述情況下可以實現(xiàn) 手動控制是否在某些場景下的脫敏,但是需要對原來的代碼進(jìn)行修改,我覺得不友好,所以采用aop的形式進(jìn)行控制
項目的返回類型基本上為兩種
- 實體類作為返回
- 分頁返回
aop注解
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})//作用于方法上
public@interfacePrivacyKeyAnnotation{
/**
*是否啟用序列化脫敏默認(rèn)開啟
*/
booleanisKey()defaulttrue;
/**
*是否為PageInfo>(分頁對象)
*/
booleanisPageKey()defaultfalse;
}
aop類
@Component
@Aspect
publicclassPrivacyKeyAspect{
publicstaticfinalLoggerlogger=LoggerFactory.getLogger(PrivacyKeyAspect.class);
/**
*@Description:環(huán)繞通知包含此注解的
*@param:ProceedingJoinPointjoinPoint
*@return:Object
*/
@Around(value="@annotation("aop注解地址xxxxx")")
publicObjectrepeatSub(ProceedingJoinPointjoinPoint)throwsThrowable{
returnjoinPoint.proceed();
}
/**
*@Description:后置通知
*/
@AfterReturning(value="@annotation("aop注解地址")",returning="result")
publicvoidsetPrivacyKeyType(JoinPointjoinPoint,Objectresult)throwsThrowable{
//進(jìn)行注解值獲取
MethodSignaturesignature=(MethodSignature)joinPoint.getSignature();
Methodmethod=signature.getMethod();
//是否開啟脫敏
booleanflag=method.getDeclaredAnnotation(PrivacyKeyAnnotation.class).isKey();
//是否對分頁進(jìn)行脫敏
booleanstatus=method.getDeclaredAnnotation(PrivacyKeyAnnotation.class).isPageKey();
if(!status){
//進(jìn)行返回值反射
Class>aClass=ClassRecursionUtils.getClass(result.getClass(),"isPrivacyKey");
if(null!=aClass){
setFieldMethod(result,flag,aClass);
}
}else{
//反射分頁page
//反射list類型
Parameter[]parameters=signature.getMethod().getParameters();
//泛型名稱
Stringname=parameters[0].getName();
//泛型class
Class>type=parameters[0].getType();
//包名
StringtypeName=type.getName();
PropertyDescriptor[]ps=Introspector.getBeanInfo(result.getClass(),Object.class).getPropertyDescriptors();
for(PropertyDescriptorprop:ps){
if(prop.getPropertyType().isAssignableFrom(List.class)){//List集合類型
Objectobj=result.getClass().getMethod(prop.getReadMethod().getName()).invoke(result);
if(obj!=null){
List>listObj=(List>)obj;
for(Objectnext:listObj){
Class>classObj=Class.forName(typeName);
//獲取成員變量
Class>keyClass=ClassRecursionUtils.getClass(classObj,"isPrivacyKey");
setFieldMethod(next,flag,keyClass);
}
}
}
}
}
}
/**
*內(nèi)容填充
*/
privatevoidsetFieldMethod(Objectresult,booleanflag,Class>aClass)throwsIllegalAccessException{
Field[]declaredFields=aClass.getDeclaredFields();
for(Fieldfield:declaredFields){
//設(shè)置屬性的可訪問性
field.setAccessible(true);
//只獲取isPrivacyKey
Stringname=field.getName();
//過濾非布爾類型
Class>type=field.getType();
//并且只添加isPrivacyKey
if(type.isAssignableFrom(Boolean.class)&&"isPrivacyKey".equals(name)){
//重新寫入
field.set(result,flag);
}
}
}
}
使用 在service implement類方法上寫入
最后,另一種實現(xiàn)方式,可以參考:
https://juejin.cn/post/7242145254057410615
-
AOP
+關(guān)注
關(guān)注
0文章
40瀏覽量
11090 -
數(shù)據(jù)權(quán)限
+關(guān)注
關(guān)注
0文章
4瀏覽量
6079 -
SpringBoot
+關(guān)注
關(guān)注
0文章
173瀏覽量
169
原文標(biāo)題:SpringBoot 采用 JsonSerializer 和 Aop 實現(xiàn)可控制的數(shù)據(jù)脫敏
文章出處:【微信號:芋道源碼,微信公眾號:芋道源碼】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論