春季安全 5 替代 OAuth2Rest 模板

在诸如 和 之类的类中,所有类都被标记为已弃用。spring-security-oauth2:2.4.0.RELEASEOAuth2RestTemplateOAuth2ProtectedResourceDetailsClientCredentialsAccessTokenProvider

从这些类的javadoc中,它指向一个春季安全迁移指南,该指南暗示人们应该迁移到核心spring-security 5项目。但是,我很难找到如何在此项目中实现我的用例。

如果您希望对应用程序的传入请求进行身份验证,并且希望使用第三方 OAuth 提供程序来验证身份,则所有文档和示例都讨论如何与第三方 OAuth 提供程序集成。

在我的用例中,我想做的就是向受OAuth保护的外部服务发出请求。目前,我用我的客户端ID和密码创建了一个,我将其传递到.我还添加了一个自定义,它只是向令牌请求添加一些额外的标头,这些标头是我正在使用的OAuth提供程序所必需的。RestTemplateOAuth2ProtectedResourceDetailsOAuth2RestTemplateClientCredentialsAccessTokenProviderOAuth2ResTemplate

在spring-security 5文档中,我发现了一个提到自定义令牌请求的部分,但同样看起来是在向第三方OAuth提供程序验证传入请求的上下文中。目前尚不清楚如何将其与类似 a 之类的内容结合使用,以确保对外部服务的每个传出请求首先获取令牌,然后获取添加到请求中的令牌。ClientHttpRequestInterceptor

同样在上面链接的迁移指南中,有一个引用,它说它说它对于在拦截器中使用很有用,但是看起来它依赖于这样的东西,如果你想使用该提供来确保传入的请求经过身份验证,它似乎是它为第三方提供商维护注册的地方。OAuth2AuthorizedClientServiceClientRegistrationRepository

有没有办法利用spring-security 5中的新功能来注册OAuth提供程序,以便获取令牌以添加到来自应用程序的传出请求中?


答案 1

Spring Security 5.2.x 的 OAuth 2.0 客户端功能不支持,而仅支持 。请参阅弹簧安全参考RestTemplateWebClient

HTTP 客户端支持

  • WebClient针对 Servlet 环境的集成(用于请求受保护的资源)

此外,将在将来的版本中弃用。请参阅 RestTemplate javadocRestTemplate

注意:从 5.0 开始,非阻塞、反应式提供了一种现代替代方案,可有效支持同步和异步以及流式处理方案。将在未来的版本中弃用,并且以后不会添加主要新功能。有关更多详细信息和示例代码,请参阅Spring框架参考文档的部分。org.springframework.web.reactive.client.WebClientRestTemplateRestTemplateWebClient

因此,最好的解决方案是放弃,转而支持 。RestTemplateWebClient


用于客户端凭据流WebClient

以编程方式或使用 Spring Boot 自动配置配置客户端注册和提供程序:

spring:
  security:
    oauth2:
      client:
        registration:
          custom:
            client-id: clientId
            client-secret: clientSecret
            authorization-grant-type: client_credentials
        provider:
          custom:
            token-uri: http://localhost:8081/oauth/token

...​和 :OAuth2AuthorizedClientManager@Bean

@Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
        ClientRegistrationRepository clientRegistrationRepository,
        OAuth2AuthorizedClientRepository authorizedClientRepository) {

    OAuth2AuthorizedClientProvider authorizedClientProvider =
            OAuth2AuthorizedClientProviderBuilder.builder()
                    .clientCredentials()
                    .build();

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

    return authorizedClientManager;
}

将实例配置为与提供的 :WebClientServerOAuth2AuthorizedClientExchangeFilterFunctionOAuth2AuthorizedClientManager

@Bean
WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
    ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =
            new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
    oauth2Client.setDefaultClientRegistrationId("custom");
    return WebClient.builder()
            .apply(oauth2Client.oauth2Configuration())
            .build();
}

现在,如果您尝试使用此实例发出请求,它将首先从授权服务器请求令牌并将其包含在请求中。WebClient


