1. Spring Boot 4 OAuth2 Jackson 3 升级实战:解决类型解析异常
最近在将项目升级到 Spring Boot 4 和 Jackson 3 的过程中,遇到了一个棘手的 OAuth2 授权服务序列化问题。这个问题困扰了我好几天,最终通过深入源码分析找到了解决方案。下面我将详细记录这个问题的排查过程和最终解决方案。
1.1 问题现象与错误分析
升级后运行时出现的核心错误如下:
code复制Caused by: tools.jackson.databind.exc.InvalidTypeIdException:
Could not resolve type id 'com.fusion.security.SecurityUser' as a subtype of `java.lang.Object`:
Configured `PolymorphicTypeValidator` (of type `tools.jackson.databind.jsontype.BasicPolymorphicTypeValidator`) denied resolution
这个错误表明 Jackson 在反序列化过程中无法正确处理 SecurityUser 类型的多态解析。具体来说:
- Jackson 3 引入了更严格的多态类型验证机制(
PolymorphicTypeValidator) - 默认的验证器会拒绝未明确配置的类型解析
- 在 OAuth2 授权信息的存储过程中,系统尝试将
SecurityUser类型作为Principal存储
关键点:Jackson 3 相比 Jackson 2 在安全性和类型验证方面做了重大改进,这导致了一些在 Jackson 2 下能正常工作的代码在升级后出现问题。
1.2 问题根源定位
通过追踪源码,发现问题出在 JdbcOAuth2AuthorizationService 的序列化机制上:
JdbcOAuth2AuthorizationService使用默认的 Jackson 映射器来序列化/反序列化授权信息- 授权信息中包含 Principal 对象(这里是
SecurityUser) - 默认配置无法正确处理自定义 Principal 类型的多态序列化
特别需要注意的是,Spring Security OAuth2 的授权服务默认实现没有提供自定义 Jackson 映射器的接口,这导致我们无法直接配置类型处理。
2. 解决方案设计与实现
2.1 解决方案概述
要解决这个问题,我们需要:
- 继承
JdbcOAuth2AuthorizationService创建自定义实现 - 提供可配置的
JsonMapper实例 - 重写行映射逻辑以使用自定义的 JSON 处理
2.2 自定义授权服务实现
以下是完整的解决方案代码:
java复制public class CustomOAuth2AuthorizationService extends JdbcOAuth2AuthorizationService {
public CustomOAuth2AuthorizationService(JdbcOperations jdbcOperations,
RegisteredClientRepository registeredClientRepository,
JsonMapper jsonMapper) {
super(jdbcOperations, registeredClientRepository);
// 使用自定义的 RowMapper 并传入 JsonMapper
setAuthorizationRowMapper(new CustomOAuth2AuthorizationRowMapper(
registeredClientRepository,
jsonMapper
));
}
// 可选:根据需要重写其他方法
@Override
public void save(OAuth2Authorization authorization) {
super.save(authorization);
}
@Override
public OAuth2Authorization findById(String id) {
return super.findById(id);
}
}
2.3 自定义行映射器实现
关键部分是自定义的行映射器,它负责处理授权信息的序列化和反序列化:
java复制public class CustomOAuth2AuthorizationRowMapper implements RowMapper<OAuth2Authorization> {
private final RegisteredClientRepository registeredClientRepository;
private final JsonMapper jsonMapper;
public CustomOAuth2AuthorizationRowMapper(RegisteredClientRepository registeredClientRepository,
JsonMapper jsonMapper) {
this.registeredClientRepository = registeredClientRepository;
this.jsonMapper = jsonMapper;
}
@Override
public OAuth2Authorization mapRow(ResultSet rs, int rowNum) throws SQLException {
// 从数据库读取数据
String registeredClientId = rs.getString("registered_client_id");
String principalName = rs.getString("principal_name");
String authorizationGrantType = rs.getString("authorization_grant_type");
String attributesJson = rs.getString("attributes");
// 使用自定义 JsonMapper 反序列化属性
Map<String, Object> attributes;
try {
attributes = jsonMapper.readValue(attributesJson, new TypeReference<Map<String, Object>>() {});
} catch (JsonProcessingException e) {
throw new IllegalArgumentException(e.getMessage(), e);
}
// 构建授权对象
RegisteredClient registeredClient = this.registeredClientRepository.findById(registeredClientId);
if (registeredClient == null) {
throw new DataRetrievalFailureException(
"The RegisteredClient with id '" + registeredClientId + "' was not found in the RegisteredClientRepository.");
}
return OAuth2Authorization.withRegisteredClient(registeredClient)
.id(rs.getString("id"))
.principalName(principalName)
.authorizationGrantType(new AuthorizationGrantType(authorizationGrantType))
.attributes(attrs -> attrs.putAll(attributes))
.build();
}
}
2.4 配置自定义 JSON 映射器
为了正确处理多态类型,我们需要配置一个自定义的 JsonMapper:
java复制@Bean
public JsonMapper jsonMapper() {
ObjectMapper objectMapper = new ObjectMapper();
// 配置多态类型处理
objectMapper.activateDefaultTyping(
objectMapper.getPolymorphicTypeValidator(),
ObjectMapper.DefaultTyping.EVERYTHING,
JsonTypeInfo.As.PROPERTY
);
// 注册自定义类型模块
SimpleModule module = new SimpleModule();
module.addAbstractTypeMapping(Principal.class, SecurityUser.class);
objectMapper.registerModule(module);
return new JsonMapper(objectMapper);
}
2.5 最终服务配置
将自定义服务配置为 Spring Bean:
java复制@Bean
public OAuth2AuthorizationService oAuth2AuthorizationService(
JdbcTemplate jdbcTemplate,
RegisteredClientRepository registeredClientRepository,
JsonMapper jsonMapper) {
return new CustomOAuth2AuthorizationService(
jdbcTemplate,
registeredClientRepository,
jsonMapper
);
}
3. 实现细节与原理分析
3.1 Jackson 3 的类型安全改进
Jackson 3 引入了更严格的类型安全机制,主要变化包括:
- 默认启用多态类型验证:所有多态反序列化操作都必须通过类型验证
- 更严格的默认验证器:
BasicPolymorphicTypeValidator默认拒绝所有未明确允许的类型 - 显式类型配置要求:必须明确声明哪些类型可以参与多态处理
这些改进提高了安全性,但也带来了迁移挑战。
3.2 OAuth2 授权信息的序列化机制
Spring Security OAuth2 授权服务存储以下信息:
- 授权元数据(客户端ID、授权类型等)
- Token 信息
- 用户主体(Principal)
- 自定义属性
其中,Principal 和自定义属性需要序列化为 JSON 存储到数据库。
3.3 自定义序列化的必要性
默认实现有以下限制:
- 使用内部
ObjectMapper实例,无法自定义配置 - 没有提供扩展点来配置多态类型处理
- 对自定义 Principal 类型支持不足
因此,我们需要完全接管序列化过程。
4. 常见问题与解决方案
4.1 类型验证失败的其他场景
除了 Principal 类型,还可能会遇到以下类似问题:
- 自定义 Token 类型:如果使用了自定义的 OAuth2Token 实现
- 复杂属性类型:授权属性中包含自定义类型
解决方案是在 JsonMapper 配置中注册这些类型:
java复制module.addAbstractTypeMapping(OAuth2Token.class, CustomToken.class);
module.addAbstractTypeMapping(SomeInterface.class, SomeImplementation.class);
4.2 性能考虑
自定义 JSON 处理可能会影响性能,建议:
- 重用
JsonMapper实例 - 考虑添加缓存层
- 对频繁访问的授权信息实现缓存
4.3 迁移注意事项
从旧版本迁移时需要注意:
- 已有数据的兼容性
- 可能需要数据迁移脚本
- 逐步验证各个授权场景
5. 最佳实践与经验总结
5.1 配置建议
- 明确类型映射:尽可能明确指定所有需要多态处理的类型
- 最小权限原则:只允许必要的类型参与多态处理
- 日志记录:添加适当的日志记录以跟踪序列化/反序列化过程
5.2 调试技巧
遇到序列化问题时可以:
- 启用 Jackson 的调试日志
- 检查类型继承层次
- 验证类型验证器配置
5.3 扩展思考
这个解决方案的模式可以应用于:
- 其他需要自定义序列化的 Spring 组件
- 复杂领域对象的持久化
- 多态类型处理的通用解决方案
我在实际项目中还发现,这种自定义方式虽然解决了眼前的问题,但也带来了一些维护成本。后续可以考虑将这些自定义配置集中管理,或者创建一个通用的序列化框架来统一处理这类问题。