Spring WebClient en OAuth2-ondersteuning

1. Overzicht

Spring Security 5 biedt OAuth2-ondersteuning voor het niet-blokkeren van Spring Webflux Web cliënt klasse.

In deze zelfstudie analyseren we verschillende benaderingen om toegang te krijgen tot beveiligde bronnen met behulp van deze klasse.

We zullen ook een kijkje nemen onder de motorkap om te begrijpen hoe Spring omgaat met het OAuth2-autorisatieproces.

2. Het scenario opzetten

In lijn met de OAuth2-specificatie hebben we, behalve onze Client - waar we in dit artikel over gaan - natuurlijk een Authorization Server en een Resource Server nodig.

We kunnen gebruik maken van bekende autorisatieproviders zoals Google of Github. Om de rol van de OAuth2-client beter te begrijpen, kunnen we ook onze eigen servers gebruiken, met een implementatie die hier beschikbaar is. We zullen niet de volledige configuratie laten zien, aangezien dit niet het onderwerp van deze tutorial is, het is voldoende om te weten dat:

  • de autorisatieserver zal zijn:
    • draait op poort 8081
    • bloot de / oauth / autoriseren,/ oauth / token en oauth / check_token endpoints om de gewenste functionaliteit uit te voeren
    • geconfigureerd met voorbeeldgebruikers (bijv. John/123) en een enkele OAuth-client (fooClientIdPassword/geheim)
  • de Resource Server wordt gescheiden van de Authentication Server en zal:
    • draait op poort 8082
    • een simpele serveren Foo object beveiligde bron toegankelijk via de / foos / {id} eindpunt

Opmerking: het is belangrijk om te begrijpen dat verschillende Spring-projecten verschillende OAuth-gerelateerde functies en implementaties bieden. In deze Spring Projects-matrix kunnen we onderzoeken wat elke bibliotheek te bieden heeft.

De Web cliënt en alle reactieve Webflux-gerelateerde functionaliteit maakt deel uit van het Spring Security 5-project. Daarom zullen we dit framework voornamelijk in dit artikel gebruiken.

3. Veerbeveiliging 5 Onder de motorkap

Om de komende voorbeelden volledig te begrijpen, is het goed om te weten hoe Spring Security de OAuth2-functies intern beheert.

Dit framework biedt mogelijkheden om:

  • vertrouw op een OAuth2-provideraccount om gebruikers in te loggen bij de applicatie
  • configureer onze service als een OAuth2-client
  • de autorisatieprocedures voor ons beheren
  • vernieuw tokens automatisch
  • sla de inloggegevens indien nodig op

Enkele van de fundamentele concepten van de OAuth2-wereld van Spring Security worden beschreven in het volgende diagram:

3.1. Aanbieders

Spring definieert de rol van OAuth2-provider, die verantwoordelijk is voor het vrijgeven van door OAuth 2.0 beschermde bronnen.

In ons voorbeeld is onze authenticatieservice degene die de provider mogelijkheden biedt.

3.2. Klantregistraties

EEN Klantregistratie is een entiteit die alle relevante informatie bevat van een specifieke klant die is geregistreerd in een OAuth2-provider (of een OpenID-provider).

In ons scenario is dit de client die is geregistreerd op de Authentication Server, geïdentificeerd door de bael-client-id ID kaart.

3.3. Geautoriseerde klanten

Zodra de eindgebruiker (ook bekend als de eigenaar van de bron) toestemming verleent aan de klant om toegang te krijgen tot zijn bronnen, wordt een OAuth2AuthorizedClient entiteit is gemaakt.

Het is verantwoordelijk voor het koppelen van toegangstokens aan klantregistraties en resource-eigenaren (vertegenwoordigd door Opdrachtgever voorwerpen).

3.4. Opslagplaatsen

Bovendien biedt Spring Security ook repository-klassen om toegang te krijgen tot de hierboven genoemde entiteiten.

Met name de ReactiveClientRegistrationRepository en de ServerOAuth2AuthorizedClientRepository klassen worden gebruikt in reactieve stapels en ze gebruiken standaard de opslag in het geheugen.

Spring Boot 2.x maakt bonen van deze opslagplaatsklassen en voegt ze automatisch toe aan de context.

3.5. Beveiliging webfilterketen

Een van de belangrijkste concepten in Spring Security 5 is het reactieve BeveiligingWebFilterChain entiteit.

Zoals de naam al aangeeft, vertegenwoordigt het een aaneengeschakelde verzameling WebFilter voorwerpen.