答案 2

嗨,也许为时已晚,但是Spring Security 5仍然支持RestTemplate,对于仍在使用非反应式应用程序RestTemplate,您所要做的就是正确配置spring安全性并创建迁移指南中提到的拦截器

使用以下配置来使用client_credentials流

应用程序.yml

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          jwk-set-uri: ${okta.oauth2.issuer}/v1/keys
      client:
        registration:
          okta:
            client-id: ${okta.oauth2.clientId}
            client-secret: ${okta.oauth2.clientSecret}
            scope: "custom-scope"
            authorization-grant-type: client_credentials
            provider: okta
        provider:
          okta:
            authorization-uri: ${okta.oauth2.issuer}/v1/authorize
            token-uri: ${okta.oauth2.issuer}/v1/token

配置到 OauthResTemplate

@Configuration
@RequiredArgsConstructor
public class OAuthRestTemplateConfig {

    public static final String OAUTH_WEBCLIENT = "OAUTH_WEBCLIENT";

    private final RestTemplateBuilder restTemplateBuilder;
    private final OAuth2AuthorizedClientService oAuth2AuthorizedClientService;
    private final ClientRegistrationRepository clientRegistrationRepository;

    @Bean(OAUTH_WEBCLIENT)
    RestTemplate oAuthRestTemplate() {
        var clientRegistration = clientRegistrationRepository.findByRegistrationId(Constants.OKTA_AUTH_SERVER_ID);

        return restTemplateBuilder
                .additionalInterceptors(new OAuthClientCredentialsRestTemplateInterceptorConfig(authorizedClientManager(), clientRegistration))
                .setReadTimeout(Duration.ofSeconds(5))
                .setConnectTimeout(Duration.ofSeconds(1))
                .build();
    }

    @Bean
    OAuth2AuthorizedClientManager authorizedClientManager() {
        var authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder()
                .clientCredentials()
                .build();

        var authorizedClientManager = new AuthorizedClientServiceOAuth2AuthorizedClientManager(clientRegistrationRepository, oAuth2AuthorizedClientService);
        authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);

        return authorizedClientManager;
    }

}

拦截 器

public class OAuthClientCredentialsRestTemplateInterceptor implements ClientHttpRequestInterceptor {

    private final OAuth2AuthorizedClientManager manager;
    private final Authentication principal;
    private final ClientRegistration clientRegistration;

    public OAuthClientCredentialsRestTemplateInterceptor(OAuth2AuthorizedClientManager manager, ClientRegistration clientRegistration) {
        this.manager = manager;
        this.clientRegistration = clientRegistration;
        this.principal = createPrincipal();
    }

    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
        OAuth2AuthorizeRequest oAuth2AuthorizeRequest = OAuth2AuthorizeRequest
                .withClientRegistrationId(clientRegistration.getRegistrationId())
                .principal(principal)
                .build();
        OAuth2AuthorizedClient client = manager.authorize(oAuth2AuthorizeRequest);
        if (isNull(client)) {
            throw new IllegalStateException("client credentials flow on " + clientRegistration.getRegistrationId() + " failed, client is null");
        }

        request.getHeaders().add(HttpHeaders.AUTHORIZATION, BEARER_PREFIX + client.getAccessToken().getTokenValue());
        return execution.execute(request, body);
    }

    private Authentication createPrincipal() {
        return new Authentication() {
            @Override
            public Collection<? extends GrantedAuthority> getAuthorities() {
                return Collections.emptySet();
            }

            @Override
            public Object getCredentials() {
                return null;
            }

            @Override
            public Object getDetails() {
                return null;
            }

            @Override
            public Object getPrincipal() {
                return this;
            }

            @Override
            public boolean isAuthenticated() {
                return false;
            }

            @Override
            public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
            }

            @Override
            public String getName() {
                return clientRegistration.getClientId();
            }
        };
    }
}

这将在第一次调用和令牌过期时生成access_token。OAuth2AuthorizedClientManager将为您管理所有这些


推荐