1. Gambaran keseluruhan
Spring Security 5 memberikan sokongan OAuth2 untuk kelas WebClient yang tidak menyekat Spring Webflux .
Dalam tutorial ini, kami akan menganalisis pendekatan yang berbeza untuk mengakses sumber yang selamat menggunakan kelas ini.
Juga, kita akan melihat di bawah pengetahuan untuk memahami bagaimana Spring mengendalikan proses kebenaran OAuth2.
2. Menyiapkan Senario
Sejajar dengan spesifikasi OAuth2, selain dari Pelanggan kami - yang menjadi fokus kami dalam artikel ini - kami semestinya memerlukan Pelayan Pengesahan dan Pelayan Sumber.
Kami boleh menggunakan penyedia kebenaran terkenal seperti Google atau Github. Untuk lebih memahami peranan Pelanggan OAuth2, kami juga dapat menggunakan pelayan kami sendiri, dengan implementasi yang tersedia di sini. Kami tidak akan menunjukkan konfigurasi penuh kerana ini bukan topik tutorial ini, cukup mengetahui bahawa:
- Pelayan Pengesahan akan:
- berjalan di port 8081
- mendedahkan titik akhir / oauth / otorh, / oauth / token dan oauth / check_token untuk melaksanakan fungsi yang diinginkan
- dikonfigurasi dengan pengguna sampel (contohnya john / 123 ) dan pelanggan OAuth tunggal ( fooClientIdPassword / rahsia )
- Pelayan Sumber akan dipisahkan dari Pelayan Pengesahan dan akan:
- berjalan di port 8082
- melayani sumber selamat objek Foo yang mudah diakses menggunakan titik akhir / foos / {id}
Catatan: penting untuk difahami bahawa beberapa projek Spring menawarkan pelbagai ciri dan pelaksanaan yang berkaitan dengan OAuth. Kami dapat memeriksa apa yang disediakan oleh setiap perpustakaan dalam matriks Spring Projects ini.
Pelanggan Web dan semua fungsi berkaitan Webflux reaktif adalah sebahagian daripada projek Spring Security 5. Oleh itu, kami akan menggunakan rangka kerja ini sepanjang artikel ini.
3. Keselamatan Musim Semi 5 Di Bawah Tudung
Untuk memahami sepenuhnya contoh-contoh yang akan datang, ada baiknya mengetahui bagaimana Spring Security menguruskan ciri OAuth2 secara dalaman.
Rangka kerja ini menawarkan keupayaan untuk:
- bergantung pada akaun penyedia OAuth2 untuk log masuk pengguna ke dalam aplikasi
- konfigurasikan perkhidmatan kami sebagai Pelanggan OAuth2
- menguruskan prosedur kebenaran untuk kami
- muat semula token secara automatik
- simpan bukti kelayakan sekiranya perlu
Beberapa konsep asas dunia OAuth2 Spring Security dijelaskan dalam rajah berikut:

3.1. Penyedia
Spring menentukan peranan OAuth2 Provider, yang bertanggungjawab untuk mendedahkan sumber perlindungan OAuth 2.0.
Dalam contoh kami, Perkhidmatan Pengesahan kami akan menjadi yang menawarkan kemampuan Penyedia.
3.2. Pendaftaran Pelanggan
A ClientRegistration adalah entiti yang mengandungi semua maklumat yang berkaitan klien tertentu berdaftar dalam OAuth2 (atau OpenID) pembekal.
Dalam senario kami, pelanggan akan didaftarkan di Authentication Server, yang dikenal pasti oleh id bael-client- id.
3.3. Pelanggan yang diberi kuasa
Setelah pengguna akhir (aka Pemilik Sumber) memberikan kebenaran kepada pelanggan untuk mengakses sumbernya, entiti OAuth2AuthorizedClient akan dibuat.
Ini akan bertanggungjawab untuk mengaitkan token akses ke pendaftaran pelanggan dan pemilik sumber (diwakili oleh objek Utama ).
3.4. Repositori
Selanjutnya, Spring Security juga menawarkan kelas repositori untuk mengakses entiti yang disebutkan di atas.
Terutama, kelas ReactiveClientRegistrationRepository dan ServerOAuth2AuthorizedClientRepository digunakan dalam tumpukan reaktif, dan mereka menggunakan storan dalam memori secara lalai.
Spring Boot 2.x membuat biji kelas repositori ini dan menambahkannya secara automatik ke konteks.
3.5. Rangkaian Penapis Web Keselamatan
Salah satu konsep utama dalam Spring Security 5 adalah entiti SecurityWebFilterChain reaktif .
Seperti namanya, ini mewakili koleksi objek WebFilter yang dirantai .
Apabila kami mengaktifkan ciri OAuth2 dalam aplikasi kami, Spring Security menambahkan dua penapis ke rantai:
- Satu penapis bertindak balas terhadap permintaan kebenaran ( URI / oauth2 / authorization / {registrationId} ) atau membuang ClientAuthorizationRequiredException . Ini mengandungi rujukan ke ReactiveClientRegistrationRepository, dan bertanggung jawab membuat permintaan kebenaran untuk mengarahkan ejen pengguna.
- Penapis kedua berbeza bergantung pada ciri mana yang kita tambahkan (keupayaan Pelanggan OAuth2 atau fungsi Masuk OAuth2). Dalam kedua kes tersebut, tanggungjawab utama penapis ini adalah membuat instance OAuth2AuthorizedClient dan menyimpannya menggunakan ServerOAuth2AuthorizedClientRepository.
3.6. Pelanggan Web
Pelanggan web akan dikonfigurasi dengan ExchangeFilterFunction yang mengandungi rujukan ke repositori.
Ia akan menggunakannya untuk mendapatkan token akses untuk menambahkannya secara automatik ke permintaan.
4. Spring Security 5 Support - Aliran Kredensial Pelanggan
Spring Security membolehkan mengkonfigurasi aplikasi kami sebagai Pelanggan OAuth2.
Dalam penulisan ini, kami akan menggunakan instance WebClient untuk mendapatkan sumber menggunakan 'Client Credentials'beri jenis terlebih dahulu, dan kemudian gunakan aliran 'Kod Kebenaran'
Perkara pertama yang harus kita lakukan ialah mengkonfigurasi pendaftaran pelanggan dan penyedia yang akan kita gunakan untuk mendapatkan token akses.
4.1. Konfigurasi Pelanggan dan Penyedia
As we've seen in the OAuth2 Login article, we can either configure it programmatically or rely on the Spring Boot auto-configuration by using properties to define our registration:
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
These are all the configurations that we need to retrieve the resource using the client_credentials flow.
4.2. Using the WebClient
We use this grant type in machine-to-machine communications where there's no end-user interacting with our application.
For example, let's imagine we have a cron job trying to obtain a secured resource using a WebClient in our application:
@Autowired private WebClient webClient; @Scheduled(fixedRate = 5000) public void logResourceServiceResponse() { webClient.get() .uri("//localhost:8084/retrieve-resource") .retrieve() .bodyToMono(String.class) .map(string -> "Retrieved using Client Credentials Grant Type: " + string) .subscribe(logger::info); }
4.3. Configuring the WebClient
Next, let's set the webClient instance that we've autowired in our scheduled task:
@Bean WebClient webClient(ReactiveClientRegistrationRepository clientRegistrations) { ServerOAuth2AuthorizedClientExchangeFilterFunction oauth = new ServerOAuth2AuthorizedClientExchangeFilterFunction( clientRegistrations, new UnAuthenticatedServerOAuth2AuthorizedClientRepository()); oauth.setDefaultClientRegistrationId("bael"); return WebClient.builder() .filter(oauth) .build(); }
As we said, the client registration repository is automatically created and added to the context by Spring Boot.
The next thing to notice here is that we're using a UnAuthenticatedServerOAuth2AuthorizedClientRepository instance. This is due to the fact that no end-user will take part in the process since it's a machine-to-machine communication. Finally, we stated that we'd use the bael client registration by default.
Otherwise, we'd have to specify it by the time we define the request in the cron job:
webClient.get() .uri("//localhost:8084/retrieve-resource") .attributes( ServerOAuth2AuthorizedClientExchangeFilterFunction .clientRegistrationId("bael")) .retrieve() // ...
4.4. Testing
If we run our application with the DEBUG logging level enabled, we'll be able to see the calls that Spring Security is doing for us:
o.s.w.r.f.client.ExchangeFunctions: HTTP POST //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 //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!
We'll also notice that the second time the task runs, the application requests the resource without asking for a token first since the last one hasn't expired.
5. Spring Security 5 Support – Implementation Using the Authorization Code Flow
This grant type is usually used in cases where less-trusted third-party applications need to access resources.
5.1. Client and Provider Configurations
In order to execute the OAuth2 process using the Authorization Code flow, we'll need to define several more properties for our client registration and the 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=authorization_code 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/user spring.security.oauth2.client.provider.bael.user-name-attribute=name
Apart from the properties, we used in the previous section, this time we also need to include:
- An endpoint to authenticate on the Authentication Server
- The URL of an endpoint containing user information
- The URL of an endpoint in our application to which the user-agent will be redirected after authenticating
Of course, for well-known providers, the first two points don't need to be specified.
The redirect endpoint is created automatically by Spring Security.
By default, the URL configured for it is /[action]/oauth2/code/[registrationId], with only authorize and login actions permitted (in order to avoid an infinite loop).
This endpoint is in charge of:
- receiving the authentication code as a query param
- using it to obtain an access token
- creating the Authorized Client instance
- redirecting the user-agent back to the original endpoint
5.2. HTTP Security Configurations
Next, we'll need to configure the SecurityWebFilterChain.
The most common scenario is using Spring Security's OAuth2 Login capabilities to authenticate users and give them access to our endpoints and resources.
If that's our case, then just 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. Configuring the WebClient
Now it's time to put in place our WebClient instance:
@Bean WebClient webClient( ReactiveClientRegistrationRepository clientRegistrations, ServerOAuth2AuthorizedClientRepository authorizedClients) { ServerOAuth2AuthorizedClientExchangeFilterFunction oauth = new ServerOAuth2AuthorizedClientExchangeFilterFunction( clientRegistrations, authorizedClients); oauth.setDefaultOAuth2AuthorizedClient(true); return WebClient.builder() .filter(oauth) .build(); }
This time we're injecting both the client registration repository and the authorized client repository from the context.
We're also enabling the setDefaultOAuth2AuthorizedClient option. With it, the framework will try to obtain the client information from the current Authentication object managed in Spring Security.
We have to take into account that with it, all HTTP requests will include the access token, which might not be the desired behavior.
Later we'll analyze alternatives to indicate the client that a specific WebClient transaction will use.
5.4. Using the WebClient
The Authorization Code requires a user-agent that can work out redirections (e.g., a browser) to execute the procedure.
Therefore, we make use of this grant type when the user is interacting with our application, usually calling an HTTP endpoint:
@RestController public class 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 retrieved the following resource using Oauth: " + string); } }
5.5. Testing
Finally, we'll call the endpoint and analyze what's going on by checking the log entries.
After we call the endpoint, the application verifies that we're not yet authenticated in the application:
o.s.w.s.adapter.HttpWebHandlerAdapter: HTTP GET "/auth-code" ... HTTP/1.1 302 Found Location: /oauth2/authorization/bael
The application redirects to the Authorization Service's endpoint to authenticate using credentials existing in the Provider's registries (in our case, we'll use the bael-user/bael-password):
HTTP/1.1 302 Found Location: //localhost:8085/oauth/authorize ?response_type=code &client_id=bael-client-id &state=... &redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Flogin%2Foauth2%2Fcode%2Fbael
After authenticating, the user-agent is sent back to the Redirect URI, together with the code as a query param and the state value that was first sent (to avoid CSRF attacks):
o.s.w.s.adapter.HttpWebHandlerAdapter:HTTP GET "/login/oauth2/code/bael?code=...&state=...
The application then uses the code to obtain an access token:
o.s.w.r.f.client.ExchangeFunctions:HTTP POST //localhost:8085/oauth/token
It obtains users information:
o.s.w.r.f.client.ExchangeFunctions:HTTP GET //localhost:8084/user
And it redirects the user-agent to the original endpoint:
HTTP/1.1 302 Found Location: /auth-code
Finally, our WebClient instance can request the secured resource successfully:
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 :Decoded "This is the resource!"
6. An Alternative – Client Registration in the Call
Earlier, we saw that using the setDefaultOAuth2AuthorizedClientimplies that the application will include the access token in any call we realize with the client.
If we remove this command from the configuration, we'll need to specify the client registration explicitly by the time we define the request.
One way, of course, is by using the clientRegistrationId as we did before when working in the client credentials flow.
Since we associated the Principal with authorized clients, we can obtain the OAuth2AuthorizedClient instance using the @RegisteredOAuth2AuthorizedClient annotation:
@GetMapping("/auth-code-annotated") Mono useOauthWithAuthCodeAndAnnotation( @RegisteredOAuth2AuthorizedClient("bael") OAuth2AuthorizedClient authorizedClient) { Mono retrievedResource = webClient.get() .uri("//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. Avoiding the OAuth2 Login Features
As we said, the most common scenario is relying on the OAuth2 authorization provider to login users in our application.
But what if we want to avoid this, but still be able to access secured resources using the OAuth2 protocol? Then we'll need to make some changes in our configuration.
For starters, and just to be clear across the board, we can use the authorize action instead of the login one when defining the redirect URI property:
spring.security.oauth2.client.registration.bael .redirect-uri=//localhost:8080/login/oauth2/code/bael
We can also drop the user-related properties since we won't be using them to create the Principal in our application.
Now, we'll configure the SecurityWebFilterChain without including the oauth2Login command, and instead, we'll include the oauth2Client one.
Even though we don't want to rely on the OAuth2 Login, we still want to authenticate users before accessing our endpoint. For this reason, we'll also include the formLogin directive here:
@Bean public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { http.authorizeExchange() .anyExchange() .authenticated() .and() .oauth2Client() .and() .formLogin(); return http.build(); }
Let's now run the application and check out what happens when we use the /auth-code-annotated endpoint.
We'll first have to log in to our application using the form login.
Afterward, the application will redirect us to the Authorization Service login, to grant access to our resources.
Note: after doing this, we should be redirected back to the original endpoint that we called. Nevertheless, Spring Security seems to be redirecting back to the root path “/” instead, which seems to be a bug. The following requests after the one triggering the OAuth2 dance will run successfully.
We can see in the endpoint response that the authorized client this time is associated with a principal named bael-client-id instead of the bael-user, named after the user configured in the Authentication Service.
8. Spring Framework Support – Manual Approach
Out of the box, 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.
We'll now see an example to understand what it would take to obtain our secured resource by performing an OAuth2 dance manually.
Simply put, we'll need to chain two HTTP requests: one to get an authentication token from the Authorization Server, and the other to obtain the resource using this 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); }
This example is mainly to understand how cumbersome it can be to leverage a request following the OAuth2 specification and to see how the setBearerAuth method is used.
Dalam senario kehidupan sebenar, kami membiarkan Spring Security mengurus semua kerja keras kami dengan telus, seperti yang kami lakukan di bahagian sebelumnya.
9. Kesimpulannya
Dalam tutorial ini, kita telah melihat bagaimana kita dapat mengatur aplikasi kita sebagai Klien OAuth2, dan lebih khusus lagi, bagaimana kita dapat mengkonfigurasi dan menggunakan WebClient untuk mendapatkan sumber yang diamankan dalam tumpukan reaktif penuh.
Akhir sekali, kami telah menganalisis bagaimana mekanisme Spring Security 5 OAuth2 beroperasi di bawah tudung untuk mematuhi spesifikasi OAuth2.
Seperti biasa, contoh lengkap boleh didapati di Github.