Wanneer we de OAuth2-functies in onze applicatie inschakelen, voegt Spring Security twee filters toe aan de keten:

  1. Eén filter reageert op autorisatieverzoeken (het / oauth2 / autorisatie / {registrationId} URI) of gooit een ClientAuthorizationRequiredException. Het bevat een verwijzing naar het ReactiveClientRegistrationRepository, en het is verantwoordelijk voor het maken van het autorisatieverzoek om de user-agent om te leiden.
  2. Het tweede filter verschilt afhankelijk van de functie die we toevoegen (OAuth2 Client-mogelijkheden of de OAuth2 Login-functie). In beide gevallen is de hoofdverantwoordelijkheid van dit filter het maken van het OAuth2AuthorizedClient instantie en sla het op met behulp van de ServerOAuth2AuthorizedClientRepository.

3.6. Web cliënt

De webclient wordt geconfigureerd met een ExchangeFilterFunction met verwijzingen naar de repositories.

Het zal ze gebruiken om het toegangstoken te verkrijgen om het automatisch aan het verzoek toe te voegen.

4. Spring Security 5 Support - de stroom van klantreferenties

Spring Security maakt het mogelijk om onze applicatie te configureren als een OAuth2 Client.

In dit artikel gebruiken we een Web cliënt instantie om bronnen op te halen met behulp van de ‘Klantreferenties 'verlenen eerst, en vervolgens de stroom ‘Autorisatiecode 'gebruiken.

Het eerste dat we moeten doen, is de clientregistratie configureren en de provider die we zullen gebruiken om het toegangstoken te verkrijgen.

4.1. Configuraties van klant en provider

Zoals we hebben gezien in het OAuth2 Login-artikel, kunnen we het programmatisch configureren of vertrouwen op de automatische configuratie van Spring Boot door eigenschappen te gebruiken om onze registratie te definiëren:

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 = // localhost: 8085 / oauth / token

Dit zijn alle configuraties die we nodig hebben om de bron op te halen met behulp van de client_credentials stromen.

4.2. De ... gebruiken Web cliënt

We gebruiken dit type toekenning in machine-to-machine-communicatie waarbij geen eindgebruiker interactie heeft met onze applicatie.

Laten we ons bijvoorbeeld voorstellen dat we een cron taak om een ​​beveiligde bron te verkrijgen met behulp van een Web cliënt in onze applicatie:

@Autowired private WebClient webClient; @Scheduled (fixedRate = 5000) public void logResourceServiceResponse () {webClient.get () .uri ("// localhost: 8084 / retrieve-resource") .retrieve () .bodyToMono (String.class) .map (string -> "Opgehaald met gebruikmaking van Klantreferenties Grant Type:" + string) .subscribe (logger :: info); }

4.3. Configureren van het Web cliënt

Laten we vervolgens het web cliënt instantie die we automatisch hebben bedraad in onze geplande taak:

@Bean WebClient webClient (ReactiveClientRegistrationRepository clientRegistrations) {ServerOAuth2AuthorizedClientExchangeFilterFunction oauth = nieuwe ServerOAuth2AuthorizedClientExchangeFilterFunction (clientRegistrations, nieuwe UnAuthenticatedServerOAuth2AuthorizedClient); oauth.setDefaultClientRegistrationId ("bael"); retourneer WebClient.builder () .filter (oauth) .build (); }

Zoals we al zeiden, wordt de opslagplaats voor clientregistratie automatisch gemaakt en door Spring Boot aan de context toegevoegd.

Het volgende dat hier opvalt, is dat we een UnAuthenticatedServerOAuth2AuthorizedClientRepository voorbeeld. Dit komt doordat er geen eindgebruiker aan het proces zal deelnemen, aangezien het een machine-to-machine-communicatie is. Ten slotte verklaarden we dat we de bael klantregistratie standaard.

Anders zouden we het moeten specificeren tegen de tijd dat we het verzoek in de cron-taak definiëren:

webClient.get () .uri ("// localhost: 8084 / retrieve-resource") .attributes (ServerOAuth2AuthorizedClientExchangeFilterFunction .clientRegistrationId ("bael")) .retrieve () // ...

4.4. Testen

Als we onze applicatie uitvoeren met de DEBUG logboekregistratieniveau ingeschakeld, kunnen we de oproepen zien die Spring Security voor ons doet:

