Server Sumber Daya OAuth 2.0 Dengan Keamanan Musim Semi 5

1. Ikhtisar

Dalam tutorial ini, kita akan mempelajari cara menyiapkan server sumber daya OAuth 2.0 menggunakan Spring Security 5 .

Kami akan melakukan ini menggunakan JWT serta token buram, dua jenis token pembawa yang didukung oleh Keamanan Musim Semi.

Sebelum beralih ke implementasi dan contoh kode, kita akan membuat beberapa latar belakang.

2. Sedikit Latar Belakang

2.1. Apa itu JWT dan Token Buram?

JWT, atau JSON Web Token adalah cara untuk mentransfer informasi sensitif dengan aman dalam format JSON yang diterima secara luas. Informasi yang terkandung bisa tentang pengguna, atau tentang token itu sendiri, seperti kedaluwarsa dan penerbitnya.

Di sisi lain, token buram, seperti namanya, adalah buram dalam hal informasi yang dibawanya. Token hanyalah pengenal yang menunjuk ke informasi yang disimpan di server otorisasi - itu divalidasi melalui introspeksi di ujung server.

2.2. Apa itu Resource Server?

Dalam konteks OAuth 2.0, server sumber daya adalah aplikasi yang melindungi sumber daya melalui token OAuth . Token ini dikeluarkan oleh server otorisasi, biasanya untuk aplikasi klien. Tugas server sumber daya adalah memvalidasi token sebelum menyajikan sumber daya ke klien.

Validitas token ditentukan oleh beberapa hal:

  • Apakah token ini berasal dari server otorisasi yang dikonfigurasi?
  • Apakah itu belum kedaluwarsa?
  • Apakah server sumber daya ini adalah audiens yang dituju?
  • Apakah token memiliki otoritas yang diperlukan untuk mengakses sumber daya yang diminta?

Untuk memvisualisasikannya, mari kita lihat diagram urutan untuk alur kode otorisasi dan lihat semua aktor beraksi:

Seperti yang bisa kita lihat di langkah 8, saat aplikasi klien memanggil API server sumber daya untuk mengakses sumber daya yang dilindungi, pertama-tama aplikasi tersebut masuk ke server otorisasi untuk memvalidasi token yang terdapat dalam header Permintaan : Otorisasi: Pembawa , dan kemudian merespons ke klien.

Langkah 9 adalah fokus kami dalam tutorial ini.

Baiklah, sekarang mari beralih ke bagian kode. Kami akan menyiapkan server otorisasi menggunakan Keycloak, server sumber daya yang memvalidasi token JWT, server sumber daya lain yang memvalidasi token buram, dan beberapa pengujian JUnit untuk menyimulasikan aplikasi klien dan memverifikasi tanggapan.

3. Server Otorisasi

Pertama, kami akan menyiapkan server otorisasi, atau hal yang mengeluarkan token.

Untuk itu, kami akan menggunakan Keycloak yang disematkan di Aplikasi Spring Boot . Keycloak adalah identitas sumber terbuka dan solusi manajemen akses. Karena kami berfokus pada server sumber daya dalam tutorial ini, kami tidak akan mendalami lebih dalam.

Server Keycloak tertanam kami memiliki dua klien yang ditentukan - fooClient dan barClient - sesuai dengan dua aplikasi server sumber daya kami.

4. Server Sumber Daya - Menggunakan JWT

Server sumber daya kami akan memiliki empat komponen utama:

  • Model - sumber daya untuk dilindungi
  • API - pengontrol REST untuk mengekspos sumber daya
  • Konfigurasi Keamanan - kelas untuk menentukan kontrol akses untuk sumber daya yang dilindungi yang diekspos oleh API
  • application.yml - file konfigurasi untuk mendeklarasikan properti, termasuk informasi tentang server otorisasi

Mari kita lihat satu per satu untuk server sumber daya kami yang menangani token JWT, setelah mengintip dependensinya.

4.1. Dependensi Maven

