spring-security 用于OAuth2RestTemplate的Spring安全性5替代品

vcirk6k6  于 2022-11-11  发布在  Spring
关注(0)|答案(5)|浏览(268)

spring-security-oauth2:2.4.0.RELEASE中,OAuth2RestTemplateOAuth2ProtectedResourceDetailsClientCredentialsAccessTokenProvider等类别都已标示为已过时。
从这些类的javadoc中,它指向一个spring security migration guide,暗示人们应该迁移到核心的spring-security 5项目中,但是我很难找到如何在这个项目中实现我的用例。
如果您希望对应用程序的传入请求进行身份验证,并且希望使用第三方OAuth提供程序来验证身份,则所有文档和示例都讨论了与第三方OAuth提供程序的集成。
在我的用例中,我所要做的就是使用RestTemplate向受OAuth保护的外部服务发出请求。目前,我创建了一个OAuth2ProtectedResourceDetails,其中包含我的客户端ID和密码,并将其传递到OAuth2RestTemplate。我还将一个自定义ClientCredentialsAccessTokenProvider添加到OAuth2ResTemplate中,它只是向令牌请求添加了一些额外的标头,这些标头是OAuth提供程序I'所需的。我正在使用。
在spring-security 5文档中,我发现有一节提到定制令牌请求,但这看起来又是在使用第三方OAuth提供程序对传入请求进行身份验证的上下文中。目前还不清楚如何将此方法与ClientHttpRequestInterceptor之类的方法结合使用,以确保对外部服务的每个传出请求首先获得一个令牌,然后将该令牌添加到的请求。
同样在上面链接的迁移指南中,有一个OAuth2AuthorizedClientService的参考,它说这对于在拦截器中使用是有用的,但这看起来又像是它依赖于ClientRegistrationRepository之类的东西,如果你想使用该提供程序来确保传入的请求被验证,它似乎是维护第三方提供程序注册的地方。
有没有什么方法可以利用spring-security 5中的新功能来注册OAuth提供者,以便获得一个令牌来添加到来自我的应用程序的传出请求中?

zpf6vheq

zpf6vheq1#

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

HTTP客户端支持

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

此外,RestTemplate在未来的版本中将不再使用。请参阅RestTemplate javadoc

**注意:**从5.0开始,非阻塞、React式org.springframework.web.reactive.client.WebClient提供了RestTemplate的现代替代方案,它对同步和异步以及流场景都提供了有效的支持。RestTemplate将在未来的版本中被弃用,并且不会添加主要的新功能。有关更多详细信息和示例代码,请参见Spring Framework参考文档的WebClient部分。

因此,最好的解决方案是放弃RestTemplate而支持WebClient

WebClient用于客户端凭据流

以编程方式或使用Sping 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;
}

WebClient示例配置为将ServerOAuth2AuthorizedClientExchangeFilterFunction与提供的OAuth2AuthorizedClientManager一起使用:

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

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

hk8txs48

hk8txs482#

嗨,也许已经太晚了,但是Spring Security 5中仍然支持RestTemplate,对于非响应式应用程序,仍然使用RestTemplate,您需要做的只是正确配置Spring Security并创建迁移指南中提到的拦截器
使用以下配置来使用client_credentials流
application.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将为您管理所有这些

4ktjp1zp

4ktjp1zp3#

我发现@matt威廉姆斯的回答很有帮助。不过我想补充一下,以防有人想通过编程传递clientId和secret来配置WebClient。下面是如何完成的。

@Configuration
    public class WebClientConfig {

    public static final String TEST_REGISTRATION_ID = "test-client";

    @Bean
    public ReactiveClientRegistrationRepository clientRegistrationRepository() {
        var clientRegistration = ClientRegistration.withRegistrationId(TEST_REGISTRATION_ID)
                .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
                .clientId("<client_id>")
                .clientSecret("<client_secret>")
                .tokenUri("<token_uri>")
                .build();
        return new InMemoryReactiveClientRegistrationRepository(clientRegistration);
    }

    @Bean
    public WebClient testWebClient(ReactiveClientRegistrationRepository clientRegistrationRepo) {

        var oauth = new ServerOAuth2AuthorizedClientExchangeFilterFunction(clientRegistrationRepo,  new UnAuthenticatedServerOAuth2AuthorizedClientRepository());
        oauth.setDefaultClientRegistrationId(TEST_REGISTRATION_ID);

        return WebClient.builder()
                .baseUrl("https://.test.com")
                .filter(oauth)
                .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
    }
}
qgzx9mmu

qgzx9mmu4#

上面来自@Anar Sultanov的回答帮助我达到了这一点,但是由于我必须向我的OAuth令牌请求添加一些额外的头部,我想我会提供一个完整的答案,说明我如何解决我的用例的问题。

配置提供程序详细信息

将以下内容添加到application.properties

spring.security.oauth2.client.registration.uaa.client-id=${CLIENT_ID:}
spring.security.oauth2.client.registration.uaa.client-secret=${CLIENT_SECRET:}
spring.security.oauth2.client.registration.uaa.scope=${SCOPE:}
spring.security.oauth2.client.registration.uaa.authorization-grant-type=client_credentials
spring.security.oauth2.client.provider.uaa.token-uri=${UAA_URL:}

