Suporte para Spring WebClient e OAuth2
1. Visão geral
Spring Security 5 fornece suporte OAuth2 para a classeWebClient sem bloqueio do Spring Webflux.
Neste tutorial, analisaremos diferentes abordagens para acessar recursos protegidos usando esta classe.
Além disso, daremos uma olhada nos bastidores para entender como o Spring lida com o processo de autorização OAuth2.
2. Configurando o cenário
Em linha comthe OAuth2 specification, além de nosso cliente - que é nosso assunto em foco neste artigo - naturalmente precisamos de um servidor de autorização e um servidor de recursos.
Podemos usar provedores de autorização conhecidos como Google ou Github. Para entender melhor a função do Cliente OAuth2, também podemos usar nossos próprios servidores,with an implementation available in here. Não mostraremos a configuração completa, pois não é o tópico deste tutorial, é o suficiente saber que:
-
o servidor de autorização será:
-
rodando na porta8081
-
expor os pontos finais/oauth/authorize,/oauth/tokeneoauth/check_token para realizar a funcionalidade desejada
-
configurado com usuários de amostra (por exemplo, john/123) and a single OAuth client (fooClientIdPassword/secret)
-
-
o servidor de recursos será separado do servidor de autenticação e será:
-
rodando na porta8082
-
servindo um recurso seguro de objetoFoo simples acessível usando o ponto de envio/foos/{id}
-
Observação: é importante entender que vários projetos Spring estão oferecendo diferentes recursos e implementações relacionados ao OAuth. Podemos examinar o que cada biblioteca fornece emthis Spring Projects matrix.
OsWebCliente todas as funcionalidades reativas relacionadas ao Webflux fazem parte do projeto Spring Security 5. Portanto, usaremos principalmente essa estrutura ao longo deste artigo.
3. Segurança de mola 5 sob o capô
Para entender completamente os exemplos que virão, é bom saber como o Spring Security gerencia os recursos OAuth2 internamente.
Essa estrutura oferece recursos para:
-
dependem de uma conta de provedor OAuth2 paralogin users into the application
-
configurar nosso serviço como um cliente OAuth2
-
gerenciar os procedimentos de autorização para nós
-
atualizar tokens automaticamente
-
armazene as credenciais, se necessário
Alguns dos conceitos fundamentais do mundo OAuth2 do Spring Security são descritos no diagrama a seguir:
3.1. Fornecedores
Spring define a função de provedor OAuth2, responsável por expor os recursos protegidos do OAuth 2.0.
Em nosso exemplo, nosso Serviço de Autenticação será aquele que oferecerá os recursos do Provedor.
3.2. Registros de clientes
UmClientRegistration é uma entidade que contém todas as informações relevantes de um cliente específico registrado em um provedor OAuth2 (ou OpenID).
Em nosso cenário, será o cliente cadastrado no Servidor de Autenticação, identificado pela idbael-client-id.
3.3. Clientes Autorizados
Depois que o usuário final (também conhecido como Proprietário do recurso) concede permissões ao cliente para acessar seus recursos, é criada umaOAuth2AuthorizedClient entity.
Ele será responsável por associar tokens de acesso aos registros de clientes e proprietários de recursos (representados por objetosPrincipal).
3.4. Repositórios
Além disso, o Spring Security também oferece classes de repositório para acessar as entidades mencionadas acima.
Particularmente, asReactiveClientRegistrationRepository e as classesServerOAuth2AuthorizedClientRepository são usadas em pilhas reativas e usam o armazenamento na memória por padrão.
Spring Boot 2.x cria beans dessas classes de repositório e os adiciona automaticamente ao contexto.
3.5. Cadeia de filtros da Web de segurança
Um dos principais conceitos no Spring Security 5 é a sensibilidadeSecurityWebFilterChain reativa.
Como seu nome indica, ele representa uma coleção encadeada de objetosWebFilter.
Quando habilitamos os recursos do OAuth2 em nosso aplicativo, o Spring Security adiciona dois filtros à cadeia:
-
Um filtro responde a solicitações de autorização (o URI/oauth2/authorization/{registrationId}) ou lança umClientAuthorizationRequiredException. Ele contém uma referência aoReactiveClientRegistrationRepository, e é responsável por criar a solicitação de autorização para redirecionar o agente do usuário.
-
O segundo filtro difere dependendo de qual recurso estamos adicionando (recursos do cliente OAuth2 ou a funcionalidade de login OAuth2). Em ambos os casos, a principal responsabilidade deste filtro é criar a posiçãoOAuth2AuthorizedClient e armazená-la usando oServerOAuth2AuthorizedClientRepository.
3.6. Cliente da web
O cliente web será configurado com umExchangeFilterFunction contendo referências aos repositórios.
Ele os usará para obter o token de acesso para adicioná-lo automaticamente à solicitação.
4. Suporte do Spring Security 5 - O fluxo de credenciais do cliente
O Spring Security permite configurar nosso aplicativo como um cliente OAuth2.
Neste artigo, usaremos uma instânciaWebClient para recuperar recursos usando o tipo 'Credenciais de cliente' grant primeiro e, em seguida, usando o fluxo de 'Código de autorização'.
A primeira coisa que teremos que fazer é configurar o registro do cliente e o provedor que usaremos para obter o token de acesso.
4.1. Configurações de cliente e provedor
Como vimos emthe OAuth2 Login article, podemos configurá-lo programaticamente ou contar com a configuração automática do Spring Boot usando propriedades para definir nosso registro:
spring.security.oauth2.client.registration.bael.authorization-grant-type=client_credentials
spring.security.oauth2.client.registration.bael.client-id=bael-client-id
spring.security.oauth2.client.registration.bael.client-secret=bael-secret
spring.security.oauth2.client.provider.bael.token-uri=http://localhost:8085/oauth/token
Essas são todas as configurações de que precisamos para recuperar o recurso usando oclient_credentials flow.
4.2. Usando oWebClient
Usamos este tipo de concessão em comunicações máquina a máquina, onde não há usuário final interagindo com nosso aplicativo.
Por exemplo, vamos imaginar que temos um trabalhocron tentando obter um recurso seguro usandoWebClient em nosso aplicativo:
@Autowired
private WebClient webClient;
@Scheduled(fixedRate = 5000)
public void logResourceServiceResponse() {
webClient.get()
.uri("http://localhost:8084/retrieve-resource")
.retrieve()
.bodyToMono(String.class)
.map(string
-> "Retrieved using Client Credentials Grant Type: " + string)
.subscribe(logger::info);
}
4.3. Configurando oWebClient
A seguir, vamos definir a instânciawebClient que conectamos automaticamente em nossa tarefa agendada:
@Bean
WebClient webClient(ReactiveClientRegistrationRepository clientRegistrations) {
ServerOAuth2AuthorizedClientExchangeFilterFunction oauth =
new ServerOAuth2AuthorizedClientExchangeFilterFunction(
clientRegistrations,
new UnAuthenticatedServerOAuth2AuthorizedClientRepository());
oauth.setDefaultClientRegistrationId("bael");
return WebClient.builder()
.filter(oauth)
.build();
}
Como dissemos, o repositório de registro do cliente é automaticamente criado e adicionado ao contexto pelo Spring Boot.
A próxima coisa a notar aqui é que estamos usando uma posiçãoUnAuthenticatedServerOAuth2AuthorizedClientRepository . Isso se deve ao fato de que nenhum usuário final participará do processo, uma vez que é uma comunicação máquina a máquina. Finalmente, afirmamos que usaríamos o registrobael client por padrão.
Caso contrário, teríamos que especificá-lo no momento em que definirmos a solicitação no cron job:
webClient.get()
.uri("http://localhost:8084/retrieve-resource")
.attributes(
ServerOAuth2AuthorizedClientExchangeFilterFunction
.clientRegistrationId("bael"))
.retrieve()
// ...
4.4. Teste
Se executarmos nosso aplicativo com o nível de registroDEBUG habilitado, poderemos ver as chamadas que Spring Security está fazendo por nós:
o.s.w.r.f.client.ExchangeFunctions:
HTTP POST http://localhost:8085/oauth/token
o.s.http.codec.json.Jackson2JsonDecoder:
Decoded [{access_token=89cf72cd-183e-48a8-9d08-661584db4310,
token_type=bearer,
expires_in=41196,
scope=read
(truncated)...]
o.s.w.r.f.client.ExchangeFunctions:
HTTP GET http://localhost:8084/retrieve-resource
o.s.core.codec.StringDecoder:
Decoded "This is the resource!"
c.b.w.c.service.WebClientChonJob:
We retrieved the following resource using Client Credentials Grant Type: This is the resource!
Também notaremos que na segunda vez que a tarefa é executada, o aplicativo solicita o recurso sem solicitar um token primeiro, já que o último não expirou.
5. Suporte do Spring Security 5 - Implementação usando o fluxo do código de autorização
Esse tipo de concessão geralmente é usado nos casos em que aplicativos de terceiros menos confiáveis precisam acessar recursos.
5.1. Configurações de cliente e provedor
Para executar o processo OAuth2 usando o fluxo do Código de Autorização, precisaremos definir várias outras propriedades para nosso registro de cliente e o provedor:
spring.security.oauth2.client.registration.bael.client-name=bael
spring.security.oauth2.client.registration.bael.client-id=bael-client-id
spring.security.oauth2.client.registration.bael.client-secret=bael-secret
spring.security.oauth2.client.registration.bael
.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.bael
.redirect-uri=http://localhost:8080/login/oauth2/code/bael
spring.security.oauth2.client.provider.bael.token-uri=http://localhost:8085/oauth/token
spring.security.oauth2.client.provider.bael
.authorization-uri=http://localhost:8085/oauth/authorize
spring.security.oauth2.client.provider.bael.user-info-uri=http://localhost:8084/user
spring.security.oauth2.client.provider.bael.user-name-attribute=name
Além das propriedades que usamos na seção anterior, desta vez também precisamos incluir:
-
Um terminal para autenticar no servidor de autenticação
-
A URL de um terminal que contém informações do usuário
-
A URL de um terminal em nosso aplicativo para o qual o agente do usuário será redirecionado após a autenticação
Claro, para provedores bem conhecidos, os primeiros dois pontos não precisam ser especificados.
O ponto de extremidade de redirecionamento é criado automaticamente pelo Spring Security.
Por padrão, a URL configurada para ele é/[action]/oauth2/code/[registrationId], com apenas açõesauthorize andlogin permitidas (para evitar um loop infinito).
Esse terminal é responsável por:
-
recebendo o código de autenticação como um parâmetro de consulta
-
usando-o para obter um token de acesso
-
criando a instância do Cliente Autorizado
-
redirecionando o agente do usuário de volta ao terminal original
5.2. Configurações de segurança HTTP
Em seguida, precisamos configurar oSecurityWebFilterChain.
O cenário mais comum é usar os recursos de login OAuth2 do Spring Security para autenticar usuários e dar-lhes acesso aos nossos endpoints e recursos.
Se for esse o nosso caso, entãojust including the oauth2Login directive in the ServerHttpSecurity definition will be enough for our application to work as an OAuth2 Client too:
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http.authorizeExchange()
.anyExchange()
.authenticated()
.and()
.oauth2Login();
return http.build();
}
5.3. Configurando oWebClient
Agora é hora de colocar em prática nossa instânciaWebClient:
@Bean
WebClient webClient(
ReactiveClientRegistrationRepository clientRegistrations,
ServerOAuth2AuthorizedClientRepository authorizedClients) {
ServerOAuth2AuthorizedClientExchangeFilterFunction oauth =
new ServerOAuth2AuthorizedClientExchangeFilterFunction(
clientRegistrations,
authorizedClients);
oauth.setDefaultOAuth2AuthorizedClient(true);
return WebClient.builder()
.filter(oauth)
.build();
}
Desta vez, estamos injetando o repositório de registro do cliente e o repositório de cliente autorizado do contexto.
Também estamos habilitando a opçãosetDefaultOAuth2AuthorizedClient . Com ele, o framework tentará obter as informações do cliente do objetoAuthentication atual gerenciado no Spring Security.
Temos que levar em consideração que, com isso, todas as solicitações HTTP incluirão o token de acesso, que pode não ser o comportamento desejado.
Posteriormente, analisaremos alternativas para indicar o cliente que uma transação específicaWebClient usará.
5.4. Usando oWebClient
O código de autorização requer um agente de usuário que pode trabalhar em redirecionamentos (por exemplo, um navegador) para executar o procedimento.
Portanto, usamos esse tipo de concessão quando o usuário está interagindo com nosso aplicativo, geralmente chamando um ponto de extremidade HTTP:
@RestController
public class ClientRestController {
@Autowired
WebClient webClient;
@GetMapping("/auth-code")
Mono useOauthWithAuthCode() {
Mono retrievedResource = webClient.get()
.uri("http://localhost:8084/retrieve-resource")
.retrieve()
.bodyToMono(String.class);
return retrievedResource.map(string ->
"We retrieved the following resource using Oauth: " + string);
}
}
5.5. Teste
Por fim, chamaremos o endpoint e analisaremos o que está acontecendo verificando as entradas de registro.
Depois de chamarmos o endpoint, o aplicativo verifica se ainda não estamos autenticados no aplicativo:
o.s.w.s.adapter.HttpWebHandlerAdapter: HTTP GET "/auth-code"
...
HTTP/1.1 302 Found
Location: /oauth2/authorization/bael
O aplicativo redireciona para o endpoint do serviço de autorização para autenticar usando as credenciais existentes nos registros do provedor (em nosso caso, usaremos obael-user/bael-password):
HTTP/1.1 302 Found
Location: http://localhost:8085/oauth/authorize
?response_type=code
&client_id=bael-client-id
&state=...
&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Flogin%2Foauth2%2Fcode%2Fbael
Após a autenticação, o agente do usuário é enviado de volta ao URI de redirecionamento, junto com o código como um parâmetro de consulta e o valor de estado que foi enviado primeiro (para evitarCSRF attacks):
o.s.w.s.adapter.HttpWebHandlerAdapter:HTTP GET "/login/oauth2/code/bael?code=...&state=...
O aplicativo usa o código para obter um token de acesso:
o.s.w.r.f.client.ExchangeFunctions:HTTP POST http://localhost:8085/oauth/token
Ele obtém informações dos usuários:
o.s.w.r.f.client.ExchangeFunctions:HTTP GET http://localhost:8084/user
E ele redireciona o agente do usuário para o terminal original:
HTTP/1.1 302 Found
Location: /auth-code
Finalmente, nossa instânciaWebClient pode solicitar o recurso protegido com sucesso:
o.s.w.r.f.client.ExchangeFunctions:HTTP GET http://localhost:8084/retrieve-resource
o.s.w.r.f.client.ExchangeFunctions:Response 200 OK
o.s.core.codec.StringDecoder :Decoded "This is the resource!"
6. Uma alternativa - registro de cliente na chamada
Anteriormente, vimos que usando as simplificaçõessetDefaultOAuth2AuthorizedClient , o aplicativo incluirá o token de acesso em qualquer chamada que realizarmos com o cliente.
Se removermos este comando da configuração, precisaremos especificar o registro do cliente explicitamente no momento em que definirmos a solicitação.
Uma maneira, é claro, é usarclientRegistrationId como fizemos antes, ao trabalhar no fluxo de credenciais do cliente.
Como associamosPrincipal com clientes autorizados, podemos obter aOAuth2AuthorizedClient instance usando a nota@RegisteredOAuth2AuthorizedClient an:
@GetMapping("/auth-code-annotated")
Mono useOauthWithAuthCodeAndAnnotation(
@RegisteredOAuth2AuthorizedClient("bael") OAuth2AuthorizedClient authorizedClient) {
Mono retrievedResource = webClient.get()
.uri("http://localhost:8084/retrieve-resource")
.attributes(
ServerOAuth2AuthorizedClientExchangeFilterFunction.oauth2AuthorizedClient(authorizedClient))
.retrieve()
.bodyToMono(String.class);
return retrievedResource.map(string ->
"Resource: " + string
+ " - Principal associated: " + authorizedClient.getPrincipalName()
+ " - Token will expire at: " + authorizedClient.getAccessToken()
.getExpiresAt());
}
7. Evitando os recursos de login do OAuth2
Como dissemos, o cenário mais comum é contar com o provedor de autorização OAuth2 para fazer login de usuários em nosso aplicativo.
Mas e se quisermos evitar isso, mas ainda conseguir acessar recursos protegidos usando o protocolo OAuth2? Então, precisaremos fazer algumas mudanças em nossa configuração.
Para começar, e apenas para ficar claro, podemos usar a açãoauthorize em vez delogin one ao definir a propriedade URI de redirecionamento:
spring.security.oauth2.client.registration.bael
.redirect-uri=http://localhost:8080/login/oauth2/code/bael
Também podemos descartar as propriedades relacionadas ao usuário, já que não as usaremos para criarPrincipal em nosso aplicativo.
Agora, vamos configurar o switchSecurityWebFilterChain incluindo o comandooauth2Login, e em vez disso, vamos incluir o comandooauth2Client.
Mesmo que não queiramos depender do login OAuth2, ainda queremos autenticar os usuários antes de acessar nosso endpoint. Por esse motivo, também incluiremos a diretivaformLogin aqui:
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http.authorizeExchange()
.anyExchange()
.authenticated()
.and()
.oauth2Client()
.and()
.formLogin();
return http.build();
}
Vamos agora executar o aplicativo e verificar o que acontece quando usamos o ponto de envio/auth-code-annotated .
Primeiro, teremos que fazer login em nosso aplicativo usando o formulário de login.
Posteriormente, o aplicativo nos redirecionará para o logon do Serviço de Autorização, para conceder acesso aos nossos recursos.
Observação: depois de fazer isso, devemos ser redirecionados de volta ao endpoint original que chamamos. No entanto, o Spring Security parece estar redirecionando de volta para o caminho raiz “/”, o que parece ser um bug. As solicitações a seguir, após a que acionou a dança do OAuth2, serão executadas com êxito.
Podemos ver na resposta do endpoint que o cliente autorizado desta vez está associado a um principal denominadobael-client-id instead debael-user, named após o usuário configurado no serviço de autenticação.
8. Suporte ao Spring Framework - Abordagem Manual
Fora da caixa,Spring 5 provides just one OAuth2-related service method to add a Bearer token header to the request easily. It’s the HttpHeaders#setBearerAuth method.
Agora veremos um exemplo para entender o que seria necessário para obter nosso recurso seguro executando uma dança OAuth2 manualmente.
Simplificando, precisaremos encadear duas solicitações HTTP: uma para obter um token de autenticação do servidor de autorização e a outra para obter o recurso usando este token:
@Autowired
WebClient client;
public Mono obtainSecuredResource() {
String encodedClientData =
Base64Utils.encodeToString("bael-client-id:bael-secret".getBytes());
Mono resource = client.post()
.uri("localhost:8085/oauth/token")
.header("Authorization", "Basic " + encodedClientData)
.body(BodyInserters.fromFormData("grant_type", "client_credentials"))
.retrieve()
.bodyToMono(JsonNode.class)
.flatMap(tokenResponse -> {
String accessTokenValue = tokenResponse.get("access_token")
.textValue();
return client.get()
.uri("localhost:8084/retrieve-resource")
.headers(h -> h.setBearerAuth(accessTokenValue))
.retrieve()
.bodyToMono(String.class);
});
return resource.map(res ->
"Retrieved the resource using a manual approach: " + res);
}
Este exemplo é principalmente para entender como pode ser complicado alavancar uma solicitação seguindo a especificação OAuth2 e para ver como o métodosetBearerAuth é usado.
Em um cenário da vida real, deixaríamos Spring Security cuidar de todo o trabalho duro para nós de uma maneira transparente, como fizemos nas seções anteriores.
9. Conclusão
Neste tutorial, vimos como podemos configurar nosso aplicativo como um cliente OAuth2 e, mais particularmente, como podemos configurar e usarWebClient para recuperar um recurso protegido em uma pilha totalmente reativa.
Por último, mas não menos importante, analisamos como os mecanismos Spring Security 5 OAuth2 operam nos bastidores para cumprir a especificação OAuth2.
Como sempre, o exemplo completo está disponível emGithub.