En spring-security-oauth2:2.4.0.RELEASE
clases como OAuth2RestTemplate
, OAuth2ProtectedResourceDetails
y ClientCredentialsAccessTokenProvider
todos han sido marcados como obsoletos.
Desde el javadoc en estas clases apunta a un guía de migración de seguridad de primavera eso insinúa que las personas deberían migrar al proyecto principal Spring-Security 5. Sin embargo, tengo problemas para encontrar cómo implementaría mi caso de uso en este proyecto.
Toda la documentación y los ejemplos hablan sobre la integración con un proveedor de OAuth de terceros si desea autenticar las solicitudes entrantes a su aplicación y desea utilizar el proveedor de OAuth de terceros para verificar la identidad.
En mi caso de uso, todo lo que quiero hacer es hacer una solicitud con un RestTemplate
a un servicio externo que está protegido por OAuth. Actualmente creo un OAuth2ProtectedResourceDetails
con mi identificación de cliente y secreto que paso a un OAuth2RestTemplate
. yo tambien tengo una costumbre ClientCredentialsAccessTokenProvider
añadido a la OAuth2ResTemplate
eso solo agrega algunos encabezados adicionales a la solicitud de token que requiere el proveedor de OAuth que estoy usando.
En la documentación de spring-security 5 encontré una sección que menciona personalizar la solicitud de token, pero, de nuevo, eso parece estar en el contexto de autenticar una solicitud entrante con un proveedor de OAuth de terceros. No está claro cómo usaría esto en combinación con algo como un ClientHttpRequestInterceptor
para garantizar que cada solicitud saliente a un servicio externo primero obtenga un token y luego se agregue a la solicitud.
También en la guía de migración vinculada anteriormente hay una referencia a un OAuth2AuthorizedClientService
que dice que es útil para usar en interceptores, pero nuevamente parece que se basa en cosas como el ClientRegistrationRepository
que parece ser donde mantiene registros para proveedores externos si desea utilizar ese suministro para garantizar que se autentique una solicitud entrante.
¿Hay alguna forma en que pueda usar la nueva funcionalidad en Spring-Security 5 para registrar proveedores de OAuth para obtener un token para agregar a las solicitudes salientes de mi aplicación?
Anar Sultanov
Las características del cliente OAuth 2.0 de Spring Security 5.2.x no son compatibles RestTemplate
pero sólo WebClient
. Ver Referencia de seguridad de primavera:
Soporte de cliente HTTP
WebClient
integración para Entornos Servlet (para solicitar recursos protegidos)
Además, RestTemplate
quedará en desuso en una versión futura. Ver RestTemplate javadoc:
NOTA: A partir de 5.0, el reactivo sin bloqueo
org.springframework.web.reactive.client.WebClient
ofrece una alternativa moderna a laRestTemplate
con soporte eficiente para sincronización y asíncrono, así como escenarios de transmisión. ElRestTemplate
quedará obsoleto en una versión futura y no se agregarán nuevas características importantes en el futuro. Ver elWebClient
de la documentación de referencia de Spring Framework para obtener más detalles y un código de ejemplo.
Por lo tanto, la mejor solución sería abandonar RestTemplate
a favor de WebClient
.
Usando WebClient
para flujo de credenciales de cliente
Configure el registro del cliente y el proveedor mediante programación o mediante la configuración automática de 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
… y el 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;
}
Configurar el WebClient
instancia a usar ServerOAuth2AuthorizedClientExchangeFilterFunction
con lo provisto OAuth2AuthorizedClientManager
:
@Bean
WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =
new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
oauth2Client.setDefaultClientRegistrationId("custom");
return WebClient.builder()
.apply(oauth2Client.oauth2Configuration())
.build();
}
Ahora, si intenta hacer una solicitud usando este WebClient
instancia, primero solicitará un token del servidor de autorización y lo incluirá en la solicitud.
-
Eso también está en desuso ahora jajaja … al menos UnAuthenticatedServerOAuth2AuthorizedClientRepository es …
– Martillo
13 de marzo de 2020 a las 22:06
-
@AnarSultanov “Por lo tanto, la mejor solución sería abandonar RestTemplate a favor de WebClient” ¿Qué pasa con los lugares donde esta no es una opción? Por ejemplo, los clientes Spring Cloud Discovery, Configuration y Feign aún confían en RestTemplate y los estados de documentación para proporcionar un RestTemplate personalizado si planea agregar seguridad como OAuth a esos servicios.
– loesak
4 de abril de 2020 a las 3:38
-
@AnarSultanov Probé ese ejemplo exacto que diste y obtengo un error 401. Parece que no se está autenticando al intentar realizar solicitudes. ¿Algún consejo sobre eso?
– rafael.braga
24 de abril de 2020 a las 1:08
-
@rafael.braga No puedo recomendar nada sin ver todo el código y la configuración. Puedes probar el ejemplo del repositorio oficial y adaptarlo a tus necesidades: github.com/spring-projects/spring-security/tree/master/samples/…
– Anar Sultanov
24 de abril de 2020 a las 8:06
-
Aquí está el doco relevante de Spring Security. Proporciona un poco más de detalles y explicaciones de las diversas formas en que puede configurar WebClient: docs.spring.io/spring-security/site/docs/5.2.1.RELEASE/…
– Crafton
16 de julio de 2021 a las 0:33
Leandro Asís
Hola, tal vez sea demasiado tarde, sin embargo, RestTemplate todavía es compatible con Spring Security 5, para la aplicación no reactiva RestTemplate todavía se usa, lo que tiene que hacer es configurar Spring Security correctamente y crear un interceptor como se menciona en la guía de migración
Use la siguiente configuración para usar el flujo client_credentials
aplicación.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
Configuración de 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;
}
}
Interceptador
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();
}
};
}
}
Esto generará access_token en la primera llamada y cada vez que expire el token. OAuth2AuthorizedClientManager administrará todo esto para usted
-
Me gusta este enfoque porque es consistente con Spring Security
DefaultClientCredentialsTokenResponseClient
que utilizaRestTemplate
internamente. Eso significa que mi proyecto no necesita una dependencia de spring-webflux.– Nathan
21 de julio de 2022 a las 2:38
-
mejor solución. Conciso y funciona a las mil maravillas. Incluso los documentos de primavera no proporcionaron una guía práctica tan clara.
– Walnussbar
17 de agosto de 2022 a las 10:04
-
Solo una pequeña nota, basada en el código fuente, me parece que
AuthorizedClientServiceOAuth2AuthorizedClientManager
no es estrictamente seguro para subprocesos. No de una manera que bloquee la aplicación, pero se realizarán varias llamadas para obtener nuevos tokens si se manejan varias solicitudes al mismo tiempo y todas usan un token que está a punto de caducar o caducar.– alexis
16 de septiembre de 2022 a las 7:25
-
@Alexis, ¿cómo podemos evitar eso? Me refiero a no usar los vencidos?
– John Roshan
22 de diciembre de 2022 a las 16:48
-
@JohnRoshan actualizará automáticamente los tokens cuando sea necesario, solo que en algunos casos extremos podría realizar varias actualizaciones en paralelo, algunas de las cuales serían redundantes. Para evitar eso, tendría que hacer la sincronización usted mismo.
– alexis
22 de diciembre de 2022 a las 17:36
Encontré la respuesta de @matt Williams bastante útil. Aunque me gustaría agregar en caso de que alguien quiera pasar programáticamente clientId y secret para la configuración de WebClient. Así es como se puede hacer.
@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);
}
}
-
¿Hay algún código de muestra que se pueda probar para el fragmento de código anterior?
– Sagar Pilkwal
15 de junio de 2020 a las 14:41
-
@SagarPilkhwal Puede crear una aplicación de arranque de resorte de muestra simple basada en seguridad de primavera (que puede encontrar fácilmente en línea). Establezca el acceso basado en client_credentials allí y exponga una API de prueba. Luego puede crear WebClient usando el código anterior e intentar llamar a esa API.
– Corredor
15 de junio de 2020 a las 18:26
La respuesta anterior de @Anar Sultanov me ayudó a llegar a este punto, pero como tenía que agregar algunos encabezados adicionales a mi solicitud de token de OAuth, pensé que proporcionaría una respuesta completa sobre cómo resolví el problema para mi caso de uso.
Configurar detalles del proveedor
Agregue lo siguiente a 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:}
Implementar personalizado ReactiveOAuth2AccessTokenResponseClient
Como se trata de una comunicación de servidor a servidor, necesitamos usar el ServerOAuth2AuthorizedClientExchangeFilterFunction
. Esto solo acepta un ReactiveOAuth2AuthorizedClientManager
no el no reactivo OAuth2AuthorizedClientManager
. Por lo tanto, cuando usamos ReactiveOAuth2AuthorizedClientManager.setAuthorizedClientProvider()
(para darle el proveedor a utilizar para hacer la solicitud de OAuth2) tenemos que darle un ReactiveOAuth2AuthorizedClientProvider
en lugar de los no reactivos OAuth2AuthorizedClientProvider
. según el documentación de referencia de seguridad de primavera si usas un no reactivo DefaultClientCredentialsTokenResponseClient
puedes usar el .setRequestEntityConverter()
método para alterar la solicitud de token OAuth2, pero el equivalente reactivo WebClientReactiveClientCredentialsTokenResponseClient
no proporciona esta facilidad, por lo que tenemos que implementar la nuestra propia (podemos hacer uso de la existente WebClientReactiveClientCredentialsTokenResponseClient
lógica).
Mi implementación se llamó UaaWebClientReactiveClientCredentialsTokenResponseClient
(Implementación omitida ya que solo altera muy levemente el headers()
y body()
métodos del valor predeterminado WebClientReactiveClientCredentialsTokenResponseClient
para agregar algunos encabezados/campos de cuerpo adicionales, no cambia el flujo de autenticación subyacente).
Configurar WebClient
El ServerOAuth2AuthorizedClientExchangeFilterFunction.setClientCredentialsTokenResponseClient()
El método ha quedado en desuso, por lo que siguiendo el consejo de desuso de ese método:
Obsoleto. Usar
ServerOAuth2AuthorizedClientExchangeFilterFunction(ReactiveOAuth2AuthorizedClientManager)
en cambio. Crear una instancia deClientCredentialsReactiveOAuth2AuthorizedClientProvider
configurado con unWebClientReactiveClientCredentialsTokenResponseClient
(o uno personalizado) y luego suministrarlo aDefaultReactiveOAuth2AuthorizedClientManager
.
Esto termina con una configuración parecida a:
@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();
}
Usar WebClient
como normal
El oAuth2WebClient
bean ahora está listo para usarse para acceder a recursos protegidos por nuestro proveedor OAuth2 configurado de la misma manera que haría cualquier otra solicitud usando un WebClient
.
Esta es una alternativa simple a OAuth2RestTemplate
. El siguiente fragmento se ha probado con Spring Boot 3.0.0-M4
y no hay application.yml
se necesita configuración.
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>