You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

升级Spring Boot至2.0.0后,password授权模式OAuth2令牌异常

Spring Boot 2.0 OAuth2 Password模式重复获取Token触发MappingMongoConverter异常的解决方案

我之前在升级Spring Boot到2.0版本时,也碰到过类似的OAuth2+MongoDB兼容性问题,结合你的场景,来帮你梳理下问题根源和解决办法:

问题根源分析

Spring Boot 2.0对Spring Data MongoDB的默认配置做了不少调整,尤其是MappingMongoConverter的行为。当你用password模式首次获取Token时,OAuth2的MongoTokenStore能把Token对象存入MongoDB,但后续读取(用Token访问接口)或重复获取同一用户的Token时,Converter无法正确解析之前存储的文档结构——因为1.5版本的默认序列化逻辑和2.0版本不兼容,导致类型转换异常。

另外,你添加的oauth2-autoconfigure修复了基础的OAuth2功能,但并没有处理MongoDB和OAuth2对象的序列化适配问题,这就是为什么client_credentials模式(不需要关联用户信息)没问题,但password模式(关联用户认证信息)后续操作会报错。

分步解决方案

1. 自定义MongoDB Converter处理OAuth2对象

你需要创建一个配置类,给MappingMongoConverter添加针对OAuth2核心对象的自定义序列化/反序列化规则,确保MongoDB能正确读写OAuth2AccessToken、OAuth2Authentication、OAuth2RefreshToken这些对象:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.data.mongodb.core.convert.MongoCustomConversions;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.common.DefaultOAuth2RefreshToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2RefreshToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.store.mongodb.MongoTokenStore;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.*;

@Configuration
public class MongoOAuth2TokenConfig {

    @Bean
    public MongoTokenStore mongoTokenStore(MongoTemplate mongoTemplate) {
        MongoTokenStore tokenStore = new MongoTokenStore(mongoTemplate);
        // 配置自定义Converter
        MappingMongoConverter converter = (MappingMongoConverter) mongoTemplate.getConverter();
        converter.setCustomConversions(new MongoCustomConversions(
                Arrays.asList(
                        new OAuth2AccessTokenReadConverter(),
                        new OAuth2AccessTokenWriteConverter(),
                        new OAuth2AuthenticationReadConverter(),
                        new OAuth2AuthenticationWriteConverter(),
                        new OAuth2RefreshTokenReadConverter(),
                        new OAuth2RefreshTokenWriteConverter()
                )
        ));
        converter.afterPropertiesSet();
        return tokenStore;
    }

    // 读取MongoDB文档转OAuth2AccessToken
    static class OAuth2AccessTokenReadConverter implements Converter<Map<String, Object>, OAuth2AccessToken> {
        @Override
        public OAuth2AccessToken convert(Map<String, Object> source) {
            DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken((String) source.get("value"));
            token.setExpiration((Date) source.get("expiration"));
            token.setRefreshToken((OAuth2RefreshToken) source.get("refreshToken"));
            token.setScope((Collection<String>) source.get("scope"));
            token.setAdditionalInformation((Map<String, Object>) source.get("additionalInformation"));
            return token;
        }
    }

    // 写入OAuth2AccessToken转MongoDB文档
    static class OAuth2AccessTokenWriteConverter implements Converter<OAuth2AccessToken, Map<String, Object>> {
        @Override
        public Map<String, Object> convert(OAuth2AccessToken source) {
            Map<String, Object> doc = new HashMap<>();
            doc.put("value", source.getValue());
            doc.put("expiration", source.getExpiration());
            doc.put("refreshToken", source.getRefreshToken());
            doc.put("scope", source.getScope());
            doc.put("additionalInformation", source.getAdditionalInformation());
            return doc;
        }
    }

    // 处理OAuth2Authentication的反序列化
    static class OAuth2AuthenticationReadConverter implements Converter<Map<String, Object>, OAuth2Authentication> {
        @Override
        public OAuth2Authentication convert(Map<String, Object> source) {
            ObjectMapper mapper = new ObjectMapper();
            try {
                return mapper.readValue(mapper.writeValueAsString(source), OAuth2Authentication.class);
            } catch (JsonProcessingException e) {
                throw new RuntimeException("Failed to deserialize OAuth2Authentication", e);
            }
        }
    }

    // 处理OAuth2Authentication的序列化
    static class OAuth2AuthenticationWriteConverter implements Converter<OAuth2Authentication, Map<String, Object>> {
        @Override
        public Map<String, Object> convert(OAuth2Authentication source) {
            ObjectMapper mapper = new ObjectMapper();
            try {
                return mapper.readValue(mapper.writeValueAsString(source), Map.class);
            } catch (JsonProcessingException e) {
                throw new RuntimeException("Failed to serialize OAuth2Authentication", e);
            }
        }
    }

    // 处理OAuth2RefreshToken的反序列化
    static class OAuth2RefreshTokenReadConverter implements Converter<Map<String, Object>, OAuth2RefreshToken> {
        @Override
        public OAuth2RefreshToken convert(Map<String, Object> source) {
            return new DefaultOAuth2RefreshToken((String) source.get("value"));
        }
    }

    // 处理OAuth2RefreshToken的序列化
    static class OAuth2RefreshTokenWriteConverter implements Converter<OAuth2RefreshToken, Map<String, Object>> {
        @Override
        public Map<String, Object> convert(OAuth2RefreshToken source) {
            Map<String, Object> doc = new HashMap<>();
            doc.put("value", source.getValue());
            return doc;
        }
    }
}

2. 清理MongoDB中的旧Token数据

因为你之前已经用旧逻辑存储了一些Token文档,这些文档的结构和新Converter期望的不匹配,所以需要先清空MongoDB中的oauth_access_token和oauth_refresh_token集合,避免后续读取旧数据时触发异常。

3. 更新AuthorizationServer配置

确保你的授权服务器配置类中,使用自定义的MongoTokenStore,而不是默认实现:

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    private final AuthenticationManager authenticationManager;
    private final MongoTemplate mongoTemplate;

    // 构造注入依赖
    public AuthorizationServerConfig(AuthenticationManager authenticationManager, MongoTemplate mongoTemplate) {
        this.authenticationManager = authenticationManager;
        this.mongoTemplate = mongoTemplate;
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
                .authenticationManager(authenticationManager)
                .tokenStore(mongoTokenStore()); // 绑定自定义的TokenStore
    }

    @Bean
    public MongoTokenStore mongoTokenStore() {
        return new MongoOAuth2TokenConfig().mongoTokenStore(mongoTemplate);
    }

    // 其他客户端详情、权限配置按需保留
}

4. 校验依赖版本兼容性

确保你的spring-security-oauth2和oauth2-autoconfigure版本和Spring Boot 2.0.0.RELEASE匹配,推荐的版本组合:

<!-- Maven依赖示例 -->
<dependency>
    <groupId>org.springframework.security.oauth</groupId>
    <artifactId>spring-security-oauth2</artifactId>
    <version>2.3.8.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.security.oauth.boot</groupId>
    <artifactId>spring-security-oauth2-autoconfigure</artifactId>
    <version>2.0.0.RELEASE</version>
</dependency>

验证效果

完成上述配置后,重启服务:

  1. 用password模式首次获取Token,确认正常
  2. 用该Token访问受保护接口,确认能正常解析
  3. 再次用同一用户的password模式获取Token,确认不会触发MappingMongoConverter异常

内容的提问来源于stack exchange,提问作者Bilal Nasir

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.25 04:10:01