1. 概述

在实际开发中,不少 OAuth2 接口可能并未完全遵循标准协议,这时候我们就需要对标准的 OAuth2 请求进行一些定制化处理。

✅ Spring Security 5.1 提供了对 OAuth2 授权请求和令牌请求的自定义支持。

本文将带你了解如何自定义请求参数以及响应处理逻辑。

2. 自定义授权请求

首先我们来定制 OAuth2 的授权请求(Authorization Request),可以修改标准参数,也可以添加额外参数。

要实现这一点,我们需要实现自己的 OAuth2AuthorizationRequestResolver

public class CustomAuthorizationRequestResolver 
  implements OAuth2AuthorizationRequestResolver {
    
    private OAuth2AuthorizationRequestResolver defaultResolver;

    public CustomAuthorizationRequestResolver(
      ClientRegistrationRepository repo, String authorizationRequestBaseUri) {
        defaultResolver = new DefaultOAuth2AuthorizationRequestResolver(repo, authorizationRequestBaseUri);
    }
    
    // ...
}

⚠️ 注意:我们使用了默认的 DefaultOAuth2AuthorizationRequestResolver 来提供基础功能。

接着重写 resolve() 方法,在其中加入我们的定制逻辑:

public class CustomAuthorizationRequestResolver 
  implements OAuth2AuthorizationRequestResolver {

    //...

    @Override
    public OAuth2AuthorizationRequest resolve(HttpServletRequest request) {
        OAuth2AuthorizationRequest req = defaultResolver.resolve(request);
        if(req != null) {
            req = customizeAuthorizationRequest(req);
        }
        return req;
    }

    @Override
    public OAuth2AuthorizationRequest resolve(HttpServletRequest request, String clientRegistrationId) {
        OAuth2AuthorizationRequest req = defaultResolver.resolve(request, clientRegistrationId);
        if(req != null) {
            req = customizeAuthorizationRequest(req);
        }
        return req;
    }

    private OAuth2AuthorizationRequest customizeAuthorizationRequest(
      OAuth2AuthorizationRequest req) {
        // ...
    }

}

最终通过 customizeAuthorizationRequest() 方法来添加自定义逻辑。

完成自定义后,需要把它注册到安全配置中:

@Configuration
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.oauth2Login()
          .authorizationEndpoint()
          .authorizationRequestResolver(
            new CustomAuthorizationRequestResolver(
              clientRegistrationRepository(), "/oauth2/authorize-client"))
        //...
    }
}

✅ 这里我们用了 oauth2Login().authorizationEndpoint().authorizationRequestResolver() 来注入自定义的 OAuth2AuthorizationRequestResolver

3. 自定义授权请求的标准参数

现在来看看具体怎么自定义。我们可以自由修改 OAuth2AuthorizationRequest

举个例子,我们可以自定义 state 参数:

private OAuth2AuthorizationRequest customizeAuthorizationRequest(
  OAuth2AuthorizationRequest req) {
    return OAuth2AuthorizationRequest
      .from(req).state("xyz").build();
}

4. 添加额外的授权请求参数

✅ 除了标准参数,还可以通过 additionalParameters() 方法添加额外参数:

private OAuth2AuthorizationRequest customizeAuthorizationRequest(
  OAuth2AuthorizationRequest req) {
    Map<String,Object> extraParams = new HashMap<String,Object>();
    extraParams.putAll(req.getAdditionalParameters()); 
    extraParams.put("test", "extra");
    
    return OAuth2AuthorizationRequest
      .from(req)
      .additionalParameters(extraParams)
      .build();
}

⚠️ 别忘了把原有的 additionalParameters 也带上,避免覆盖。

4.1. 自定义 Okta 授权请求

以 Okta 为例,它支持一些额外的可选参数,如 idp(identity provider):

private OAuth2AuthorizationRequest customizeOktaReq(OAuth2AuthorizationRequest req) {
    Map<String,Object> extraParams = new HashMap<String,Object>();
    extraParams.putAll(req.getAdditionalParameters()); 
    extraParams.put("idp", "https://idprovider.com");
    return OAuth2AuthorizationRequest
      .from(req)
      .additionalParameters(extraParams)
      .build();
}

5. 自定义令牌请求

接下来是令牌请求(Token Request)的自定义。

✅ 我们可以通过自定义 OAuth2AccessTokenResponseClient 来实现。

默认实现是 DefaultAuthorizationCodeTokenResponseClient

我们可以自定义请求参数通过 RequestEntityConverter,也可以自定义响应处理通过 RestOperations

