Jackson 配置与扩展
[一]前言
JSON 可以说是现今软件开发,特别是 Web 开发中不可或缺的组成部分。相信搞过开发的一定非常熟悉,估计也会觉得一个 JSON 没有讲解的必要。
- 如果你的工作只会涉及常规的 JSON 使用,只要能够接收和发送参数、实现 JSON 和对象的互转就可以满足日常工作的需要,那么 JSON 确实没有讲解的必要。
- 如果你想进一步提升自己的技术水平,了解更多的设计思想,那么 JSON 是非常重要的一个学习切入点。特别是 Spring Boot 生态中 Jackson 的使用以及设计实现。
[1]为什么会说 Jackson 在 Spring Boot 生态中很重要?
Jackson
是 Spring Boot 生态中核心的 JSON 处理组件,贯穿于 Spring Boot 生态包括 Spring Cloud 的各个组件中。- 正因为日常可见,往往就越不重视。而实际开发中,初次使用 Spring Boot 生态组件,遇到的很多问题往往就出自
Jackson
。 - Spring Boot 生态中
Jackson
的使用和设计,采用了 Spring Boot 生态中非常常用的一种我称之为Customizer
设计模式。这种设计模式不仅仅用于Jackson
,在涉及的很多组件中都有体现,掌握了这个知识点,对于 Spring Boot 生态中很多设计实现就会非常容易理解了
提示
Dante Cloud 自定义错误码体系,在后期的版本中,也是借鉴了 Spring Boot Jackson
所使用的这种设计模式。从而解决了之前版本中,所有的错误码必须写死、必须统一在某一个模块中定义、不灵活不好扩展、无法跨模块定义等问题。
[2]Dante Cloud 中的 JSON
在 Dante Cloud 系统中,主要依赖的 JSON 处理组件有:Jackson
、Fastjson
、Gson
以及 Hutool 中封装的 JSON 处理工具。
(1)Jackson
Dante Cloud 中主要使用的、也是默认使用 JSON 处理组件就是 Jackson
。之所以是用 Jackson
主要由以下几方面考虑:
- 一方面是为了与 Spring Boot 生态保持一致
使用纯整的 Spring Boot 生态是 Dante Cloud 的目标和特色之一。
- 另一方面也是由于 Fastjson 频频爆出安全漏洞问题,为了减少系统安全漏洞、让用户不再受安全漏洞的影响,做大量无用的补漏和升级工作。
其实,个人认为 Fastjson 经常爆出的安全漏洞,并不是说 Fastjson 本身不好,反而是因为 Fastjson 太过方便所导致,这会在后面内容中详细解释。
(2)Fastjson
可能会有人有疑惑,既然你主要使用的是 Jackson
,什么系统中还要依赖 Fastjson
?
依赖 Fastjson
并不代表就会使用这个组件,之所以这样做的原因是:
- 我个人主张使用
Jackson
,并不代表所有人都喜欢使用Jackson
。 - 我个人在意
Fastjson
的安全性问题,不代表所有用户都在意。 - Java 生态圈中,还是有很多第三方组件使用了
Fastjson
,只要使用了这些组件就会间接的依赖Fastjson
,而这些组件并不是所有都会像 Dante Cloud 一样随时更新依赖组件的版本,那么就很可能间接依赖了有安全漏洞的Fastjson
版本。通过在 Dante Cloud 中指定依赖Fastjson
的版本,就可以统一所有依赖中Fastjson
的版本,如果发现漏洞版本可以及时通过更新版本修复问题。
(3)Gson
不同的组件即使用途是相似的,但是由于设计和实现的思路不同,也会又各自的特色和特点存在,这也为在不同场景下选择更适合自己的机制提供了条件。
目前 Dante Cloud 仅在处理 SQL 注入的代码中使用了 Gson
。并不是说 Gson
在 SQL 注入方面有什么特别的支持,而是在实际应用中发现,Gson 在 JSON 的遍历方面特别是对结构复杂的 JSON 遍历,API 的使用更加简洁方便。
(4)Hutool JSON 工具
因为 Dante Cloud 默认是依赖了 Hutool-all 这个包,所以自然就包含了 hutool JSON 工具,是否使用全凭用户自己选择
[二]Spring Boot 中的 Jackson
[1]基本用法
ObjectMapper
是 Jackson
最核心的、最底层的类,当我们想要使用 Jackson
处理 JSON 时,只要初始化一个 ObjectMapper
对象,例如:ObjectMapper objectMapper = new ObjectMapper()
,就可以编写各种各样的 JSON 处理逻辑了。
在实际的开发中,只是通过 new ObjectMapper()
对象,是不足以满足使用需求的。比如,在对象转换成 JSON 时,我们不希望结果中包含有空值的属性。那么就需要对 ObjectMapper
对象进行一些参数的设置,当然 ObjectMapper
也提供了丰富的自定义参数,方便用户定义自己的满足自己需求的 ObjectMapper
对象。
[2]存在的问题
但这也带来的新的问题,就是每次在使用 Jackson
的时候都需要 new ObjectMapper()
对象,然后再把对应的参数设置一遍,就非常不方便。常见的解决方法就是会定义一个单例模式的 Utils,这样就不用每次重新创建 ObjectMapper
对象,还可以统一 ObjectMapper
中的参数配置。虽然这种方法在一定程度上解决了问题,但其实还是不够灵活,还存在几方面的问题:
- 一旦需要修改参数就需要修改代码;
- 这种方式是全局的方式,一旦设置就很难做差异化处理。
- Utils 的方式仅能统一自己的代码,第三方组件中如果也用了
Jackson
,就很难统一还是需要个性化处理
为了解决这个问题,在旧版本的 Spring Boot 中,默认的会帮我们创建一个 Bean 形式的 ObjectMapper
对象,同时利用 Spring Boot 的 Properties 机制,实现可以通过配置文件来修改 ObjectMapper
中的参数。
在 application.properties
中,修改配置是修改默认 ObjectMapper
最便捷的方式。
下面是 Jackson
配置的常规结构
spring.jackson.<category_name>.<feature_name>=true,false
以关闭 Jackson
SerializationFeature.WRITE_DATES_AS_TIMESTAMPS
配置举例,Spring Boot 中对应的配置如下:
spring.jackson.serialization.write-dates-as-timestamps=false
除了上述功能类别之外,我们还可以配置属性包含:
spring.jackson.default-property-inclusion=always, non_null, non_absent, non_default, non_empty
这种方式进一步提升了 ObjectMapper
配置的灵活性,通过 Bean 注入的方式还可以实现 ObjectMapper
的统一,但是还是不足以完美解决使用 ObjectMapper
存在的问题:
- 这种方法的缺点是,我们无法自定义高级选项,如
LocalDateTime
的自定义日期格式,解决这个问题最终还是要回归到在代码中进行设置 - Spring Boot 中允许定义多个
ObjectMapper
Bean,不同的组件会根据自己的需求定义自己的ObjectMapper
Bean,这样就又变成无法全局统一ObjectMapper
对象。
[3]使用@Primary注解
在代码开发中,很多问题和设计都会无法做到“完美”的,更多的时候必须要“平衡”和“取舍”。
在使用了 Spring Boot 的项目中,最常见的方式就是自己重新定义 ObjectMapper
Bean,在这个 Bean 中进行自己的参数设置,再利用 @Primary
注解标注这个 Bean,示例代码如下所示
使用了
@Primary
注解,所有注入ObjectMapper
Bean 的代码,包括第三方组件都只会注入@Primary
注解标注的ObjectMapper
Bean 。
@Bean
@Primary
public ObjectMapper objectMapper() {
JavaTimeModule module = new JavaTimeModule();
module.addSerializer(LOCAL_DATETIME_SERIALIZER);
return new ObjectMapper()
.setSerializationInclusion(JsonInclude.Include.NON_NULL)
.registerModule(module);
}
上面这种方式,本质就是一种“取舍”:“取”的是全局性配置,“舍”的是个性化配置
[三]Spring Boot 中的 Customizer
[1]Customizer
设计模式原理
在现今的 Spring Boot 中(只要不是太老的版本),Spring Boot 采用了一种我称之为 Customizer
设计模式来支持 Jackson
的配置。
Customizer
设计模式的实现是利用了 Spring Boot 中一项隐藏的技能:同一个接口的不同实现类的 Bean,可以被注入到以该接口为了类型的集合中
说得很绕口,下面举例解释一下:
假设,我们先定义一个接口(interface) A
public interface A {
······
}
A
这个接口,有两个实现类,分别为 B
和 C
public class B implements A {
······
}
public class C implements A {
······
}
那么,我们就可以通过注入的方式,动态获得基类为 A
的所有Bean。在下面例子中,我们首先定义 B
和 C
两个 Bean
@Configuration(proxyBeanMethods = false)
public class BeanConfiguration {
@Bean
public A classB() {
B b = new B();
return b;
}
@Bean
public A classB() {
C c = new C();
return c;
}
}
然后,我们就可以通过下面两种写法获取到 B
和 C
的集合
- 写法一
@Service
public class CollectionService {
@Autowired
private List<A> alist = new ArrayList();
}
- 写法二
@Configuration(proxyBeanMethods = false)
public class CollectionConfiguration {
@Bean
public D classD(List<A> alist) {
return new D;
}
}
上面两个例子中,alist
集合中就会自动存入已经配置好的 B
和 C
两个 Bean。
注意:前提一定是要
B
和C
两个 Bean在,List<A> alist
这段代码之前就已经注入和存在了才行
提示
这种同一类型基类的 Bean 会自动注入集合的方式,在 Spring Boot 中被大量使用。除了前面说的 Customizer
模式,还可以延伸出 策略模式
。
在 Dante Cloud 中大量使用了这种策略模式
,比如 Access 模块中的策略。Dante Cloud 也借鉴 Customizer
模式,重构了自身的自定义错误码体系
[2] Jackson2ObjectMapperBuilderCustomizer
那么 Spring Boot 是如何利用 Customizer
设计模式来扩展 Jackson
的呢?
Spring Boot 对 Jackson
的基础扩展,是 spring-boot-autoconfigure
包的 JacksonAutoConfiguration
类中完成。下面来看看JacksonAutoConfiguration
类中具体的实现代码
第一步:首先进行 StandardJackson2ObjectMapperBuilderCustomizer
Bean 的注入
// org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(Jackson2ObjectMapperBuilder.class)
@EnableConfigurationProperties(JacksonProperties.class)
static class Jackson2ObjectMapperBuilderCustomizerConfiguration {
@Bean
StandardJackson2ObjectMapperBuilderCustomizer standardJacksonObjectMapperBuilderCustomizer(
JacksonProperties jacksonProperties, ObjectProvider<Module> modules) {
return new StandardJackson2ObjectMapperBuilderCustomizer(jacksonProperties, modules.stream().toList());
}
static final class StandardJackson2ObjectMapperBuilderCustomizer implements Jackson2ObjectMapperBuilderCustomizer, Ordered {
private final JacksonProperties jacksonProperties;
private final Collection<Module> modules;
StandardJackson2ObjectMapperBuilderCustomizer(JacksonProperties jacksonProperties, Collection<Module> modules) {
this.jacksonProperties = jacksonProperties;
this.modules = modules;
}
@Override
public int getOrder() {
return 0;
}
@Override
public void customize(Jackson2ObjectMapperBuilder builder) {
if (this.jacksonProperties.getDefaultPropertyInclusion() != null) {
builder.serializationInclusion(this.jacksonProperties.getDefaultPropertyInclusion());
}
if (this.jacksonProperties.getTimeZone() != null) {
builder.timeZone(this.jacksonProperties.getTimeZone());
}
configureFeatures(builder, FEATURE_DEFAULTS);
configureVisibility(builder, this.jacksonProperties.getVisibility());
configureFeatures(builder, this.jacksonProperties.getDeserialization());
configureFeatures(builder, this.jacksonProperties.getSerialization());
configureFeatures(builder, this.jacksonProperties.getMapper());
configureFeatures(builder, this.jacksonProperties.getParser());
configureFeatures(builder, this.jacksonProperties.getGenerator());
configureDateFormat(builder);
configurePropertyNamingStrategy(builder);
configureModules(builder);
configureLocale(builder);
configureDefaultLeniency(builder);
configureConstructorDetector(builder);
}
······
在这个过程中,会将 application.properties
中相关的配置,通过 Jackson2ObjectMapperBuilderCustomizer
方式设置进来。
这里可以注意到,StandardJackson2ObjectMapperBuilderCustomizer
不仅实现了 Jackson2ObjectMapperBuilderCustomizer
接口,还实现了 Ordered
接口。通过 Ordered
接口,来控制不同 Customizer 的配置顺序。
StandardJackson2ObjectMapperBuilderCustomizer
的 Order 被设置为 “0”,意味着其它 Customizer 的 Order 值,如果比 0 小,就在 StandardJackson2ObjectMapperBuilderCustomizer
之前配置;如果比 0 大,则在 StandardJackson2ObjectMapperBuilderCustomizer
之后配置。
第二歩:进行 Jackson2ObjectMapperBuilder
Bean 的注入
// org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(Jackson2ObjectMapperBuilder.class)
static class JacksonObjectMapperBuilderConfiguration {
@Bean
@Scope("prototype")
@ConditionalOnMissingBean
Jackson2ObjectMapperBuilder jacksonObjectMapperBuilder(ApplicationContext applicationContext, List<Jackson2ObjectMapperBuilderCustomizer> customizers) {
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
builder.applicationContext(applicationContext);
customize(builder, customizers);
return builder;
}
private void customize(Jackson2ObjectMapperBuilder builder, List<Jackson2ObjectMapperBuilderCustomizer> customizers) {
for (Jackson2ObjectMapperBuilderCustomizer customizer : customizers) {
customizer.customize(builder);
}
}
}
在这个过程中,会拿到所有的 Jackson2ObjectMapperBuilderCustomizer
类型的Bean,将其对应的配置,设置到 Jackson2ObjectMapperBuilder
中。
注意:这里如上一部分所说,会根据不同 Customizer 的 Order 顺序进行处理,这就意味着会存在相同配置的覆盖。
第三步:进行 ObjectMapper
Bean 的注入
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(Jackson2ObjectMapperBuilder.class)
static class JacksonObjectMapperConfiguration {
@Bean
@Primary
@ConditionalOnMissingBean
ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {
return builder.createXmlMapper(false).build();
}
}
这里通过已经设置好各项配置的 Jackson2ObjectMapperBuilder
,来创建 ObjectMapper
。这里使用了 @Primary
,那么所有通过注入方式获取到的 ObjectMapper
对象将会是同一个对象。
第四步:进入自定义的MappingJackson2HttpMessageConverter的注入
@Bean
public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter(ObjectMapper objectMapper) {
log.trace("[Herodotus] |- Bean [Jackson2 Http Message Converter] Auto Configure.");
return new MappingJackson2HttpMessageConverter(objectMapper);
}
总结
- 以上所说的第一步、第二步等完全是根据跟踪代码、过程分析而得来的经验之谈,并不是实际存在这样的步骤过程。
- 经过分析,以上四步所涉及到
ObjectMapper
是同一个对象。在其它代码中,使用@Autowaire
注入的ObjectMapper
也是同一个对象。 - 如果在某个代码中,使用下面方式创建新的
ObjectMapper
, 那么这个对象与上面所说的对象,是不同的对象,即所有的配置对这个新创建的对象,不生效。
ObjectMapper objectMapper = new ObjectMapper()
[四]为什么 Fastjson 总有安全漏洞?
不可否认,任何一个组件出现安全漏洞,和这个组件代码的设计及实现逻辑肯定有密不可分的关系。
对于 JSON 处理组件来说,不管用哪个组件都需要支持反序列化
,即:JSON 可以转换成对象,这就是 JSON 组件产生安全漏洞的根源。
究其原理其实和我们所熟知的 SQL 注入是一个道理。SQL 注入就是将违规内容作为参数传递给了 JDBC,最终导致拼装出一个会产生异常操作结果的 SQL 语句。JSON 也是一样,在实际开发中 JSON 就是一个中间传输介质,单纯的 JSON 是无法执行任何操作的,我们需要把 JSON 反序列化为对象,即把 JSON 中的值 set
到对象指定属性中,其实就相当于 SQL 注入中的传参,从而会导致产生安全隐患。
由于不同的 JSON 实现机制和用法的不同,表现出的安全问题也会有差异。
[1]Fastjson
相信大家之所以选择 Fastjson
,除了早期宣传的 Fastjson
有多快以外,我想还有一个关键点就是 Fastjson
用着非常方便。
Fastjson
的序列化自然不用说,反序列化也非常方便,除非特别复杂的类型转换,比如多层泛型嵌套外,基本上用一个方法就可以搞定反序列化。
这就意味着,所有的安全问题都需要有 Fastjson
来帮你处理和考虑,一旦有遗漏或者考虑不周的地方,就容易出现安全漏洞。
[2]Jackson
通过以上的分析可以得出结论,Jackson
也支持 JSON 的反序列化,肯定也会出现安全漏洞,出现漏洞大概率也是出现在反序列化的过程中。
但是,Jackson
或者说使用 Jackson
的组件,采用了另外的一种降低安全风险的办法,就是尽量由用户自己来降低安全风险
- 在使用方面,除了
Jackson
所谓“慢”以外,规则要求也相对的多一些,特别对于复杂类型的反序列化,通常需要用户自己写大量的代码来适配,否则很难反序列化成功(想必这也是很多开发人员不愿意用Jackson
的原因之一)。 - 另外,
Jackson
提供了强大的扩展能力,这给使用Jackson
的第三方组件提供了更多的想象空间,间接的分散出去大量的安全问题,由第三方组件自己设计提升安全。
这也就是为什么使用新版本 Spring Security, 要求对 Spring Security 中核心类的扩展类,必须手动编写 Mixin
代码,同时需要将这些代码手动加入白名单
,否则在不仅不允许运行,运行的时候就会出现 XXX is not in the allowlist.If you believe this class is safe to deserialize, please provide an explicit mapping using Jackson annotations or by providing a Mixin.
错误提示。详细原因会在后续讲解
[五]Jackson 中的 Mixin 和 Module
[1]Mixin
Mixin
对于前端开发者可不陌生,Vue、React 等知名前端框架都使用了 Mixin
。而对于后端开发,尤其是Java后端开发来说 Mixin
却是一个很陌生的概念。
Mixin
的意思是混入,是 Jackson
中一个非常重要的概念和机制。为什么需要混入?比如我们引用了一个Jar包,其中的某个类在某个场景需要反序列化,但是这个类没有提供默认构造。那么该怎么办呢?把原来的项目拉下来,重写一下?下下策! 你可以使用 Jackson
提供的 Mixin
特性来解决这个问题。
Jackson
中 Mixin
(混入) 主要的用途是:在目标对象无法实现的序列化或反序列化时,通过配置一个混入对象,在序列化或反序列化的时候把这些个性化配置混入到目标对象中,从而达到序列化或反序列化目标对象的目的。混入不改变目标对象本身的任何特性,混入对象和目标对象是映射的关系。下面我们来看一个混入的例子:
(1)新建一个特殊实例
假设,我们有一个 User
类,为了演示需要,我们极端一些,这个User没有无参构造,也没有属性的 getter方法。
public class User {
private final String name;
private final Integer age;
public User(String name, Integer age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
(2) 编写Mixin类
想对这个极端的 User
进行序列化和反序列化。按以前的玩法我们会在在 User
类上加上 @JsonAutoDetect
注解就可以实现序列化了;加上 @JsonDeserialize
注解并指定反序列化类就可以反序列化了。不过现在我们不需要对 User
进行任何更改,只需要编写一个 Mixin
类把上述两个注解配置好就可以了。
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE,
isGetterVisibility = JsonAutoDetect.Visibility.NONE)
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonDeserialize(using = UserMixin.UserDeserializer.class)
public abstract class UserMixin {
/**
* 反序列化类
**/
static class UserDeserializer extends JsonDeserializer<User> {
@Override
public User deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
ObjectMapper mapper = (ObjectMapper) p.getCodec();
JsonNode jsonNode = mapper.readTree(p);
String name = readJsonNode(jsonNode, "name").asText(null);
String age = readJsonNode(jsonNode, "age").asText(null);
Integer ageVal = Objects.isNull(age)? null: Integer.valueOf(age);
return new User(name,ageVal);
}
private JsonNode readJsonNode(JsonNode jsonNode, String field) {
return jsonNode.has(field) ? jsonNode.get(field) : MissingNode.getInstance();
}
}
}
(3)Mixin 映射目标类
编写完 Mixin
类后,我们通过ObjectMapper
中的 addMixIn
方法把UserMixin
和User
映射起来。并编写一个序列化和反序列化的例子。
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.addMixIn(User.class, UserMixin.class);
User test = new User("test", 12);
String json = objectMapper.writeValueAsString(test);
//{"name":"test","age":12}
System.out.println("json = " + json);
String jsonStr = "{\"name\":\"test\",\"age\":12}";
User user = objectMapper.readValue(jsonStr, User.class);
// User{name='test', age=12}
System.out.println("user = " + user);
这样我们在不对目标类进行任何改变的情况下实现了个性化的JSON序列化和反序列化。
[2]Module
Jackson
还提供了模块化功能,可以将个性化配置进行模块化统一管理,而且可以按需引用,甚至可插拔。它同样能够管理一组 Mixin
。声明一个 Jackson Module 非常简单,继承 SimpleModule
覆写它的一些方法即可。针对 Mixin
我们可以这样写:
public class UserModule extends SimpleModule {
public UserModule() {
super(UserModule.class.getName());
}
@Override
public void setupModule(SetupContext context) {
context.setMixInAnnotations(User.class,UserMixin.class);
}
}
Module 同样可以注册到 ObjectMapper
中,同样也能实现我们想要的效果:
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new UserModule());
Module的功能更加强大。平常我们会使用以下几个 Module:
- jackson-module-parameter-names :此模块能够访问构造函数和方法参数的名称
- jackson-datatype-jdk8:除了Java8的时间API外其它新特性的的支持
- jackson-datatype-jsr310 :用以支持Java8新增的JSR310时间API
另外 Spring Security 也提供了 Module 支持 SecurityJackson2Modules
,它包含了下面的一些模块:
private static final List<String> securityJackson2ModuleClasses = Arrays.asList(
"org.springframework.security.jackson2.CoreJackson2Module",
"org.springframework.security.cas.jackson2.CasJackson2Module",
"org.springframework.security.web.jackson2.WebJackson2Module",
"org.springframework.security.web.server.jackson2.WebServerJackson2Module");
private static final String webServletJackson2ModuleClass = "org.springframework.security.web.jackson2.WebServletJackson2Module";
private static final String oauth2ClientJackson2ModuleClass = "org.springframework.security.oauth2.client.jackson2.OAuth2ClientJackson2Module";
private static final String javaTimeJackson2ModuleClass = "com.fasterxml.jackson.datatype.jsr310.JavaTimeModule";
private static final String ldapJackson2ModuleClass = "org.springframework.security.ldap.jackson2.LdapJackson2Module";
private static final String saml2Jackson2ModuleClass = "org.springframework.security.saml2.jackson2.Saml2Jackson2Module";
[3]Module 的应用逻辑
经过长时间的调试代码发现,Jackson
在处理序列化的过程中,会利用所有的 Module
对同一个事物进行序列化或反序列化操作,就类似于 Spring Security Filter,像“链”一样用所有的 Module
处理一遍。
例如 Jackson
默认配置了 A、B、C 三个 Module,请求发送一个 DTO 到接口,Jackson 会用A、B、C 每一个 Module 对这个DTO 处理一遍。添加一个新的 Jackson2ObjectMapperBuilderCustomizer
,在其中新增 D 和 E 两个 Module,那么这时就会按照顺序,用 A、B、C、D、E 把这个 DTO 处理一遍。
[六]Spring Authorization Server
中的 Jackson 的特殊处理
[1]Spring Security 中的 Jackson安全
在 Sring Security 中就通过注解混入来解决反序列化时安全配置问题。
@Override
public JavaType typeFromId(DatabindContext context, String id) throws IOException {
DeserializationConfig config = (DeserializationConfig) context.getConfig();
JavaType result = this.delegate.typeFromId(context, id);
String className = result.getRawClass().getName();
if (isInAllowlist(className)) {
return result;
}
boolean isExplicitMixin = config.findMixInClassFor(result.getRawClass()) != null;
if (isExplicitMixin) {
return result;
}
JacksonAnnotation jacksonAnnotation = AnnotationUtils.findAnnotation(result.getRawClass(),
JacksonAnnotation.class);
if (jacksonAnnotation != null) {
return result;
}
throw new IllegalArgumentException("The class with " + id + " and name of " + className
+ " is not in the allowlist. "
+ "If you believe this class is safe to deserialize, please provide an explicit mapping using Jackson annotations or by providing a Mixin. "
+ "If the serialization is only done by a trusted source, you can also enable default typing. "
+ "See https://github.com/spring-projects/spring-security/issues/4370 for details");
}
从上面的代码可以看出 反序列化时只允许满足三种条件的类型执行:Spring Security 的白名单内、被混入、被JacksonAnnotation注解。
提示
Spring Security 为什么要求对核心类的序列化使用混入呢?
因为前文在讲解 Jackson Mixin 时提过,Mixin 主要用于在不修改“外部”代码的情况下,对“外部”的代码进行序列化操作。
在使用 Spring Security 时,通常都需要对其核心类进行扩展,才能满足我们的实际应用需求。站在 Spring Security 角度,我们扩展的这些类对于 Spring Security 来说就是“外部”类,想要 Spring Security 帮你完成序列化就需要你提供 Mixin
。
当然,最核心的还是前文提到的从安全性角度考虑,由用户自己来保证反序列化的安全性。
[2]Spring Authorization Server 中的 Jackson
想必大家在初次使用 Spring Authorization Server
时,就会遇到代码无法运行,要求你增加白名单的问题。
之所以会出现这个问题,原因就是可以把 Spring Authorization Server
理解为在 Spring Security 基础上“套了一层壳”。对 Spring Authorization Server
的大多数配置,本质上就是对底层 Spring Security 的配置,使用的还是 Spring Security 的核心内容。因此,在对一些核心类反序列化时,还是会用到的 Spring Security 的反序列化安全机制,所以就必须要编写对应的序列化 Mixin
。
在 Spring Authorization Server
中,核心内容都是存储在数据库中,而且有些字段是以 JSON 的形式进行存储,这一块内容就是需要自己编写 Mixin
的部分。
Dante Cloud 是用 JPA 来实现的 Spring Authorization Server
数据存储。在这里就编写了大量的 Jackson
Mixin
代码。
在Spring Authorization Server
JPA 实现类中设置自定义的 Mixin
public OAuth2JacksonProcessor() {
objectMapper = new ObjectMapper();
ClassLoader classLoader = OAuth2JacksonProcessor.class.getClassLoader();
List<Module> securityModules = SecurityJackson2Modules.getModules(classLoader);
objectMapper.registerModules(securityModules);
objectMapper.registerModules(new OAuth2AuthorizationServerJackson2Module());
objectMapper.registerModules(new HerodotusJackson2Module());
objectMapper.registerModules(new OAuth2TokenJackson2Module());
}
[七]Dante Cloud 中的 Jackson 的配置
[1]配置方式
在 Dante Cloud 早期版本中,Jackson
的配置是采用前面章节描述的方式,即定义 ObjectMapper
Bean,并标注为 @Primary
。
这种方式确实可以做到“全局” 设置 ObjectMapper
,也是最简单的方式。但是采用这种方式,存在几个问题
JacksonAutoConfiguration
中,很多的 Bean 的配置将不再进行注入,包括:Jackson2ObjectMapperBuilder
。在这种模式下Jackson2ObjectMapperBuilderCustomizer
也不再生效- 同时,在
application.properties
中配置Jackson
相关内容也不再生效。所有的配置只能通过修改代码,在ObjectMapper
中配置。 - 这种配置方式在大多数情况下,只要不纠结细节,在使用上是没有任何问题的。但是,在需要有特殊处理的情况下,就会出现互相干扰
在 Dante Cloud 新的版本中,修改为使用 Jackson2ObjectMapperBuilderCustomizer
方式进行 ObjectMapper
的配置。即兼顾了全局统一设置ObjectMapper
,又同时支持配置文件配置,以及 Jackson配置的自定义。
最主要的优点是,你可以定义多个 Jackson2ObjectMapperBuilderCustomizer
接口的实现类来扩展 Jackson
,而且这些类可以分散在不同的代码模块中,这就实现了针对不同的模块可以有不同的 Jackson
定义。
[2]Jackson2Utils
虽然上面所述的方式,解决了 Jackson
配置中的大量问题,但是在使用中还是不太方便,因为需要你在所有用到 Jackson
的代码中,都注入 ObjectMapper
对象。
为了解决这个问题,Dante Cloud 在上述内容的基础之上,又封装了一个 Jackson2Utils
类。
Jackson2Utils
不仅封装了 Jackson
常用方法。而且为了解决 ObjectMapper
的统一,Jackson2Utils
类被定义为了 Spring Boot 的 @Component
,需要注入 ObjectMapper
。这样在需要使用 Jackson 的地方只需要调用 Jackson2Utils
静态方法即可。
提示
Jackson2Utils
类也是“静态类注入Bean”的典型示例
[3]注意事项
所有的 Jackson2ObjectMapperBuilderCustomizer
,其基本原理就是通过该接口向 Jackson2ObjectMapperBuilder
中设置属性。
这里存在一个问题,通过查看源代码可以发现,所有的 moduleToInstall
、modules
方法,都会重新设置 module
属性的值,而不是向其中增加新的值。这就意味着如果有两个 Jackson2ObjectMapperBuilderCustomizer
,每一个里面都用到了 moduleToInstall
或 modules
方法,后面设置的 modules
将会覆盖前面设置的 modules
,将会导致前面设置的 modules
失效。
为了解决这个问题,Dante Cloud 采用的二次设置的方式,先拿到已有的 Module,再增加新的 Module,然后再将所有的 Module 设置回去。
builder.modulesToInstall(
modules -> {
List<Module> install = new ArrayList<>(modules);
install.add(new EncapsulationClassJackson2Module());
install.add(new Jdk8Module());
install.add(new JavaTimeModule());
builder.modulesToInstall(toArray(install));
}
);
[4]Spring Authorization Server
中的特殊处理
Dante Cloud 对 Spring Authorization Server
中 Jackson
相关的处理,采用的是重新 new ObjectMapper()
的方式处理相关数据的序列化,并没有使用前文所说的Jackson2ObjectMapperBuilderCustomizer
方式。
最初,也考虑到使用 new ObjectMapper()
方式来处理 Spring Authorization Server
中 Jackson
的配置,不够优雅,没有与现有的 Dante Cloud Jackson
Customizer
模式形成统一体系。
也尝试过将 Spring Authorization Server
相关的 Jackson
配置代码,也定义为一个 Jackson2ObjectMapperBuilderCustomizer
。但是在实践的过程中发现,这种方式会反而会导致 Spring Authorization Server
运行就会出现序列化问题
经过调试代码发现,Jackson
Module
的“链”式机制(参见前文【Jackson 中的 Module】)。而 Spring Security Jackson
相关的 Module
与 Spring Boot 中默认配置的 Jackson
Module
有冲突,会让反序列化产生不同的结果,导致原本正常的反序列化失败。
因此,在 Dante Cloud Spring Authorization Server
JPA 的实现模块中,单独采用常规 new ObjectMapper()
的方式处理序列化和反序列化的问题,这就与系统全局配置的 ObjectMapper
形成了隔离,互补干扰互不影响。