oswrfclient.Exchange Functies: HTTP POST // localhost: 8085 / oauth / token oshttp.codec.json.Jackson2JsonDecoder: gedecodeerd [{access_token = 89cf72cd-183e-48a8-9d08-661584db4310, token_type = scope-drager = 41196ires_type = reikwijdte-drager = 41196ires_drager = read (afgekapt) ...] oswrfclient.ExchangeFunctions: HTTP GET // localhost: 8084 / retrieve-resource oscore.codec.StringDecoder: gedecodeerd "Dit is de bron!" c.b.w.c.service.WebClientChonJob: We hebben de volgende bron opgehaald met behulp van Client Credentials Grant Type: Dit is de bron!

We zullen ook opmerken dat de toepassing de tweede keer dat de taak wordt uitgevoerd, de bron opvraagt ​​zonder eerst om een ​​token te vragen, aangezien de laatste niet is verlopen.

5. Spring Security 5 Support - Implementatie met behulp van de autorisatiecodestroom

Dit type toekenning wordt meestal gebruikt in gevallen waarin minder vertrouwde toepassingen van derden toegang moeten hebben tot bronnen.

5.1. Configuraties van klant en provider

Om het OAuth2-proces uit te voeren met behulp van de autorisatiecodestroom, moeten we nog een aantal eigenschappen definiëren voor onze klantregistratie en de provider:

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 = autorisatiecode spring.security.oauth2.client.registration.bael .redirect-uri = // localhost: 8080 / login / oauth2 / code / bael spring.security.oauth2.client.provider.bael.token-uri = // localhost: 8085 / oauth / token spring.security.oauth2.client.provider.bael .authorization-uri = // localhost: 8085 / oauth / authorize spring.security.oauth2.client.provider.bael.user-info-uri = // localhost: 8084 / gebruiker spring.security.oauth2.client.provider.bael.user-name-attribute = naam

Afgezien van de eigenschappen die we in de vorige sectie hebben gebruikt, moeten we deze keer ook opnemen:

  • Een eindpunt om te verifiëren op de verificatieserver
  • De URL van een eindpunt met gebruikersinformatie
  • De URL van een eindpunt in onze applicatie waarnaar de user-agent wordt doorgestuurd na authenticatie

Voor bekende providers hoeven de eerste twee punten natuurlijk niet te worden gespecificeerd.

Het omleidings-eindpunt wordt automatisch gemaakt door Spring Security.

Standaard is de URL die ervoor is geconfigureerd / [action] / oauth2 / code / [registrationId], met alleen toestemming geven en Log in acties toegestaan ​​(om een ​​oneindige lus te vermijden).

Dit eindpunt is verantwoordelijk voor:

  • het ontvangen van de authenticatiecode als een queryparameter
  • het gebruiken om een ​​toegangstoken te verkrijgen
  • het creëren van de Authorized Client-instantie
  • het omleiden van de user-agent terug naar het oorspronkelijke eindpunt

5.2. HTTP-beveiligingsconfiguraties

Vervolgens moeten we het BeveiligingWebFilterChain.

Het meest voorkomende scenario is het gebruik van de OAuth2-inlogmogelijkheden van Spring Security om gebruikers te authenticeren en hen toegang te geven tot onze eindpunten en bronnen.

Als dat ons geval is, dan alleen inclusief de Inloggen richtlijn in het ServerHttpSecurity definitie zal voldoende zijn om onze applicatie ook als een OAuth2-client te laten werken:

@Bean public SecurityWebFilterChain springSecurityFilterChain (ServerHttpSecurity http) {http.authorizeExchange () .anyExchange () .authenticated () .and () .oauth2Login (); retourneer http.build (); }

5.3. Configureren van het Web cliënt

Nu is het tijd om onze Web cliënt voorbeeld:

@Bean WebClient webClient (ReactiveClientRegistrationRepository clientRegistrations, ServerOAuth2AuthorizedClientRepository AuthorizedClients) {ServerOAuth2AuthorizedClientExchangeFilterFunction oauth = nieuwe ServerOAuth2AuthorizedClientExchangeFilterFunction (clientRegistrations); oauth.setDefaultOAuth2AuthorizedClient (true); retourneer WebClient.builder () .filter (oauth) .build (); }

Deze keer injecteren we zowel de clientregistratierepository als de geautoriseerde clientrepository vanuit de context.

We schakelen ook het setDefaultOAuth2AuthorizedClient keuze. Hiermee probeert het raamwerk de klantinformatie uit de stroom te halen Authenticatie object beheerd in Spring Security.

