醉卧草庐听风雨

君子藏器于身,待时而动

0%

Spring MVC实现jackson动态过滤字段

去年为解决json字段自定义返回曾写了个JsonReturnValueHandler组件,点此查看原文,当时图方便json序列化使用的fastjson,然而今年就爆出fastjson存在严重的安全漏洞,在升级了线上的fastjson版本之后,为了不再担惊受怕于是决定去fastjson,作为其编写使用者的我就只能再对JsonReturnValueHandler组件进行jackson改造。

方案分析

阅读之前的fastjson方案,可知其组成主要有三部分:

  • CustomJson以及CustomJsons注解
  • 基于HandlerMethodReturnValueHandler的JsonReturnValueHandler返回值处理
  • fastjson的SerializeFilter字段的过滤器

由于使用了注解,所以Controller使用方的变动可以忽略,但需要注意的是之前bean字段上的fastjson注解,需替换为jackson注解。那么需要改动就只有自定义注解处理的方式,由之前fastjson改为jackson。

jackson实现

关于jackson实现对字段的动态过滤,有以下几种方式:

  • JsonView注解,此方式存在固有缺陷,即不支持嵌套过滤,若是A中的一个字段是B,那么对B过滤就会失效。而且此方案与现有的自定义注解方案难以融合,所以out
  • StdSerializer序列化,此方案可行,但需要较多的代码实现,容易出错,有兴趣的可以自己挑战下
  • FilterProvider,此方案实现难度较低,直接通过不同的FilterProvider来实现,网上大多数教程也是采用此方式

FilterProvider的作用是根据filterId查到相关的PropertyFilter,由PropertyFilter决定字段是否被序列化,按照此逻辑CustomJson中targetClass可以当作filterId,includes和excludes则可以包装为PropertyFilter。相关代码例子已上至码云,下面直接是核心的代码实现

自定义FilterProvider和PropertyFilter

这里定义了JsonFilterProvider和JsonPropertyFilter,由JsonFilterProvider持有Map<Class, JsonPropertyFilter>映射关系,而JsonPropertyFilter则实现了对includes和excludes的处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public class JsonFilterProvider extends FilterProvider {
private final Map<Object, PropertyFilter> cache;

public JsonFilterProvider(Map<Object, PropertyFilter> cache) {
this.cache = cache;
}

@Override
public BeanPropertyFilter findFilter(Object filterId) {
return null;
}

@Override
public PropertyFilter findPropertyFilter(Object filterId, Object valueToFilter) {
return cache.get(filterId);
}

}

public class JsonPropertyFilter extends SimpleBeanPropertyFilter {
private final List<String> includes;
private final List<String> excludes;

private JsonPropertyFilter(String[] includes, String[] excludes) {
this.includes = Arrays.asList(includes);
this.excludes = Arrays.asList(excludes);
}

@Override
protected boolean include(PropertyWriter writer) {
// 字段是否被序列化
String name = writer.getName();
if (includes.isEmpty()) {
return !excludes.contains(name);
}
return includes.contains(name);
}

public static JsonPropertyFilter build(CustomJson customJson) {
String[] includes = customJson.includes();
String[] excludes = customJson.excludes();
// includes,excludes两种类型仅能存在一种
boolean allExists = includes.length != 0 && excludes.length != 0;
Assert.isTrue(!allExists, "includes or excludes must be empty");
return new JsonPropertyFilter(includes, excludes);
}
}

JsonReturnValueHandler改造

基础类准备好之后就是如何对已有的JsonReturnValueHandler进行改造了。其中值得注意的是baseObjectMapper的创建,这里需要重写findFilterId的实现,否则序列化对象将无法触发JsonFilterProvider获取指定的JsonPropertyFilter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
public class JsonReturnValueHandler implements HandlerMethodReturnValueHandler {
/**
* 使用guava缓存已处理方法
*/
private LoadingCache<MethodParameter, ObjectWriter> cache = CacheBuilder.newBuilder()
.build(new CacheLoader<MethodParameter, ObjectWriter>() {
@Override
public ObjectWriter load(MethodParameter parameter) {
return build(parameter);
}
});


/**
* 指定基础的ObjectMapper
*/
private ObjectMapper baseObjectMapper = new ObjectMapper()
.setAnnotationIntrospector(new JacksonAnnotationIntrospector() {
@Override
public Object findFilterId(Annotated annotated) {
// 提取待序列化对象的class作为FilterId
if (annotated instanceof AnnotatedClass) {
return annotated.getRawType();
}
return null;
}
});

@Override
public boolean supportsReturnType(MethodParameter methodParameter) {
CustomJson customJson = methodParameter.getMethodAnnotation(CustomJson.class);
CustomJsons customJsons = methodParameter.getMethodAnnotation(CustomJsons.class);
Assert.isTrue(customJson == null || customJsons == null, "only use one of CustomJson or CustomJsons ");
return customJson != null || customJsons != null;
}

@Override
public void handleReturnValue(Object o, MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer,
NativeWebRequest nativeWebRequest) throws Exception {
modelAndViewContainer.setRequestHandled(true);

HttpServletResponse response = nativeWebRequest.getNativeResponse(HttpServletResponse.class);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
// 获取ObjectWriter写入结果
cache.get(methodParameter).writeValue(response.getWriter(), o);
}

/**
* 将MethodParameter转换为ObjectWriter
*/
private ObjectWriter build(MethodParameter parameter) {
// key为CustomJson的targetClass,value为includes,excludes包装后的JsonPropertyFilter
Map<Object, PropertyFilter> cache;
CustomJson customJson = parameter.getMethodAnnotation(CustomJson.class);
if (customJson == null) {
cache = Arrays.stream(parameter.getMethodAnnotation(CustomJsons.class).value())
.collect(Collectors.toMap(CustomJson::targetClass, JsonPropertyFilter::build));
} else {
cache = Collections.singletonMap(customJson.targetClass(), JsonPropertyFilter.build(customJson));
}
return baseObjectMapper.writer(new JsonFilterProvider(cache));
}
}

写在最后

经过上述代码改造,可以无感将CustomJson的处理由fastjson切换到jackson,让人摆脱fastjson无穷无尽的漏洞问题,实现一劳永逸。不过话说自己最近人有些疲惫,一晃距离上次博客发表过去3个月了。