自定义注解脱敏
前言
帮助项目快速接入脱敏能力,企业安全要求增加,要求后端数据需要脱敏返回,本项目旨在帮助现有项目快速接入脱敏能力,减少代码改动,减少代码入侵,快速实现,如果有兴趣,可以看看评论区的maven,引入后即可快速接入脱敏能力,增加开发效率。
快速接入
<dependency>
<groupId>io.github.HHs11</groupId>
<artifactId>desensitization</artifactId>
<version>1.0.4</version>
</dependency>
源码解析
创建一个自定义注解,用于定于aop的切点
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Desensitized {
String value() default "Desensitized_null";
// 用于接收脱敏方法名字
String method() default "desensitizedPhoneNumber";
}
定义了一个可以用在方法和变量的注解Desensitized ;声名method变量用于接收脱敏方法名。默认为手机脱敏方法,value参数暂时没有用。
创建一个工具类
package com.example.demo.utils;
import org.apache.commons.lang.StringUtils;
/**
* @className:关键信息脱敏工具类
* @author:hhs
* @date:2021-07-07 16:51
*/
public class DesensitizedUtil {
/**
* 地址脱敏
*
* @param address
* @return
*/
public static String desensitizedAddress(String address) {
if (!StringUtils.isNotEmpty(address)) {
return StringUtils.left(address, 3).concat(StringUtils.removeStart(StringUtils.leftPad(StringUtils.right(address, address.length() - 11), StringUtils.length(address), "*"), "***"));
}
return address;
}
/**
* 地址脱敏v2
*
* @param address
* @return
*/
public static String desensitizedAddressV2(String address) {
return address.replaceAll(String.format("[0-9]"), "*");
}
/**
* 电话脱敏
*/
public static String desensitizedPhoneNumber(String phoneNumber) {
if (phoneNumber.length() < 11) {
return phoneNumber.replaceAll("(\\w{2})\\w*(\\w{3})", "$1****$2");
}
if (StringUtils.isNotEmpty(phoneNumber)) {
phoneNumber = phoneNumber.replaceAll("(\\w{3})\\w*(\\w{4})", "$1****$2");
}
return phoneNumber;
}
/**
* 姓名脱敏
*/
public static String desensitizedName(String name) {
String retName = name.charAt(0) + "";
if (StringUtils.isNotEmpty(name)) {
for (int i = 1; i < name.length(); i++) {
retName += "*";
}
}
return retName;
}
}
工具类中包括了一些常见的脱敏方法。包括地址脱敏,电话脱敏,等。可以在里面按照格式继续添加脱敏方法,常见的地址脱敏有隐藏中间信息,还有隐藏具体门牌数字等,手机脱敏则显示前三后四。
创建Aop,切入注解切面处理数据
package com.example.demo.aop.impl;
import com.example.demo.aop.Desensitized;
import com.example.demo.utils.DesensitizedUtil;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Field;
/**
* @className:DesensitizedAop
* @author:hhs
* @date:2021-07-30 10:04
*/
@Component
@Aspect
public class DesensitizedAop {
@Around(value = "@annotation(desensitized)")
public Object paramCheck(ProceedingJoinPoint joinPoint, Desensitized desensitized) throws Throwable {
// 获取返回值并脱敏修改后返回
return execute(joinPoint.proceed());
}
/**
* 判断是否为基本类型:包括String
*
* @param clazz clazz
* @return true:是; false:不是
*/
private boolean isPrimite(Class<?> clazz) {
return clazz.isPrimitive() || clazz == String.class;
}
/**
* 递归实现脱敏,如果入参不是一个基本数据类型,则需要递归,直到基本类型结束
*/
private Object execute(Object obj) throws IllegalAccessException {
Class<?> clazz = obj instanceof Field ? ((Field) obj).getType() : obj.getClass();
// 基本类型直接返回
if (isPrimite(clazz)) {
return obj;
}
// 反射拿到属性
Field[] declaredFields = clazz.getDeclaredFields();
// 遍历属性并依次判断,是否基本类型,是否有自定义注解
for (Field declaredField : declaredFields) {
// 设置accessible属性为真,可以修改非public类型属性
declaredField.setAccessible(true);
if (isPrimite(declaredField.getType())) {
Desensitized annotation = declaredField.getAnnotation(Desensitized.class);
if (annotation != null) {
declaredField.set(obj, desensitization(declaredField.get(obj), annotation));
}
} else { // 非基本类型继续递归
if (declaredField.get(obj) != null) {
declaredField.set(obj, execute(declaredField.get(obj)));
}
}
}
return obj;
}
/**
* 脱敏方法
*/
private Object desensitization(Object obj, Desensitized annotation) {
//利用反射,调用自定义注解中的参数方法
MethodType methodType = MethodType.methodType(String.class, String.class);
// 获取静态方法的句柄
MethodHandle method = null;
try {
// 反射拿到该属性上自定义注解中的method值,并调用DesensitizedUtil中方法名为该值的方法
method = MethodHandles.lookup()
.findStatic(DesensitizedUtil.class, annotation.method(), methodType);
} catch (NoSuchMethodException e) {
return obj;
} catch (IllegalAccessException e) {
return obj;
}
Object r;
try {
r = method.invoke((String) obj); // invoke|invoke
} catch (Throwable e) {
return obj;
}
return r;
}
}
利用aop的joinPoint.proceed()方法拦截返回值,然后分析返回值,判断是否是基本数据类型,如果不是,则利用递归逐级处理,如果是基本类型,则在有脱敏注解的字段处理成脱敏数据后重新写入。详情看注释。
使用
@Data
public class Student {
@Desensitized(method = "desensitizedName")
String name;
String number;
@Desensitized(method = "desensitizedPhoneNumber")
String phone;
// 需要脱敏的字段加上自定义注解,并指定method脱敏方法名
@Desensitized(method = "desensitizedAddressV2")
String address;
Book book;
}
在需要脱敏的实体类字段上添加 @Desensitized ,method的值是需要执行工具类中脱敏方法的方法名,如手机脱敏,则是调用desensitizedPhoneNumber。(需要脱敏的controller上也需要加上这个注解@Desensitized(),参数为null就行)
测试
可在git中运行查看
https://gitee.com/pinus_taiwanensis/desensitization
总结
根据我的方法可以解决大部分返回数据敏感问题,建议直接使用maven引入,更加快速接入。如有疑问和建议反馈或者指出错误,可以在评论区留言讨论,如有转载,请署名并表明来源。