We moeten er rekening mee houden dat alle HTTP-verzoeken het toegangstoken bevatten, wat misschien niet het gewenste gedrag is.

Later zullen we alternatieven analyseren om de klant aan te geven dat een specifiek Web cliënt transactie zal gebruiken.

5.4. De ... gebruiken Web cliënt

De autorisatiecode vereist een user-agent die omleidingen kan uitwerken (bijvoorbeeld een browser) om de procedure uit te voeren.

Daarom maken we gebruik van dit type toekenning wanneer de gebruiker interactie heeft met onze applicatie, waarbij hij gewoonlijk een HTTP-eindpunt aanroept:

@RestController openbare klasse ClientRestController {@Autowired WebClient webClient; @GetMapping ("/ auth-code") Mono useOauthWithAuthCode () {Mono retrievedResource = webClient.get () .uri ("// localhost: 8084 / retrieve-resource") .retrieve () .bodyToMono (String.class); return retrievedResource.map (string -> "We hebben de volgende bron opgehaald met behulp van Oauth:" + string); }}

5.5. Testen

Ten slotte noemen we het eindpunt en analyseren we wat er aan de hand is door de logboekvermeldingen te controleren.

Nadat we het eindpunt hebben aangeroepen, verifieert de applicatie dat we nog niet geauthenticeerd zijn in de applicatie:

o.s.w.s.adapter.HttpWebHandlerAdapter: HTTP GET "/ auth-code" ... HTTP / 1.1 302 Locatie gevonden: / oauth2 / autorisatie / bael

De applicatie wordt omgeleid naar het eindpunt van de autorisatieservice om te verifiëren met behulp van inloggegevens die in de registers van de provider staan ​​(in ons geval gebruiken we de bael-user / bael-wachtwoord):

HTTP / 1.1 302 Locatie gevonden: // localhost: 8085 / oauth / authorize? Response_type = code & client_id = bael-client-id & state = ... & redirect_uri = http% 3A% 2F% 2Flocalhost% 3A8080% 2Flogin% 2Foauth2% 2Fcode% 2Fbael

Na authenticatie wordt de user-agent teruggestuurd naar de omleidings-URI, samen met de code als een queryparameter en de statuswaarde die als eerste werd verzonden (om CSRF-aanvallen te voorkomen):

o.s.w.s.adapter.HttpWebHandlerAdapter: HTTP GET "/ login / oauth2 / code / bael? code = ... & state = ...

De applicatie gebruikt vervolgens de code om een ​​toegangstoken te verkrijgen:

o.s.w.r.f.client.Exchange Functies: HTTP POST // localhost: 8085 / oauth / token

Het verkrijgt gebruikersinformatie:

o.s.w.r.f.client.Exchange Functies: HTTP GET // localhost: 8084 / gebruiker

En het leidt de user-agent om naar het oorspronkelijke eindpunt:

HTTP / 1.1 302 gevonden locatie: / auth-code

Eindelijk, onze Web cliënt instantie kan de beveiligde bron met succes aanvragen:

o.s.w.r.f.client.ExchangeFunctions: HTTP GET // localhost: 8084 / retrieve-resource o.s.w.r.f.client.ExchangeFunctions: Response 200 OK o.s.core.codec.StringDecoder: gedecodeerd "Dit is de bron!"

6. Een alternatief - Klantregistratie in het gesprek

Eerder zagen we dat het gebruik van de setDefaultOAuth2AuthorizedClientimpliceert dat de applicatie het toegangstoken zal opnemen in elke oproep die we met de klant realiseren.

Als we deze opdracht uit de configuratie verwijderen, moeten we de clientregistratie expliciet specificeren tegen de tijd dat we het verzoek definiëren.

Eén manier is natuurlijk door de clientRegistrationId zoals we eerder deden bij het werken in de stroom van clientreferenties.

Omdat we de Opdrachtgever met geautoriseerde klanten kunnen we de OAuth2AuthorizedClient instantie met behulp van de @ RegisteredOAuth2AuthorizedClient annotatie:

