数据脱敏:姓名、电话号码等进行字段脱敏,中间部分显示成**

时间:2024-03-01 21:12:58

  在前端展示时,有时需要将名字、电话号码、身份证等敏感信息过滤展示(脱敏),这种一般需要后端处理,提前将敏感信息过滤换成**的字样。

  第一种方式是在每个页面展示时,去过滤,但是需要改动的地方非常多。实用性不强;

  第二种方式是采用面向切面编程AOP相类似的方式,只需要写一个方法,然后在方法上加一个自定义类注解,在过滤的属性上加上类型注解就解决。

这里主要讲第二种方式 实现步骤为:

  ①定义脱敏的类型

  ②自定义类或方法的注解和字段的注解

  ③定义返回数据的格式

  ④实现脱敏的规则

  ⑤在字段和类上使用注解,声明脱敏的字段

  ⑥测试调用接口 获得结果

本博客涉及到的知识:

  ①如何自定义注解,以及各注解代表的含义

  ②了解特定注解@ControllerAdvice的含义以及接口ResponseBodyAdvice的机制

  ③反射获取数据,解析数据

  ④实现脱敏的逻辑

1.自定义注解

  声明一个枚举脱敏类型

    /**
     * 数据脱敏类型
     */
    public enum DesensitizeType {

        NAME, // 名称
        ID_CARD_18, //身份证 18
        EMAIL,//email
        MOBILE_PHONE; //手机号
    }

 

   声明脱敏的字段 的注解(用在字段上)

    /**
     * 标记字段 使用何种策略来脱敏
     */
    @Documented
    @Retention(value = RetentionPolicy.RUNTIME)
    @Target(value = {ElementType.FIELD})
    @Inherited
    public @interface Desensitize {

        DesensitizeType type();
    }

 

   声明脱敏的方法或类的注解

    /**
     * 标记在类、方法上,是否需要脱敏
     */
    @Documented
    @Retention(value = RetentionPolicy.RUNTIME)
    @Target(value={ElementType.METHOD, ElementType.TYPE})
    @Inherited //说明子类可以继承父类中的该注解
    public @interface DesensitizeSupport {

    }

 

2.实现数据脱敏

  定义响应的对象格式

/**
 * 响应实体
 */
public class ResResult {
    /**
     * 编码
     */
    private String code;
    /**
     * 提示信息
     */
    private String message;
    /**
     * 数据
     */
    private Object data;

    //get //set...
}

 

  数据的model,对要脱敏的字段加注解@Desensitize和脱敏类型DesensitizeType

public class UserModel implements Serializable {
    /**
     * 姓名
     */
    @Desensitize(type = DesensitizeType.NAME)
    private String name;

    private Integer age;

    private String desc;
    /**
     * 电话号码
     */
    @Desensitize(type = DesensitizeType.MOBILE_PHONE)
    private String telNumber;

    //get //set...
}

 

   controller层,在类或者方法上加注解@DesensitizeSupport 表示该类或方法支持脱敏

@RestController
@RequestMapping("/test")
@DesensitizeSupport
public class UserController {
    
    @Autowired
    private IUserService iUserService;

    @GetMapping(value = "/listuser")
    public ResResult testHello() {
        ResResult result = new ResResult();
        List<UserModel> list = iUserService.listUser();
        result.setData(list);
        return result;
    }
}

 

   Service层

@Service
public class UserServiceImpl implements IUserService {

    @Override
    public List<UserModel> listUser() {
        UserModel user = new UserModel();
        user.setName("李四");
        user.setAge(123);
        ArrayList<UserModel> list = new ArrayList<>();
        list.add(user);
        return list;
    }
}

 

   有了以上的部分后,还不会进行脱敏,还需要加上脱敏的具体操作。在Controller中执行了return语句后,在返回到前端之前,会执行如下代码进行脱敏:

/**
 * 脱敏工具类
 */
public class DesensitizeUtils {

    public static void main(String[] args) {
        String name = "李明";
        System.out.println(repVal(name, 1, 1));
    }
    
    public static String dataMasking(DesensitizeType type, String oldValue) {
        String newVal = null;
        switch (type) {
            case NAME:
                newVal = repVal(oldValue, 1, 1);
                break;
            case ID_CARD_18:
                break;
            case EMAIL:
                break;
            case MOBILE_PHONE:
                break;
        }
        return newVal;
    }
    /**
     * 字符替换
     * @param val
     * @param beg
     * @param end
     * @return
     */
    public static String repVal(String val, int beg, int end) {
        if (StringUtils.isEmpty(val)) {
            return null;
        }
        String name = val.substring(0, beg);
        int length = val.length();
        if (length > 2 && length > end) {
            return name + "**" + val.substring(length-end);
        } else if (length == 2) {
            return name + "*";
        }
        return val;
    }
}

 

 

/**
 * 统一处理 返回值/响应体
 */
@ControllerAdvice
public class DesensitizeResponseBodyAdvice implements ResponseBodyAdvice<Object> {

