基于AOP的Controller接口脱敏方式

前言

我们的后台系统收到了数据脱敏的需求, 要求在一些关联页面的手机号, 收货地址等重要信息需要进行脱敏显示. 所以才有了这个脱敏方案

常见方案

方案一

在关键的DTO 和VO上增加注解, 然后这些对象通过接口返回调用者的时候, 会屏蔽掉注释字段的值.
优点:

  1. 只要增加了注释, 所有的返回这个对象的接口都会屏蔽, 不用每个接口单独写代码

缺点:

  1. 无法针对特定的接口精细化控制
  2. 返回的前端的DTO对象是其它微服务中定义的, 不方便在字段上增加注释

方案二

在controller接口中加上注解, 标识出具体的字段需要进行屏蔽
优点:

  1. 精细控制每个接口的返回结果屏蔽
  2. 对于返回对象的类型的代码不用修改

缺点:

  1. 需要每个接口都写上注解,
  2. 需要写AOP切面类

方案三

前端自己进行屏蔽显示
缺点: 属于自己骗自己的方案

综合考虑, 我们的后台系统引用了太多其他微服务的DTO对象, 并直接返回给前端, 无法采用方案一, 最终使用了方案二

实现方式

一次请求大致需要经过的流程如下:
在这里插入图片描述
我采用的是方案二, 实现:

  • 在返回结果的时候切面类中获取注解值. 并将注解值放入ThreadLocal 中, 注解使用的是jsonPath的语法标识屏蔽字段
  • 在序列化的时候, 获取ThreadLocal中的注解, 根据注解的信息屏蔽对应的字段值, 具体屏蔽使用的是fastjson的

注意:

  • 返回响应的时候在切面类在获取注解放入ThreadLocal中, 由于可能在controller的逻辑中会发出http请求, 这样就会在controller中又内嵌上图中的一套逻辑, 过早的放入ThreadLocal 会互相影响.
  • 序列化的时候 从ThreadLocal中获取配置信息, 再使用fastjson的jsonpath 修改对应的值

引入依赖

	<dependency>
		<groupId>cn.hutool</groupId>
		<artifactId>hutool-all</artifactId>
		<version>5.7.9</version>
	</dependency>
	<dependency>
		<groupId>com.alibaba</groupId>
		<artifactId>fastjson</artifactId>
		<version>1.2.70</version>
	</dependency>

两个注解类

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SensitiveWord {
    Sensitive[] value();
}
import java.lang.annotation.*;

import static com.youdao.athena.starfire.core.support.sensitive.DesensitizedType.PASSWORD;

@Target({})
@Retention(RetentionPolicy.RUNTIME)
public @interface Sensitive {
    /**
     * json path 的标识
     * @return
     */
    String jsonPath();

    /**
     * 脱敏的字段的数据分类,  默认是密码类型脱敏,   
     * @return
     */
     DesensitizedType desensitizedType() default PASSWORD;
}

切面类

使用匿名内部类和反射的方式获取注解信息, 并放入到ThreadLocal中

    @Bean
    public Advisor pointcutAdvisor() {
        MethodInterceptor interceptor = methodInvocation -> {
            Object proceed = methodInvocation.proceed();

            SensitiveWord annotation = AnnotationUtil.getAnnotation(methodInvocation.getMethod(), SensitiveWord.class);
            if (annotation != null && !this.isExport(methodInvocation)) {
                Sensitive[] value = annotation.value();
                sensitiveThreadLocal.set(value);
            }
            return proceed;
        };

        AnnotationMatchingPointcut pointcut = new AnnotationMatchingPointcut(null, SensitiveWord.class);
        // 配置增强类advisor
        DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor();
        advisor.setPointcut(pointcut);
        advisor.setAdvice(interceptor);
        return advisor;
    }

脱敏序列化类

  • MappingJackson2HttpMessageConverter 覆盖这个类的writeInternal 方法 , 在序列化的时候进行脱敏操作
  • replace 方法是进行jsonPath 替换的, 使用到的DesensitizedUtil 类是hutool的脱敏相关的工具类
    @Bean
    public MappingJackson2HttpMessageConverter getMappingJackson2HttpMessageConverter() {
        MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter() {
            /**
             * 重写方法, 进行敏感信息的脱敏操作
             * @param object
             * @param type
             * @param outputMessage
             * @throws IOException
             * @throws HttpMessageNotWritableException
             */
            @Override
            protected void writeInternal(Object object, Type type, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
                Sensitive[] values = InterceptorConfig.sensitiveThreadLocal.get();
                if (values == null) {
                    super.writeInternal(object, type, outputMessage);
                    return;
                }
                JSONObject jsonObject = (JSONObject) JSON.toJSON(object);
                for (Sensitive sensitive : values) {
                    String jsonPath = sensitive.jsonPath();
                    replace(jsonObject, jsonPath);
                }
                super.writeInternal(jsonObject, type, outputMessage);
                InterceptorConfig.sensitiveThreadLocal.remove();
            }
        };

        return mappingJackson2HttpMessageConverter;
    }

    /**
     * 敏感数据替换
     *
     * @param jsonObject
     * @param jsonPath
     */
    private static void replace(JSONObject jsonObject, String jsonPath) {
        if (JSONPath.contains(jsonObject, jsonPath)) {
            int index = jsonPath.lastIndexOf("[*]");
            if (index > -1) {
                String prefix = StrUtil.subPre(jsonPath, index);
                String suffix = StrUtil.subSuf(jsonPath, index + 3);
                Object eval = JSONPath.eval(jsonObject, prefix);
                JSONArray jsonArray = (JSONArray) eval;
                int size = jsonArray.size();
                for (int i = 0; i < size; i++) {
                    String indexJsonPath = StrUtil.strBuilder().append(prefix).append("[").append(i).append("]").append(suffix).toString();
                    String desensitized = Convert.toStr(JSONPath.eval(jsonObject, indexJsonPath));
                    if (StrUtil.isBlank(desensitized)) {
                        continue;
                    }
                    desensitized = DesensitizedUtil.desensitized(desensitized, DesensitizedType.MOBILE_PHONE);
                    JSONPath.set(jsonObject, indexJsonPath, desensitized);
                }
            } else {
                Object eval = JSONPath.eval(jsonObject, Convert.toStr(jsonPath));
                String desensitized = DesensitizedUtil.desensitized(Convert.toStr(eval), DesensitizedType.MOBILE_PHONE);
                JSONPath.set(jsonObject, jsonPath, desensitized);
            }
        }
    }

接口类

  • SensitiveWord 注解标识这个接口需要脱敏
  • Sensitive 有几个字段需要脱敏就配置几个次注解
    @SensitiveWord({
            @Sensitive(jsonPath = "$.body.courseUsers[*].mobile",desensitizedType = MOBILE_PHONE),
            @Sensitive(jsonPath = "$.body.courseUsers[*].address",desensitizedType = ADDRESS),
    })
    @PostMapping("/query/list")
    public WebResponse queryList(UserDTO userDTO, @RequestBody CourseUserQueryParam queryParam) {
   
		..... 业务代码省略
    }

接口调用

调用结果 如下图
在这里插入图片描述

踩过的坑

  1. 在切面代理controller方法前面就把注解放入threadLocal中, 结果controller方法中还会通过RPC(http)方式调用其他的微服务的接口, 也会使用到MappingJackson2HttpMessageConverter 进行序列化, 结果就是threadLocal中的注解值被rpc方法使用并释放了

  2. jsonPath在获取指定位置的值的时候, 会忽略调用null的情况, 导致屏蔽错位

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值