实作自订ReactiveOAuth2AccessTokenResponseClient

由于这是服务器到服务器的通信,我们需要使用ServerOAuth2AuthorizedClientExchangeFilterFunction。而不是非React性的OAuth2AuthorizedClientManager。因此当我们使用ReactiveOAuth2AuthorizedClientManager.setAuthorizedClientProvider()时(为了给予它提供用于进行OAuth2请求的提供程序),我们必须给它一个ReactiveOAuth2AuthorizedClientProvider,而不是非React式OAuth2AuthorizedClientProvider。安全参考文档如果您使用非React式DefaultClientCredentialsTokenResponseClient,则可以使用.setRequestEntityConverter()方法来更改OAuth2令牌请求,但React式等效WebClientReactiveClientCredentialsTokenResponseClient不提供此功能,因此我们必须实现自己的功能(我们可以使用现有的WebClientReactiveClientCredentialsTokenResponseClient逻辑)。
我的实现名为UaaWebClientReactiveClientCredentialsTokenResponseClient(省略了实现,因为它只对headers()body()方法从默认的WebClientReactiveClientCredentialsTokenResponseClient进行了非常细微的修改,以添加一些额外的头/主体字段,它并没有改变底层的身份验证流)。

设定WebClient

ServerOAuth2AuthorizedClientExchangeFilterFunction.setClientCredentialsTokenResponseClient()方法已过时,因此请遵循该方法的过时建议:
已弃用。* 请改用ServerOAuth2AuthorizedClientExchangeFilterFunction(ReactiveOAuth2AuthorizedClientManager)。创建一个使用WebClientReactiveClientCredentialsTokenResponseClient(或自定义)配置的ClientCredentialsReactiveOAuth2AuthorizedClientProvider的示例,然后将其提供给DefaultReactiveOAuth2AuthorizedClientManager。*
最后的配置如下所示:

@Bean("oAuth2WebClient")
public WebClient oauthFilteredWebClient(final ReactiveClientRegistrationRepository 
    clientRegistrationRepository)
{
    final ClientCredentialsReactiveOAuth2AuthorizedClientProvider
        clientCredentialsReactiveOAuth2AuthorizedClientProvider =
            new ClientCredentialsReactiveOAuth2AuthorizedClientProvider();
    clientCredentialsReactiveOAuth2AuthorizedClientProvider.setAccessTokenResponseClient(
        new UaaWebClientReactiveClientCredentialsTokenResponseClient());

    final DefaultReactiveOAuth2AuthorizedClientManager defaultReactiveOAuth2AuthorizedClientManager =
        new DefaultReactiveOAuth2AuthorizedClientManager(clientRegistrationRepository,
            new UnAuthenticatedServerOAuth2AuthorizedClientRepository());
    defaultReactiveOAuth2AuthorizedClientManager.setAuthorizedClientProvider(
        clientCredentialsReactiveOAuth2AuthorizedClientProvider);

    final ServerOAuth2AuthorizedClientExchangeFilterFunction oAuthFilter =
        new ServerOAuth2AuthorizedClientExchangeFilterFunction(defaultReactiveOAuth2AuthorizedClientManager);
    oAuthFilter.setDefaultClientRegistrationId("uaa");

    return WebClient.builder()
        .filter(oAuthFilter)
        .build();
}

正常使用WebClient

oAuth2WebClient bean现在可以用来访问由我们配置的OAuth2提供程序保护的资源,就像您使用WebClient发出任何其他请求一样。

lbsnaicq

lbsnaicq5#

这是OAuth2RestTemplate的一个简单的替代方案。下面的代码片段已经使用Sping Boot 3.0.0-M4进行了测试,并且不需要application.yml配置。
SecurityConfig.java

@Bean
    public ReactiveClientRegistrationRepository getRegistration() {
        ClientRegistration registration = ClientRegistration
                .withRegistrationId("custom")
                .tokenUri("<token_URI>")
                .clientId("<client_id>")
                .clientSecret("<secret>")
                .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
                .build();
        return new InMemoryReactiveClientRegistrationRepository(registration);
    }

    @Bean
    public WebClient webClient(ReactiveClientRegistrationRepository clientRegistrations) {
        InMemoryReactiveOAuth2AuthorizedClientService clientService = new InMemoryReactiveOAuth2AuthorizedClientService(clientRegistrations);
        AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager authorizedClientManager = new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(clientRegistrations, clientService);
        ServerOAuth2AuthorizedClientExchangeFilterFunction oauth = new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
        oauth.setDefaultClientRegistrationId("custom");
        return WebClient.builder()
                .filter(oauth)
                .filter(errorHandler()) // This is an optional
                .build();

    }

    public static ExchangeFilterFunction errorHandler() {
        return ExchangeFilterFunction.ofResponseProcessor(clientResponse -> {

            if (clientResponse.statusCode().is5xxServerError() || clientResponse.statusCode().is4xxClientError()) {
                return clientResponse.bodyToMono(String.class)
                        .flatMap(errorBody -> Mono.error(new IllegalAccessException(errorBody)));
            } else {
                return Mono.just(clientResponse);
            }
        });
    }

pom.xml

<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.0.0-M4</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-oauth2-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-oauth2-jose</artifactId>
        </dependency>
    <dependencies>

相关问题