@Configuration
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.tokenEndpoint()
          .accessTokenResponseClient(accessTokenResponseClient())
            //...
    }

    @Bean
    public OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient(){
        DefaultAuthorizationCodeTokenResponseClient accessTokenResponseClient = 
          new DefaultAuthorizationCodeTokenResponseClient(); 
        accessTokenResponseClient.setRequestEntityConverter(new CustomRequestEntityConverter()); 

        OAuth2AccessTokenResponseHttpMessageConverter tokenResponseHttpMessageConverter = 
          new OAuth2AccessTokenResponseHttpMessageConverter(); 
        tokenResponseHttpMessageConverter.setAccessTokenResponseConverter(new CustomTokenResponseConverter()); 
        RestTemplate restTemplate = new RestTemplate(Arrays.asList(
          new FormHttpMessageConverter(), tokenResponseHttpMessageConverter)); 
        restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler()); 
        
        accessTokenResponseClient.setRestOperations(restTemplate); 
        return accessTokenResponseClient;
    }
}

✅ 注入方式是:tokenEndpoint().accessTokenResponseClient()

6. 添加额外的令牌请求参数

通过实现 Converter<OAuth2AuthorizationCodeGrantRequest, RequestEntity<?>> 来添加额外参数:

public class CustomRequestEntityConverter implements 
  Converter<OAuth2AuthorizationCodeGrantRequest, RequestEntity<?>> {

    private OAuth2AuthorizationCodeGrantRequestEntityConverter defaultConverter;
    
    public CustomRequestEntityConverter() {
        defaultConverter = new OAuth2AuthorizationCodeGrantRequestEntityConverter();
    }
    
    @Override
    public RequestEntity<?> convert(OAuth2AuthorizationCodeGrantRequest req) {
        RequestEntity<?> entity = defaultConverter.convert(req);
        MultiValueMap<String, String> params = (MultiValueMap<String,String>) entity.getBody();
        params.add("test2", "extra2");
        return new RequestEntity<>(params, entity.getHeaders(), 
          entity.getMethod(), entity.getUrl());
    }

}

✅ 我们使用默认转换器作为基础,然后添加自定义参数。

7. 自定义令牌响应处理

接下来是自定义令牌响应的处理。

✅ 我们可以继承默认的 OAuth2AccessTokenResponseHttpMessageConverter 实现方式。

比如,自定义 scope 的解析方式:

public class CustomTokenResponseConverter implements 
  Converter<Map<String, Object>, OAuth2AccessTokenResponse> {
    private static final Set<String> TOKEN_RESPONSE_PARAMETER_NAMES = Stream.of(
        OAuth2ParameterNames.ACCESS_TOKEN, 
        OAuth2ParameterNames.TOKEN_TYPE, 
        OAuth2ParameterNames.EXPIRES_IN, 
        OAuth2ParameterNames.REFRESH_TOKEN, 
        OAuth2ParameterNames.SCOPE).collect(Collectors.toSet());

    @Override
    public OAuth2AccessTokenResponse convert(Map<String, Object> tokenResponseParameters) {
        Object accessToken = tokenResponseParameters.get(OAuth2ParameterNames.ACCESS_TOKEN);

        Set<String> scopes = Collections.emptySet();
        if (tokenResponseParameters.containsKey(OAuth2ParameterNames.SCOPE)) {
            Object scope = tokenResponseParameters.get(OAuth2ParameterNames.SCOPE);
            scopes = Arrays.stream(StringUtils.delimitedListToStringArray(scope.toString(), ","))
                .collect(Collectors.toSet());
        }

        //...
        return OAuth2AccessTokenResponse.withToken(accessToken.toString())
          .tokenType(accessTokenType)
          .expiresIn(expiresIn)
          .scopes(scopes)
          .refreshToken(refreshToken.toString())
          .additionalParameters(additionalParameters)
          .build();
    }

}

7.1. LinkedIn 令牌响应处理

LinkedIn 的响应中只包含 access_tokenexpires_in,缺少 token_type,我们可以手动添加:

public class LinkedinTokenResponseConverter 
  implements Converter<Map<String, String>, OAuth2AccessTokenResponse> {

    @Override
    public OAuth2AccessTokenResponse convert(Map<String, String> tokenResponseParameters) {
        String accessToken = tokenResponseParameters.get(OAuth2ParameterNames.ACCESS_TOKEN);
        long expiresIn = Long.valueOf(tokenResponseParameters.get(OAuth2ParameterNames.EXPIRES_IN));
        
        OAuth2AccessToken.TokenType accessTokenType = OAuth2AccessToken.TokenType.BEARER;

        return OAuth2AccessTokenResponse.withToken(accessToken)
          .tokenType(accessTokenType)
          .expiresIn(expiresIn)
          .build();
    }
}

8. 总结

在这篇文章中,我们学习了如何通过自定义参数来定制 OAuth2 的授权请求和令牌请求。

完整代码可以在 GitHub 找到。


原始标题:Customizing Authorization and Token Requests with Spring Security 5.1 Client