动态可定义数据脱敏的方案

时间:2024-03-07 16:45:26

动态脱敏的一种思路

引言

​ 本文只是记录一种动态定义脱敏字段的解决方法。
​ 本人是一个Java实习生,本方法只针对特殊环境进行使用。
​ 此方法已运用与某区域医疗集成平台以及某健康门户Vip平台

​ 注:此方法不适合大并发项目。

现有方案

  • 基于注解

    • GitHub链接

      优点:方便 缺点:无法不重启修改需要脱敏的字段,不符合需求

  • 前端脱敏

    优点:无需技巧

    缺点:稍微懂一点计算机的,看一下浏览器控制台就拿到数据了。一般不可取

思路

  • 无需重启即可更新字段

    • 解决方案

      • 写在独立的配置文件

      • 独立维护一个表(脱敏表),存储脱敏规则,定期读取更新或者触发式读取更新

        此处独立选择维护一个脱敏表。

        项目启动时,将需要脱敏的表,字段以及字段对应的规则全部读取存入Hash Map 中。

        对外暴露一个新增和更新接口,触发新增或者更新接口时,通知工具类对Hash Map进行更新。(观察者模式)

        下为规则更新的伪代码。仅展示单个表的方法,多表则修改Hash Map的存储类型即可

      public class DesensitiseUtil{
          /**
      	* Long为角色id,List<String>规则存储方法
      	*/
      	static HashMap<Long, List<String>> desensitiseMap = new HashMap<>();
      
      	/**
          * 观察者模式,插入新的规则或者更新规则触发
          */
          public static void update() {
              // 读取数据库
              // 根据Key更新对应的 Hash Map的值
          }
      }
      
      

      ​ 下为外暴接口,提供给Controller层调用

      @Service
      public class DesensitiseServiceImpl implements DesensitiseService {
          @Override
          public int insertOrUpdateRule(Dto dto){
              // 插入或者更新
              // 根据dto的输入更新脱敏规则表
              // 调用DesensitiseUtil的更新接口
              DesensitiseUtil.update();
              // 返回插入或者更新影响行数
      	}
      }
      
  • 脱敏方法

    • 解决方案

      • 通过反射进行脱敏
      public class DesensitiseUtil {
      	/**
          * 对任意类型的属性进行
          *
          * @param result    查询结果
          * @param fieldName 需要脱敏的属性
          */
          public static <T> void desensitise(T result, String fieldName) {
              Field field = ReflectionUtils.findField(result.getClass(), fieldName);
              if (field != null) {
                  field.setAccessible(true);
                  switch (fieldName) {
                      case "mobile": {
                          ReflectionUtils.setField(field, result, SensitiveUtil.sensitiveMobilePhone(ReflectionUtils.getField(field, result)));
                          break;
                      }
                      case "realName": {
                          ReflectionUtils.setField(field, result, SensitiveUtil.sensitiveName(ReflectionUtils.getField(field, result)));
                          break;
                      }
                      case "certificateNum": {
                          ReflectionUtils.setField(field, result, SensitiveUtil.sensitiveExitOrEntryPermit(ReflectionUtils.getField(field, result)));
                          break;
                      }
                      default: {
                          break;
                      }
                  }
              }
          }
      
          /**
          * 对结果集进行脱敏
          *
          * @param resultList 结果列
          * @param fieldName  需要脱敏的属性
          */
          private static <T> void desensitise(List<T> resultList, String fieldName) {
              resultList.forEach(result-> desensitise(result, fieldName));
          }
      
          /**
          * 对任意List进行脱敏
          *
          * @param resultList    结果集
          * @param fieldNameList 需要脱敏的属性
          */
          private static <T> void desensitise(List<T> resultList, List<String> fieldNameList) {
              fieldNameList.forEach(fieldName-> desensitise(resultList, fieldName));
          }
      
          /**
          * 对返回体内的结果集进行批量脱敏
          *
          * @param result        结果集
          * @param rowsName      结果集所在的属性
          * @param fieldNameList 结果集内单个结果需要脱敏的属性
          */
          @SuppressWarnings({"rawtypes", "unchecked"})
          private static <T> void desensitise(T result, String rowsName, List<String> fieldNameList) {
              Field field = ReflectionUtils.findField(result.getClass(), rowsName);
              if (field != null) {
                  field.setAccessible(true);
                  List resultList = (List) ReflectionUtils.getField(field, result);
                  if (null != resultList && !resultList.isEmpty()) {
                      fieldNameList.forEach(fieldName->desensitise(resultList, fieldName));
                  }
              }
          }
      
          /**
          * 根据角色id对应的规则进行脱敏
          *
          * @param roleId   角色id,用于获取角色对应的脱敏列表 {@link #getDesensitiseRule(Long)}
          * @param result   结果集
          * @param rowsName 结果集所在的属性
          */
          public static <T> void desensitise(Long roleId, T result, String rowsName) {
              List<String> ruleList = getDesensitiseRule(roleId);
              desensitise(result, rowsName, ruleList);
          }
      
          /**
          * 根据角色id对应的规则进行脱敏
          *
          * @param roleId     角色id,用于获取角色对应的脱敏列表 {@link #getDesensitiseRule(Long)}
          * @param resultList 结果列
          */
          public static <T> void desensitise(Long roleId, List<T> resultList) {
              List<String> ruleList = getDesensitiseRule(roleId);
              desensitise(resultList, ruleList);
          }
      
          /**
          * 通过角色id获得角色对应的脱敏列表
          *
          * @param roleId 角色id,用于获取角色对应的脱敏列表
          * @return 角色对应的脱敏列表
          */
          private static List<String> getDesensitiseRule(Long roleId) {
              // 获得角色对应的脱敏规则
          }
      
      }
      
  • 调用方法

    • 解决方案

      • Mybatis拦截器

        @Intercepts(
            value = {
                @Signature(
                    type = Executor.class,
                    method = "query",
                    args = {
                        MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class
                    }
                )
            }
        )
        public class DesensitizationInterceptor implements Interceptor {
            final String INVOCATION_QUERY_METHOD = "query";
        
            @Override
            public Object intercept(Invocation invocation) throws Throwable {
                // Query语句
        		if (INVOCATION_QUERY_METHOD.equals(invocation.getMethod().getName())) {
                    Object proceed = invocation.proceed();
                    if(proceed instanceof List){
                        // 拿到角色id,设为roleId
                        // 调用函数
                        List<entity> list = (List<entity>) proceed;
                        desensitise(roleId,list);
                        // 返回结果
                    }else if(proceed instanceof Dto){
                        // 拿到角色id,设为roleId
                        // 调用函数
                        Entity list = (Entity) proceed;
                        desensitise(roleId,dto,rowsName);
                         // 返回结果
                    }
                }
                 return invocation.proceed();
            }
        
        • 存在的问题:如果存在查到A,取A的属性,若A的属性是脱敏后的,就会查不到数据
      • AOP 拦截

        @Aspect
        @Component
        public class DesensitizationAspect {
            final DesensitizationTool desensitizationTool;
        
            // 定义切入点
            @Pointcut(...)
            public void desensitization() {
            }
        
            @Around("desensitization()")
            public Object aroundMethod(ProceedingJoinPoint proceedingJoinPoint) {
                Object result = null;
                    try {
                        result = proceedingJoinPoint.proceed();
                        if(result instanceof List){
                        // 拿到角色id,设为roleId
                        // 调用函数
                        List<entity> list = (List<entity>) proceed;
                        desensitise(roleId,list);
                        // 返回结果
                    }else if(result instanceof Dto){
                        // 拿到角色id,设为roleId
                        // 调用函数
                        Entity list = (Entity) proceed;
                        desensitise(roleId,dto,rowsName);
                         // 返回结果
                    }
                    } catch (Throwable throwable) {
                        throwable.printStackTrace();
                    }
                return result;
            }
        
      • Controller层调用

        该方法不再展示,直接调用函数即可,如果是现有项目改造,修改量较大