OAuth 2.0-Ressourcenserver mit Spring Security 5

1. Übersicht

In diesem Lernprogramm erfahren Sie, wie Sie einen OAuth 2.0-Ressourcenserver mit Spring Security 5 einrichten .

Wir werden dies sowohl mit JWT als auch mit undurchsichtigen Token tun, den beiden Arten von Inhaber-Token, die von Spring Security unterstützt werden.

Bevor wir zu den Implementierungs- und Codebeispielen übergehen, werden wir einige Hintergrundinformationen erstellen.

2. Ein kleiner Hintergrund

2.1. Was sind JWTs und undurchsichtige Token?

JWT oder JSON Web Token ist eine Möglichkeit, vertrauliche Informationen sicher im allgemein akzeptierten JSON-Format zu übertragen. Die enthaltenen Informationen können sich auf den Benutzer oder auf das Token selbst beziehen, z. B. auf dessen Ablauf und Aussteller.

Andererseits ist ein undurchsichtiges Token, wie der Name schon sagt, in Bezug auf die darin enthaltenen Informationen undurchsichtig. Das Token ist nur eine Kennung, die auf die auf dem Autorisierungsserver gespeicherten Informationen verweist. Es wird durch Introspektion am Ende des Servers überprüft.

2.2. Was ist ein Ressourcenserver?

Im Kontext von OAuth 2.0 ist ein Ressourcenserver eine Anwendung, die Ressourcen über OAuth-Token schützt . Diese Token werden von einem Autorisierungsserver ausgegeben, normalerweise an eine Clientanwendung. Die Aufgabe des Ressourcenservers besteht darin, das Token zu validieren, bevor dem Client eine Ressource bereitgestellt wird.

Die Gültigkeit eines Tokens wird durch verschiedene Dinge bestimmt:

  • Kam dieses Token vom konfigurierten Autorisierungsserver?
  • Ist es nicht abgelaufen?
  • Ist dieser Ressourcenserver seine Zielgruppe?
  • Verfügt das Token über die erforderliche Berechtigung, um auf die angeforderte Ressource zuzugreifen?

Schauen wir uns zur Visualisierung ein Sequenzdiagramm für den Autorisierungscode-Fluss an und sehen Sie alle Akteure in Aktion:

Wie wir in Schritt 8 sehen können, geht die Clientanwendung, wenn sie die API des Ressourcenservers aufruft, um auf eine geschützte Ressource zuzugreifen, zuerst zum Autorisierungsserver, um das im Header Authorization: Bearer der Anforderung enthaltene Token zu validieren , und antwortet dann dem Client.

In diesem Tutorial konzentrieren wir uns auf Schritt 9.

Also gut, jetzt springen wir in den Codeteil. Wir werden einen Autorisierungsserver mit Keycloak einrichten, einen Ressourcenserver, der JWT-Token überprüft, einen anderen Ressourcenserver, der undurchsichtige Token überprüft, und einige JUnit-Tests, um Client-Apps zu simulieren und Antworten zu überprüfen.

3. Autorisierungsserver

Zuerst richten wir einen Autorisierungsserver ein oder das, was Token ausstellt.

Dafür verwenden wir Keycloak, das in eine Spring Boot-Anwendung eingebettet ist . Keycloak ist eine Open-Source-Lösung für das Identitäts- und Zugriffsmanagement. Da wir uns in diesem Tutorial auf den Ressourcenserver konzentrieren, werden wir uns nicht näher damit befassen.

Für unseren eingebetteten Keycloak-Server sind zwei Clients definiert - fooClient und barClient - entsprechend unseren beiden Ressourcenserveranwendungen.

4. Ressourcenserver - Verwenden von JWTs

Unser Ressourcenserver wird vier Hauptkomponenten haben:

  • Modell - die zu schützende Ressource
  • API - ein REST-Controller zum Offenlegen der Ressource
  • Sicherheitskonfiguration - Eine Klasse zum Definieren der Zugriffssteuerung für die geschützte Ressource, die von der API verfügbar gemacht wird
  • application.yml - Eine Konfigurationsdatei zum Deklarieren von Eigenschaften, einschließlich Informationen zum Autorisierungsserver

Lassen Sie uns sie einzeln für unseren Ressourcenserver sehen, der JWT-Token verarbeitet, nachdem wir einen Blick auf die Abhängigkeiten geworfen haben.

4.1. Maven-Abhängigkeiten

Hauptsächlich benötigen wir den Spring-Boot-Starter-Oauth2-Ressourcenserver , den Starter von Spring Boot für die Unterstützung von Ressourcenservern. Dieser Starter enthält standardmäßig Spring Security, daher müssen wir ihn nicht explizit hinzufügen:

 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 

Abgesehen davon haben wir auch Web-Support hinzugefügt.

Zu Demonstrationszwecken generieren wir Ressourcen nach dem Zufallsprinzip, anstatt sie aus einer Datenbank abzurufen . Dabei wird die commons-lang3- Bibliothek von Apache unterstützt .

4.2. Modell

Um es einfach zu halten, verwenden wir Foo , ein POJO, als unsere geschützte Ressource:

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

4.3. API

Hier ist unser Rest-Controller, um Foo für Manipulationen verfügbar zu machen :

@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"); } }

Es ist offensichtlich, dass wir die Möglichkeit haben, alle Foo s zu erhalten, ein Foo anhand der ID zu erhalten und ein Foo zu veröffentlichen .

4.4. Sicherheitskonfiguration

In dieser Konfigurationsklasse definieren wir Zugriffsebenen für unsere Ressource:

@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()); } } 

Anyone with an access token having the read scope can get Foos. In order to POST a new Foo, their token should have a write scope.

Additionally, we added a call to jwt() using the oauth2ResourceServer() DSL to indicate the type of tokens supported by our server here.

4.5. application.yml

In the application properties, in addition to the usual port number and context-path, we need to define the path to our authorization server's issuer URI so that the resource server can discover its provider configuration:

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

The resource server uses this information to validate the JWT tokens coming in from the client application, as per Step 9 of our sequence diagram.

For this validation to work using the issuer-uri property, the authorization server must be up and running. Otherwise, the resource server wouldn't start.

If we need to start it independently, then we can supply the jwk-set-uri property instead to point to the authorization server's endpoint exposing public keys:

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

And that's all we need to get our server to validate JWT tokens.

4.6. Testing

For testing, we'll set up a JUnit. In order to execute this test, we need the authorization server as well as resource server up and running.

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.