目录
前言
我们的后台系统收到了数据脱敏的需求, 要求在一些关联页面的手机号, 收货地址等重要信息需要进行脱敏显示. 所以才有了这个脱敏方案
常见方案
方案一
在关键的DTO 和VO上增加注解, 然后这些对象通过接口返回调用者的时候, 会屏蔽掉注释字段的值.
优点:
- 只要增加了注释, 所有的返回这个对象的接口都会屏蔽, 不用每个接口单独写代码
缺点:
- 无法针对特定的接口精细化控制
- 返回的前端的DTO对象是其它微服务中定义的, 不方便在字段上增加注释
方案二
在controller接口中加上注解, 标识出具体的字段需要进行屏蔽
优点:
- 精细控制每个接口的返回结果屏蔽
- 对于返回对象的类型的代码不用修改
缺点:
- 需要每个接口都写上注解,
- 需要写AOP切面类
方案三
前端自己进行屏蔽显示
缺点: 属于自己骗自己的方案
综合考虑, 我们的后台系统引用了太多其他微服务的DTO对象, 并直接返回给前端, 无法采用方案一, 最终使用了方案二
实现方式
一次请求大致需要经过的流程如下:
我采用的是方案二, 实现:
- 在返回结果的时候切面类中获取注解值. 并将注解值放入ThreadLocal 中, 注解使用的是jsonPath的语法标识屏蔽字段
- 在序列化的时候, 获取ThreadLocal中的注解, 根据注解的信息屏蔽对应的字段值, 具体屏蔽使用的是fastjson的
注意:
- 返回响应的时候在切面类在获取注解放入ThreadLocal中, 由于可能在controller的逻辑中会发出http请求, 这样就会在controller中又内嵌上图中的一套逻辑, 过早的放入ThreadLocal 会互相影响.
- 序列化的时候 从ThreadLocal中获取配置信息, 再使用fastjson的jsonpath 修改对应的值
两个注解类
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) {
..... 业务代码省略
}
接口调用
调用结果 如下图
踩过的坑
-
在切面代理controller方法前面就把注解放入threadLocal中, 结果controller方法中还会通过RPC(http)方式调用其他的微服务的接口, 也会使用到MappingJackson2HttpMessageConverter 进行序列化, 结果就是threadLocal中的注解值被rpc方法使用并释放了
-
jsonPath在获取指定位置的值的时候, 会忽略调用null的情况, 导致屏蔽错位