Terutama, kita memerlukan server spring-boot-starter-oauth2-resource-server , starter Spring Boot untuk dukungan server sumber daya. Pemula ini menyertakan Keamanan Musim Semi secara default, jadi kami tidak perlu menambahkannya secara eksplisit:

 org.springframework.boot spring-boot-starter-web 2.2.6.RELEASE   org.springframework.boot spring-boot-starter-oauth2-resource-server 2.2.6.RELEASE   org.apache.commons commons-lang3 3.9 

Selain itu, kami juga menambahkan dukungan web.

Untuk tujuan demonstrasi kami, kami akan menghasilkan sumber daya secara acak alih-alih mendapatkannya dari database, dengan bantuan dari pustaka commons-lang3 Apache .

4.2. Model

Sederhananya, kami akan menggunakan Foo , sebuah POJO, sebagai sumber daya terlindungi kami:

public class Foo { private long id; private String name; // constructor, getters and setters } 

4.3. API

Inilah pengontrol istirahat kami, agar Foo tersedia untuk manipulasi:

@RestController @RequestMapping(value = "/foos") public class FooController { @GetMapping(value = "/{id}") public Foo findOne(@PathVariable Long id) { return new Foo(Long.parseLong(randomNumeric(2)), randomAlphabetic(4)); } @GetMapping public List findAll() { List fooList = new ArrayList(); fooList.add(new Foo(Long.parseLong(randomNumeric(2)), randomAlphabetic(4))); fooList.add(new Foo(Long.parseLong(randomNumeric(2)), randomAlphabetic(4))); fooList.add(new Foo(Long.parseLong(randomNumeric(2)), randomAlphabetic(4))); return fooList; } @ResponseStatus(HttpStatus.CREATED) @PostMapping public void create(@RequestBody Foo newFoo) { logger.info("Foo created"); } }

Sebagai bukti, kami memiliki ketentuan untuk GET semua Foo , GET a Foo dengan id, dan POST a Foo .

4.4. Konfigurasi Keamanan

Di kelas konfigurasi ini, kami menentukan tingkat akses untuk sumber daya kami:

@Configuration public class JWTSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests(authz -> authz .antMatchers(HttpMethod.GET, "/foos/**").hasAuthority("SCOPE_read") .antMatchers(HttpMethod.POST, "/foos").hasAuthority("SCOPE_write") .anyRequest().authenticated()) .oauth2ResourceServer(oauth2 -> oauth2.jwt()); } } 

Siapa pun dengan token akses yang memiliki cakupan baca bisa mendapatkan Foo . Untuk melakukan POST Foo baru , token mereka harus memiliki cakupan tulis .

Selain itu, kami menambahkan panggilan ke jwt () menggunakan DSL oauth2ResourceServer () untuk menunjukkan jenis token yang didukung oleh server kami di sini .

4.5. application.yml

Di properti aplikasi, selain nomor port biasa dan jalur konteks, kami perlu menentukan jalur ke URI penerbit server otorisasi kami sehingga server sumber daya dapat menemukan konfigurasi penyedia :

server: port: 8081 servlet: context-path: /resource-server-jwt spring: security: oauth2: resourceserver: jwt: issuer-uri: //localhost:8083/auth/realms/baeldung

Server sumber daya menggunakan informasi ini untuk memvalidasi token JWT yang masuk dari aplikasi klien, sesuai Langkah 9 diagram urutan kami.

Agar validasi ini bekerja menggunakan properti penerbit-uri , server otorisasi harus aktif dan berjalan. Jika tidak, server sumber daya tidak akan mulai.

Jika kita perlu memulainya secara independen, maka kita dapat menyediakan properti jwk-set-uri sebagai gantinya untuk menunjuk ke titik akhir server otorisasi yang mengekspos kunci publik:

jwk-set-uri: //localhost:8083/auth/realms/baeldung/protocol/openid-connect/certs

Dan hanya itu yang kami butuhkan agar server kami memvalidasi token JWT.

4.6. Menguji