@GetMapping ("/ auth-code-annotated") Mono useOauthWithAuthCodeAndAnnotation (@ RegisteredOAuth2AuthorizedClient ("bael") OAuth2AuthorizedClient authorisedClient) {Mono retrievedResource = webClient.get (). Resource ("// localhost: 8084 /. attributen (ServerOAuth2AuthorizedClientExchangeFilterFunction.oauth2AuthorizedClient (AuthorizedClient)) .retrieve () .bodyToMono (String.class); return retrievedResource.map (string -> "Resource:" + string + "- Principal geassocieerd:" + AuthorizedClient.getPrincipalName () + "- Token verloopt op:" + AuthorizedClient.getAccessToken () .getExpiresAt ()); }

7. Het vermijden van de OAuth2-inlogfuncties

Zoals we al zeiden, vertrouwt het meest voorkomende scenario op de OAuth2-autorisatieprovider om gebruikers in te loggen in onze applicatie.

Maar wat als we dit willen vermijden, maar toch toegang willen hebben tot beveiligde bronnen met behulp van het OAuth2-protocol? Dan moeten we enkele wijzigingen aanbrengen in onze configuratie.

Om te beginnen, en voor de duidelijkheid, kunnen we de toestemming geven actie in plaats van de Log in een bij het definiëren van de omleidings-URI-eigenschap:

spring.security.oauth2.client.registration.bael .redirect-uri = // localhost: 8080 / login / oauth2 / code / bael

We kunnen ook de gebruikersgerelateerde eigenschappen verwijderen, omdat we ze niet zullen gebruiken om het Opdrachtgever in onze applicatie.

Nu gaan we het BeveiligingWebFilterChain zonder de Inloggen commando, en in plaats daarvan zullen we de oauth2Client een.

Hoewel we niet willen vertrouwen op de OAuth2-login, willen we toch gebruikers verifiëren voordat we toegang krijgen tot ons eindpunt. Om deze reden nemen we ook de formLogin richtlijn hier:

@Bean public SecurityWebFilterChain springSecurityFilterChain (ServerHttpSecurity http) {http.authorizeExchange () .anyExchange () .authenticated () .and () .oauth2Client () .en () .formLogin (); retourneer http.build (); }

Laten we nu de applicatie uitvoeren en kijken wat er gebeurt als we het / auth-code-geannoteerd eindpunt.

We zullen eerst moeten inloggen op onze applicatie met het formulier login.

Daarna zal de applicatie ons doorverwijzen naar de autorisatieservice login, om toegang te verlenen tot onze bronnen.

Opmerking: nadat we dit hebben gedaan, moeten we worden teruggeleid naar het oorspronkelijke eindpunt dat we hebben aangeroepen. Desalniettemin lijkt Spring Security terug te leiden naar het rootpad "/", wat een bug lijkt te zijn. De volgende verzoeken na degene die de OAuth2-dans heeft geactiveerd, zullen met succes worden uitgevoerd.

We kunnen in het antwoord van het eindpunt zien dat de geautoriseerde client deze keer is gekoppeld aan een principal met de naam bael-client-id in plaats van de bael-gebruiker, genoemd naar de gebruiker die is geconfigureerd in de Authentication Service.

8. Spring Framework Support - handmatige aanpak

Uit de doos, Spring 5 biedt slechts één OAuth2-gerelateerde servicemethode om eenvoudig een Bearer-token-header aan het verzoek toe te voegen. Het is de HttpHeaders # setBearerAuth methode.

We zullen nu een voorbeeld zien om te begrijpen wat er nodig is om onze beveiligde bron te verkrijgen door handmatig een OAuth2-dans uit te voeren.

Simpel gezegd, we moeten twee HTTP-verzoeken koppelen: een om een ​​authenticatietoken van de autorisatieserver te krijgen en de andere om de bron te verkrijgen met behulp van dit token:

@Autowired WebClient-client; openbare Mono gainSecuredResource () {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 -> "Haalde de bron op met behulp van een handmatige benadering:" + res); }

Dit voorbeeld is voornamelijk bedoeld om te begrijpen hoe omslachtig het kan zijn om gebruik te maken van een verzoek volgens de OAuth2-specificatie en om te zien hoe de setBearerAuth methode wordt gebruikt.

In een realistisch scenario zouden we Spring Security op een transparante manier al het harde werk voor ons laten doen, zoals we in eerdere secties hebben gedaan.

9. Conclusie

In deze zelfstudie hebben we gezien hoe we onze applicatie kunnen instellen als een OAuth2-client, en meer in het bijzonder hoe we de Web cliënt om een ​​beveiligde bron op te halen in een volledig reactieve stapel.

Last but not least hebben we geanalyseerd hoe de OAuth2-mechanismen van Spring Security 5 onder de motorkap werken om te voldoen aan de OAuth2-specificatie.

Zoals altijd is het volledige voorbeeld beschikbaar op Github.