    private final static Logger logger = LoggerFactory.getLogger(DesensitizeResponseBodyAdvice.class);

    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        AnnotatedElement annotatedElement = returnType.getAnnotatedElement();
        //1.首先判断该方法是否存在@DesensitizeSupport注解
        //2.判断类上是否存在
        Method method = returnType.getMethod();
        DesensitizeSupport annotation = method.getAnnotation(DesensitizeSupport.class);
        DesensitizeSupport clazzSup = method.getDeclaringClass().getAnnotation(DesensitizeSupport.class);
        return annotation != null || clazzSup != null;
    }

    /**
     *
     * @param body
     * @param returnType
     * @param selectedContentType
     * @param selectedConverterType
     * @param request
     * @param response
     * @return
     */
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
                                  Class<? extends HttpMessageConverter<?>> selectedConverterType,
                                  ServerHttpRequest request, ServerHttpResponse response) {
        logger.debug("Test ResponseBodyAdvice ==> beforeBodyWrite:" + body.toString() + ";" + returnType);
        Class<?> childClazz = body.getClass();
        Field childField = null;
        List filedValue = null;
        try {
            //获取数据
            childField = childClazz.getDeclaredField("data");
            //设置可访问
            childField.setAccessible(true);
            Object objs = childField.get(body);

            if (!(objs instanceof List)) {
                logger.debug("这不是List类型");
                return body;
            }
            filedValue = (List) objs;
            //对值进行脱敏
            for (Object obj : filedValue) {
                dealValue(obj);
            }
        } catch (NoSuchFieldException e) {
            logger.error("未找到数据; message:" + e.getMessage());
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            logger.error("处理异常; message:" + e.getMessage());
            e.printStackTrace();
        }
        return body;
    }

    public void dealValue(Object obj) throws IllegalAccessException {
        Class<?> clazz = obj.getClass();
        //获取奔雷和父类的属性
        List<Field> fieldList = getAllFields(clazz);
        for (Field field : fieldList) {
            //获取属性上的注解
            Desensitize annotation = field.getAnnotation(Desensitize.class);
            if (annotation == null) {
                continue;
            }
            Class<?> type = field.getType();
            //判断属性的类型
            if (String.class != type) {
                //非字符串的类型 直接返回
                continue;
            }
            //获取脱敏类型  判断是否脱敏
            DesensitizeType annotType = annotation.type();
            field.setAccessible(true);
            String oldValue = (String) field.get(obj);
            String newVal = DesensitizeUtils.dataMasking(annotType, oldValue);
            field.set(obj, newVal);
        }
    }

    /**
     * 获取所有的字段(包括父类的)
     * @param clazz
     * @return
     */
    public List<Field> getAllFields(Class<?> clazz) {
        List<Field> fieldList = new ArrayList<>();
        while (clazz != null) {
            Field[] declaredFields = clazz.getDeclaredFields();
            fieldList.addAll(Arrays.asList(declaredFields));
            //获取父类,然后获取父类的属性
            clazz = clazz.getSuperclass();
        }
        return fieldList;
    }
}

 

3.结果

  响应的结果,我们期待的两个字的名称【李四】会【李*】,三个字或三个以上的【李小明】会变成【李**明】(规则可自己进行设置)

 

注:在Controller层执行了return语句后,在返回到前端之前 会执行DesensitizeResponseBodyAdvice类中的supports和beforeBodyWrite方法,其中在类上有一个很重要的注解@ControllerAdvice和很重要的接口ResponseBodyAdvice,这两个结合在一起,就具有统一处理返回值/响应体的功能。(相当于一个拦截器)

①@ControllerAdvice注解,这是一个Controller的增强型注解,可以实现三方面的功能:

  1. 全局异常处理
  2. 全局数据绑定
  3. 全局数据预处理

接口ResponseBodyAdvice

  继承了该接口,需要实现两个方法,supports和beforeBodyWrite方法。在supports方法返回为true后,才会执行beforeBodyWrite方法。其中beforeBodyWrite方法中的body就是响应对象response中的响应体,可以对响应体做统一的处理,比如加密、签名、脱敏等操作。

 

这里简单讲解一下其中的注解

使用【@interface】是自定义一个注解,通常自定义的注解上面还有其他注解,如以下几个:

@Documented 表示标记这个注解是否会包含在文档中
@Retention 标识这个注解怎么保存,有三种状态,value = RetentionPolicy.RUNTIME 表示不仅保留在源码中,也保留在class中,并且在运行时可以访问;
       SOURCE 表示只保留在源码中,当在class文件中时被遗弃;CLASS 表示保留在class文件中,但jvm加载class文件时被遗弃。
@Target 标注这个注解属于Java哪个成员,通常有属类、方法;字段;参数;包等
@Inherited 标记这个注解是继承于哪个注解类

 

 

 

 

 

 

 

 

 若需要完整的代码,请点【推荐】,然后留言。或觉得博文不错 也请推荐留言,感谢你的支持。