Untuk pengujian, kami akan menyiapkan JUnit. Untuk menjalankan tes ini, kita membutuhkan server otorisasi serta server sumber daya yang aktif dan berjalan.

Let's verify that we can get Foos from resource-server-jwt with a read scoped token in our test:

@Test public void givenUserWithReadScope_whenGetFooResource_thenSuccess() { String accessToken = obtainAccessToken("read"); Response response = RestAssured.given() .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) .get("//localhost:8081/resource-server-jwt/foos"); assertThat(response.as(List.class)).hasSizeGreaterThan(0); }

In the above code, at Line #3 we obtain an access token with read scope from the authorization server, covering Steps from 1 through 7 of our sequence diagram.

Step 8 is performed by RestAssured‘s get() call. Step 9 is performed by the resource server with the configurations we saw and is transparent to us as users.

5. Resource Server – Using Opaque Tokens

Next, let's see the same components for our resource server handling opaque tokens.

5.1. Maven Dependencies

To support opaque tokens, we'll additionally need the oauth2-oidc-sdk dependency:

 com.nimbusds oauth2-oidc-sdk 8.19 runtime 

5.2. Model and Controller

For this one, we'll add a Bar resource:

public class Bar { private long id; private String name; // constructor, getters and setters } 

We'll also have a BarController with endpoints similar to our FooController before, to dish out Bars.

5.3. application.yml

In the application.yml here, we'll need to add an introspection-uri corresponding to our authorization server's introspection endpoint. As mentioned before, this is how an opaque token gets validated:

server: port: 8082 servlet: context-path: /resource-server-opaque spring: security: oauth2: resourceserver: opaque: introspection-uri: //localhost:8083/auth/realms/baeldung/protocol/openid-connect/token/introspect introspection-client-id: barClient introspection-client-secret: barClientSecret

5.4. Security Configuration

Keeping access levels similar to that of Foo for the Bar resource as well, this configuration class also makes a call to opaqueToken() using the oauth2ResourceServer() DSL to indicate the use of the opaque token type:

@Configuration public class OpaqueSecurityConfig extends WebSecurityConfigurerAdapter { @Value("${spring.security.oauth2.resourceserver.opaque.introspection-uri}") String introspectionUri; @Value("${spring.security.oauth2.resourceserver.opaque.introspection-client-id}") String clientId; @Value("${spring.security.oauth2.resourceserver.opaque.introspection-client-secret}") String clientSecret; @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests(authz -> authz .antMatchers(HttpMethod.GET, "/bars/**").hasAuthority("SCOPE_read") .antMatchers(HttpMethod.POST, "/bars").hasAuthority("SCOPE_write") .anyRequest().authenticated()) .oauth2ResourceServer(oauth2 -> oauth2 .opaqueToken(token -> token.introspectionUri(this.introspectionUri) .introspectionClientCredentials(this.clientId, this.clientSecret))); } } 

Here we're also specifying the client credentials corresponding to the authorization server's client we'll be using. We defined these earlier in our application.yml.

5.5. Testing

We'll set up a JUnit for our opaque token-based resource server, similar to how we did it for the JWT one.

In this case, let's check if a write scoped access token can POST a Bar to resource-server-opaque:

@Test public void givenUserWithWriteScope_whenPostNewBarResource_thenCreated() { String accessToken = obtainAccessToken("read write"); Bar newBar = new Bar(Long.parseLong(randomNumeric(2)), randomAlphabetic(4)); Response response = RestAssured.given() .contentType(ContentType.JSON) .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) .body(newBar) .log() .all() .post("//localhost:8082/resource-server-opaque/bars"); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED.value()); }

If we get a status of CREATED back, it means the resource server successfully validated the opaque token and created the Bar for us.

6. Conclusion

In this tutorial, we saw how to configure a Spring Security based resource server application for validating JWT as well as opaque tokens.

As we saw, with minimal setup, Spring made it possible to seamlessly validate the tokens with an issuer and send resources to the requesting party – in our case, a JUnit test.

As always, source code is available over on GitHub.