自定义Json注解实现输出日志字段脱敏
背景
在日志输出的时候,有时会输出一些用户的敏感信息,如手机号,身份证号,银行卡号等,现需要对这些信息在日志输出的时候进行脱敏处理
思路
使用fastjson的ValueFilter对带有自定义注解的字段进行过滤
public enum SensitiveType {
ID_CARD,
BANK_CARD,
PHONE
}
@Target({ElementType.TYPE, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface SensitiveInfo {
SensitiveType type();
}
public class SensitiveInfoUtils {
public static String toJsonString(Object object) {
return JSON.toJSONString(object, getValueFilter());
}
private static String desensitizePhoneOrIdCard(String num) {
if (StringUtils.isBlank(num)) {
return "";
}
return StringUtils.left(num, 3).concat(StringUtils.removeStart(StringUtils.leftPad(StringUtils.right(num, 4), StringUtils.length(num), "*"), "***"));
}
private static String desensitizeBankCard(String cardNum) {
if (StringUtils.isBlank(cardNum)) {
return "";
}
return StringUtils.left(cardNum, 4).concat(StringUtils.removeStart(StringUtils.leftPad(StringUtils.right(cardNum, 4), StringUtils.length(cardNum), "*"), "****"));
}
private static final ValueFilter getValueFilter() {
return new ValueFilter() {
@Override
public Object process(Object obj, String key, Object value) {//obj-对象 key-字段名 value-字段值
try {
Field field = obj.getClass().getDeclaredField(key);
SensitiveInfo annotation = field.getAnnotation(SensitiveInfo.class);
if (null != annotation && value instanceof String) {
String strVal = (String) value;
if (StringUtils.isNotBlank(strVal)) {
switch (annotation.type()) {
case PHONE:
return desensitizePhoneOrIdCard(strVal);
case ID_CARD:
return desensitizePhoneOrIdCard(strVal);
case BANK_CARD:
return desensitizeBankCard(strVal);
default:
break;
}
}
}
} catch (NoSuchFieldException e) {
//找不到的field对功能没有影响,空处理
}
return value;
}
};
}
public static void main(String[] args) {
CardInfo cardInfo = new CardInfo();
cardInfo.setId("11111111111111111");
cardInfo.setCardId("6228480402564890018");
System.out.println(SensitiveInfoUtils.toJsonString(cardInfo));
}
}
附CardInfo类
public class CardInfo {
private String userId;
private String name;
@SensitiveInfo(type = SensitiveType.ID_CARD)
private String certId;
@SensitiveInfo(type = SensitiveType.BANK_CARD)
private String cardId;
private String bank;
private String phone;
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getCertId() {
return certId;
}
public void setCertId(String certId) {
this.certId = certId;
}
public String getCardId() {
return cardId;
}
public void setCardId(String cardId) {
this.cardId = cardId;
}
public String getBank() {
return bank;
}
public void setBank(String bank) {
this.bank = bank;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
}
java注解式脱敏
随着互联网时代普及,用户的信息越来越重要,我们开发软件过程中也需要对用户的信息进行脱敏处理活着加密处理,针对于比较繁杂的工作,个人来讲解如何实现注解式脱敏,支持静态调用和aop统一拦截实现脱敏或者加密返回。
代码讲解
脱敏枚举类
定义枚举类,处理所有脱敏和加密等,同时可扩展性,这里只是注解式调用方法而已,以便编写样例。DesensitizationEnum若还需要其他脱敏或者加密方法是,只需要添加下面枚举类型即可
package com.lgh.common.sensitive;
import com.lgh.common.utils.MaskUtils;
import java.lang.reflect.Method;
public enum DesensitizationEnum {
// 执行类和脱敏方法名
PHONE(MaskUtils.class, "maskPhone", new Class[]{String.class});
private Class<?> clazz;
private Method method;
DesensitizationEnum(Class<?> target, String method, Class[] paramTypes) {
this.clazz = target;
try {
this.method = target.getDeclaredMethod(method, paramTypes);
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
}
public Method getMethod() {
return method;
}
}
脱敏工具
package com.lgh.common.utils;
import org.springframework.util.StringUtils;
public class MaskUtils {
public static String maskPhone(String phone){
if(StringUtils.isEmpty(phone) || phone.length() < 8){
return phone;
}
return phone.replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2");
}
}
注解类编写
此类添加到需要脱敏的类属性上即可实现脱敏,具体是递归遍历此注解,通过反射机制来实现脱敏功能
package com.lgh.common.sensitive;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SensitiveValid {
DesensitizationEnum type();
}
脱敏工具类
特殊声明,我们递归时是索引递归,会出现死循环的情况,比如对象引用了对象,循环地址引用,所以会出现死循环,这里设置了10层递归,一般我们也不允许有那么深的对象设置。
package com.lgh.common.utils;
import com.lgh.common.sensitive.DesensitizationEnum;
import com.lgh.common.sensitive.SensitiveValid;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.ReflectionUtils;
import java.lang.reflect.Field;
import java.util.Collection;
import java.util.Map;
public class DesensitizationUtils {
private static final Logger log = LoggerFactory.getLogger(DesensitizationUtils.class);
private DesensitizationUtils() {
}
public static void format(Object obj) {
DesensitizationUtils.formatMethod(obj, 10);
}
private static void formatMethod(Object obj, int level) {
if (obj == null || isPrimitive(obj.getClass()) || level <= 0) {
return;
}
if (obj.getClass().isArray()) {
for (Object object : (Object[]) obj) {
formatMethod(object, level--);
}
} else if (Collection.class.isAssignableFrom(obj.getClass())) {
for (Object o : ((Collection) obj)) {
formatMethod(o, level--);
}
} else if (Map.class.isAssignableFrom(obj.getClass())) {
for (Object o : ((Map) obj).values()) {
formatMethod(o, level--);
}
} else {
objFormat(obj, level);
}
}
private static void objFormat(Object obj, int level) {
for (Field field : obj.getClass().getDeclaredFields()) {
try {
if (isPrimitive(field.getType())) {
SensitiveValid sensitiveValid = field.getAnnotation(SensitiveValid.class);
if (sensitiveValid != null) {
ReflectionUtils.makeAccessible(field);
DesensitizationEnum desensitizationEnum = sensitiveValid.type();
Object fieldV = desensitizationEnum.getMethod().invoke(null, field.get(obj));
ReflectionUtils.setField(field, obj, fieldV);
}
} else {
ReflectionUtils.makeAccessible(field);
Object fieldValue = ReflectionUtils.getField(field, obj);
if (fieldValue == null) {
continue;
}
formatMethod(fieldValue, level - 1);
}
} catch (Exception e) {
log.error("脱敏数据处理异常", e);
}
}
}
public static boolean isPrimitive(Class<?> clz) {
try {
if (String.class.isAssignableFrom(clz) || clz.isPrimitive()) {
return true;
} else {
return ((Class) clz.getField("TYPE").get(null)).isPrimitive();
}
} catch (Exception e) {
return false;
}
}
}
脱敏AOP的实现
aop插拔式编程,以便防止有不需要的操作,所以编写可控制类注解EnableDesensitization
package com.lgh.common.sensitive;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface EnableDesensitization {
}
最后实现拦截aop
package com.lgh.common.sensitive;
import com.lgh.common.utils.DesensitizationUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.context.annotation.Configuration;
import org.aspectj.lang.annotation.Aspect;
import java.lang.reflect.Method;
@Aspect
@Configuration
public class SensitiveAspect {
public static final String ACCESS_EXECUTION = "execution(* com.lgh.controller..*.*(..))";
@Around(ACCESS_EXECUTION)
public Object sensitiveClass(ProceedingJoinPoint joinPoint) throws Throwable {
return sensitiveFormat(joinPoint);
}
public Object sensitiveFormat(ProceedingJoinPoint joinPoint) throws Throwable {
Object obj = joinPoint.proceed();
if (obj == null || DesensitizationUtils.isPrimitive(obj.getClass())) {
return obj;
}
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
EnableDesensitization desensitization = joinPoint.getTarget().getClass().getAnnotation(EnableDesensitization.class);
if (desensitization != null || method.getAnnotation(EnableDesensitization.class) != null) {
DesensitizationUtils.format(obj);
}
return obj;
}
}
实战演练
我居于上一章节的UserDetail对象增加phone字段,同时加入注解,如下:
package com.lgh.common.authority.entity;
import com.lgh.common.sensitive.DesensitizationEnum;
import com.lgh.common.sensitive.SensitiveValid;
public class UserDetail {
private long id;
private String name;
@SensitiveValid(type = DesensitizationEnum.PHONE)
private String phone;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void setPhone(String phone) {
this.phone = phone;
}
public String getPhone() {
return phone;
}
}
接下来controller中启动注解
@GetMapping("/detail")
@EnableDesensitization
public IResult<UserDetail> getUser(@AuthenticationPrincipal UserDetail userDetail) {
return CommonResult.successData(userDetail);
}
大功告成,接下来我们实现一下访问操作
github,以上为个人经验,希望能给大家一个参考,也希望大家多多支持编程网。