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

Spring Security OAuth2 Client对接Shopify报错:tokenType cannot be null

问题

我有一个Spring Boot应用,想用OAuth认证调用Shopify API,使用的是Spring Security OAuth2 Client库。OAuth流程已完成,但在交换授权码获取访问令牌时受阻。Shopify返回的访问令牌响应如下:

{
  "access_token": "f85632530bf277ec9ac6f649fc327f17",
  "scope": "write_orders,read_customers"
}

此时Spring Security抛出异常:tokenType cannot be null,来自OAuth2AccessToken第72行。我尝试过自定义配置,但OAuth2AccessToken.TokenType是final类,仅存在Bearer这一个实例。推测Shopify不将其视为Bearer令牌,因为它要求用自定义请求头X-Shopify-Access-Token而非Authentication头携带令牌。OAuth2AccessToken被OAuth2AuthorizationCodeAuthenticationToken使用,该类在OAuth2AuthorizationCodeGrantFilter#processAuthorizationResponse中调用,属于库核心逻辑,似乎无法自定义。请问如何用Spring Security OAuth2 Client支持这类访问令牌?

我曾尝试强制设为Bearer令牌类型临时解决,但担心后续库将其当作Bearer令牌处理引发问题,自定义转换器代码如下:

/**
 * Modified from DefaultMapOAuth2AccessTokenResponseConverter
 */
public class ShopifyAccessTokenResponseConverter implements Converter<Map<String, Object>, OAuth2AccessTokenResponse> {

    @Override
    public OAuth2AccessTokenResponse convert(Map<String, Object> source) {
        String accessToken = getParameterValue(source, OAuth2ParameterNames.ACCESS_TOKEN);
        OAuth2AccessToken.TokenType accessTokenType = OAuth2AccessToken.TokenType.BEARER; // as if I had a choice
        Set<String> scopes = getScopes(source);
        String refreshToken = getParameterValue(source, OAuth2ParameterNames.REFRESH_TOKEN);
        return OAuth2AccessTokenResponse.withToken(accessToken)
                                        .tokenType(accessTokenType)
                                        .scopes(scopes)
                                        .refreshToken(refreshToken)
                                        .build();
    }

    private static Set<String> getScopes(Map<String, Object> tokenResponseParameters) {
        if (tokenResponseParameters.containsKey(OAuth2ParameterNames.SCOPE)) {
            String scope = getParameterValue(tokenResponseParameters, OAuth2ParameterNames.SCOPE);
            return new HashSet<>(Arrays.asList(StringUtils.delimitedListToStringArray(scope, " ")));
        }
        return Collections.emptySet();
    }

    private static String getParameterValue(Map<String, Object> tokenResponseParameters, String parameterName) {
        Object obj = tokenResponseParameters.get(parameterName);
        return (obj != null) ? obj.toString() : null;
    }

}
解决方案

1. 保留自定义令牌响应转换器,强制设置TokenType为Bearer

你的临时方案是可行的,Spring Security OAuth2 Client核心逻辑仅要求TokenType不为null,不会强制按Bearer规则处理令牌,只要后续覆盖令牌的携带方式即可。

2. 自定义OAuth2请求拦截器,替换令牌携带方式

要让请求使用X-Shopify-Access-Token头而非默认的Authorization: Bearer <token>,需自定义OAuth2AuthorizedClientManager并添加请求拦截器修改请求头:

@Configuration
public class ShopifyOAuth2Config {

    @Bean
    public OAuth2AuthorizedClientManager authorizedClientManager(
            ClientRegistrationRepository clientRegistrationRepository,
            OAuth2AuthorizedClientService authorizedClientService) {

        OAuth2AuthorizedClientProvider authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder()
                .authorizationCode()
                .refreshToken()
                .build();

        DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(
                clientRegistrationRepository, authorizedClientService);
        authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);

        return authorizedClientManager;
    }

    @Bean
    public OAuth2RestTemplate shopifyRestTemplate(OAuth2AuthorizedClientManager authorizedClientManager) {
        ClientRegistration shopifyRegistration = authorizedClientManager.getClientRegistrationRepository()
                .findByRegistrationId("shopify");
        OAuth2RestTemplate restTemplate = new OAuth2RestTemplate(shopifyRegistration);

        // 添加拦截器替换Authorization头为X-Shopify-Access-Token
        restTemplate.getInterceptors().add((request, body, execution) -> {
            OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId("shopify")
                    .principal(SecurityContextHolder.getContext().getAuthentication())
                    .build();
            OAuth2AuthorizedClient authorizedClient = authorizedClientManager.authorize(authorizeRequest);
            
            if (authorizedClient != null && authorizedClient.getAccessToken() != null) {
                request.getHeaders().remove(HttpHeaders.AUTHORIZATION);
                request.getHeaders().add("X-Shopify-Access-Token", authorizedClient.getAccessToken().getTokenValue());
            }
            return execution.execute(request, body);
        });

        return restTemplate;
    }

    // 注册自定义的令牌响应转换器
    @Bean
    public OAuth2AccessTokenResponseConverter<Map<String, Object>> shopifyAccessTokenResponseConverter() {
        return new ShopifyAccessTokenResponseConverter();
    }

    @Bean
    public ClientRegistrationRepository clientRegistrationRepository() {
        return new InMemoryClientRegistrationRepository(shopifyClientRegistration());
    }

    private ClientRegistration shopifyClientRegistration() {
        return ClientRegistration.withRegistrationId("shopify")
                .clientId("你的客户端ID")
                .clientSecret("你的客户端密钥")
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                .redirectUri("{baseUrl}/login/oauth2/code/shopify")
                .authorizationUri("https://{shop}.myshopify.com/admin/oauth/authorize")
                .tokenUri("https://{shop}.myshopify.com/admin/oauth/access_token")
                .scope("write_orders", "read_customers")
                .clientName("Shopify")
                .build();
    }
}

3. 关键说明

  • 自定义的ShopifyAccessTokenResponseConverter是必须的,用来补全Spring Security要求的tokenType字段,避免空指针异常。
  • 通过拦截器移除默认的Authorization头,添加Shopify要求的X-Shopify-Access-Token头,确保请求符合Shopify的API规范。
  • 无需担心Spring Security把令牌当作Bearer处理,因为拦截器已经替换了令牌的携带方式,核心逻辑中只要TokenType不为null就不会报错。

内容的提问来源于stack exchange,提问作者Miles Pomeroy

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.13 05:13:38