Verwenden von JWT mit Spring Security OAuth

1. Übersicht

In diesem Tutorial wird erläutert, wie Sie unsere Spring Security OAuth2-Implementierung dazu bringen, JSON-Web-Token zu verwenden.

Wir bauen auch weiterhin auf dem Artikel Spring REST API + OAuth2 + Angular in dieser OAuth-Serie auf.

2. Der OAuth2-Autorisierungsserver

Zuvor bot der Spring Security OAuth-Stack die Möglichkeit, einen Autorisierungsserver als Spring-Anwendung einzurichten. Wir mussten es dann für die Verwendung von JwtTokenStore konfigurieren, damit wir JWT-Token verwenden konnten.

Der OAuth-Stack wurde jedoch von Spring nicht mehr unterstützt und jetzt verwenden wir Keycloak als Autorisierungsserver.

Dieses Mal richten wir unseren Autorisierungsserver als eingebetteten Keycloak-Server in einer Spring Boot-App ein . Standardmäßig werden JWT-Token ausgegeben, sodass diesbezüglich keine weitere Konfiguration erforderlich ist.

3. Ressourcenserver

Schauen wir uns nun an, wie Sie unseren Ressourcenserver für die Verwendung von JWT konfigurieren.

Wir machen das in einer application.yml- Datei:

server: port: 8081 servlet: context-path: /resource-server spring: security: oauth2: resourceserver: jwt: issuer-uri: //localhost:8083/auth/realms/baeldung jwk-set-uri: //localhost:8083/auth/realms/baeldung/protocol/openid-connect/certs

JWTs enthalten alle Informationen im Token. Der Ressourcenserver muss daher die Signatur des Tokens überprüfen, um sicherzustellen, dass die Daten nicht geändert wurden. Die JWK-Set-uri Eigenschaft enthält den öffentlichen Schlüssel , dass der Server für diesen Zweck verwenden kann .

Die Eigenschaft issuer-uri verweist auf den Basis-Authorization-Server-URI, der auch als zusätzliche Sicherheitsmaßnahme zum Überprüfen des iss- Anspruchs verwendet werden kann.

Wenn die Eigenschaft jwk-set-uri nicht festgelegt ist, versucht der Ressourcenserver, mithilfe der Aussteller-Benutzeroberfläche den Speicherort dieses Schlüssels vom Metadatenendpunkt des Authorization Servers aus zu bestimmen.

Wenn Sie die Eigenschaft issuer-uri hinzufügen , müssen Sie den Authorization Server ausführen, bevor Sie die Resource Server-Anwendung starten können .

Nun wollen wir sehen, wie wir die JWT-Unterstützung mithilfe der Java-Konfiguration konfigurieren können:

@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.cors() .and() .authorizeRequests() .antMatchers(HttpMethod.GET, "/user/info", "/api/foos/**") .hasAuthority("SCOPE_read") .antMatchers(HttpMethod.POST, "/api/foos") .hasAuthority("SCOPE_write") .anyRequest() .authenticated() .and() .oauth2ResourceServer() .jwt(); } }

Hier überschreiben wir die Standardkonfiguration für HTTP-Sicherheit. Daher müssen wir explizit angeben, dass dies ein Ressourcenserver sein soll und dass wir JWT-formatierte Zugriffstoken mit den Methoden oauth2ResourceServer () bzw. jwt () verwenden.

Die obige JWT-Konfiguration wird von der Standard-Spring Boot-Instanz bereitgestellt. Dies kann auch angepasst werden, wie wir in Kürze sehen werden.

4. Benutzerdefinierte Ansprüche im Token

Lassen Sie uns nun eine Infrastruktur einrichten, um dem vom Autorisierungsserver zurückgegebenen Zugriffstoken einige benutzerdefinierte Ansprüche hinzufügen zu können . Die vom Framework bereitgestellten Standardansprüche sind alle gut und schön, aber die meiste Zeit benötigen wir einige zusätzliche Informationen im Token, um sie auf der Clientseite verwenden zu können.

Nehmen wir ein Beispiel für einen benutzerdefinierten Anspruch, eine Organisation , die den Namen der Organisation eines bestimmten Benutzers enthält.

4.1. Konfiguration des Autorisierungsservers

Dazu müssen wir unserer Realm-Definitionsdatei baeldung-Realm.json einige Konfigurationen hinzufügen :

  • Fügen Sie ein Attribut Organisation unserer Benutzer [email protected] :
    "attributes" : { "organization" : "baeldung" },
  • Fügen Sie der jwtClient- Konfiguration einen protocolMapper mit dem Namen organisation hinzu :
    "protocolMappers": [{ "id": "06e5fc8f-3553-4c75-aef4-5a4d7bb6c0d1", "name": "organization", "protocol": "openid-connect", "protocolMapper": "oidc-usermodel-attribute-mapper", "consentRequired": false, "config": { "userinfo.token.claim": "true", "user.attribute": "organization", "id.token.claim": "true", "access.token.claim": "true", "claim.name": "organization", "jsonType.label": "String" } }],

Bei einem eigenständigen Keycloak-Setup kann dies auch über die Admin-Konsole erfolgen.

Darüber hinaus ist zu beachten, dass die oben genannte JSON-Konfiguration für Keycloak spezifisch ist und sich für andere OAuth-Server unterscheiden kann .

Mit dieser neuen Konfiguration erhalten wir ein zusätzliches Attribut organisation = baeldung in der Token-Nutzlast für [email protected] :

{ jti: "989ce5b7-50b9-4cc6-bc71-8f04a639461e" exp: 1585242462 nbf: 0 iat: 1585242162 iss: "//localhost:8083/auth/realms/baeldung" sub: "a5461470-33eb-4b2d-82d4-b0484e96ad7f" typ: "Bearer" azp: "jwtClient" auth_time: 1585242162 session_state: "384ca5cc-8342-429a-879c-c15329820006" acr: "1" scope: "profile write read" organization: "baeldung" preferred_username: "[email protected]" }

4.2. Verwenden Sie das Zugriffstoken im Angular Client

Als Nächstes möchten wir die Token-Informationen in unserer Angular Client-Anwendung verwenden. Wir werden dafür die angular2-jwt-Bibliothek verwenden.

Wir werden den Organisationsanspruch in unserem AppService verwenden und eine Funktion getOrganization hinzufügen :

getOrganization(){ var token = Cookie.get("access_token"); var payload = this.jwtHelper.decodeToken(token); this.organization = payload.organization; return this.organization; }

Diese Funktion verwendet JwtHelperService aus der angular2-jwt- Bibliothek, um das Zugriffstoken zu dekodieren und unseren benutzerdefinierten Anspruch abzurufen . Jetzt müssen wir es nur noch in unserer AppComponent anzeigen :

@Component({ selector: 'app-root', template: ` Spring Security Oauth - Authorization Code 

{{organization}}

` }) export class AppComponent implements OnInit { public organization = ""; constructor(private service: AppService) { } ngOnInit() { this.organization = this.service.getOrganization(); } }

5. Greifen Sie auf zusätzliche Ansprüche im Ressourcenserver zu

Aber wie können wir auf der Seite des Ressourcenservers auf diese Informationen zugreifen?

5.1. Zugriff auf Authentifizierungsserveransprüche

Das ist wirklich einfach: Wir müssen nur aus dem Extrakt org.springframework.security.oauth2.jwt.Jwt s‘ AuthenticationPrincipal , wie wir für ein anderes Attribut in tun würden UserInfoController :

@GetMapping("/user/info") public Map getUserInfo(@AuthenticationPrincipal Jwt principal) { Map map = new Hashtable(); map.put("user_name", principal.getClaimAsString("preferred_username")); map.put("organization", principal.getClaimAsString("organization")); return Collections.unmodifiableMap(map); } 

5.2. Konfiguration zum Hinzufügen / Entfernen / Umbenennen von Ansprüchen

Was ist nun, wenn wir weitere Ansprüche auf der Seite des Ressourcenservers hinzufügen möchten? Oder einige entfernen oder umbenennen?

Angenommen, wir möchten den vom Authentifizierungsserver eingehenden Organisationsanspruch ändern , um den Wert in Großbuchstaben zu erhalten. Wenn der Anspruch für einen Benutzer nicht vorhanden ist, müssen wir seinen Wert als unbekannt festlegen .

Um dies zu erreichen, müssen wir zunächst eine Klasse hinzufügen, die die Converter- Schnittstelle implementiert und MappedJwtClaimSetConverter zum Konvertieren von Ansprüchen verwendet :

public class OrganizationSubClaimAdapter implements Converter
    
      { private final MappedJwtClaimSetConverter delegate = MappedJwtClaimSetConverter.withDefaults(Collections.emptyMap()); public Map convert(Map claims) { Map convertedClaims = this.delegate.convert(claims); String organization = convertedClaims.get("organization") != null ? (String) convertedClaims.get("organization") : "unknown"; convertedClaims.put("organization", organization.toUpperCase()); return convertedClaims; } }
    

Zweitens müssen wir in unserer SecurityConfig- Klasse unsere eigene JwtDecoder- Instanz hinzufügen , um die von Spring Boot bereitgestellte zu überschreiben, und unseren OrganizationSubClaimAdapter als Anspruchskonverter festlegen :

@Bean public JwtDecoder customDecoder(OAuth2ResourceServerProperties properties) { NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri( properties.getJwt().getJwkSetUri()).build(); jwtDecoder.setClaimSetConverter(new OrganizationSubClaimAdapter()); return jwtDecoder; } 

Now when we hit our /user/info API for the user [email protected], we'll get the organization as UNKNOWN.

Note that overriding the default JwtDecoder bean configured by Spring Boot should be done carefully to ensure all the necessary configuration is still included.

6. Loading Keys From a Java Keystore

In our previous configuration, we used the Authorization Server's default public key to verify our token's integrity.

We can also use a keypair and certificate stored in a Java Keystore file to do the signing process.

6.1. Generate JKS Java KeyStore File

Let's first generate the keys – and more specifically a .jks file – using the command line tool keytool:

keytool -genkeypair -alias mytest -keyalg RSA -keypass mypass -keystore mytest.jks -storepass mypass

The command will generate a file called mytest.jks which contains our keys – the Public and Private keys.

Also make sure keypass and storepass are the same.

6.2. Export Public Key

Next, we need to export our Public key from generated JKS, we can use the following command to do so:

keytool -list -rfc --keystore mytest.jks | openssl x509 -inform pem -pubkey

A sample response will look like this:

-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAgIK2Wt4x2EtDl41C7vfp OsMquZMyOyteO2RsVeMLF/hXIeYvicKr0SQzVkodHEBCMiGXQDz5prijTq3RHPy2 /5WJBCYq7yHgTLvspMy6sivXN7NdYE7I5pXo/KHk4nz+Fa6P3L8+L90E/3qwf6j3 DKWnAgJFRY8AbSYXt1d5ELiIG1/gEqzC0fZmNhhfrBtxwWXrlpUDT0Kfvf0QVmPR xxCLXT+tEe1seWGEqeOLL5vXRLqmzZcBe1RZ9kQQm43+a9Qn5icSRnDfTAesQ3Cr lAWJKl2kcWU1HwJqw+dZRSZ1X4kEXNMyzPdPBbGmU6MHdhpywI7SKZT7mX4BDnUK eQIDAQAB -----END PUBLIC KEY----- -----BEGIN CERTIFICATE----- MIIDCzCCAfOgAwIBAgIEGtZIUzANBgkqhkiG9w0BAQsFADA2MQswCQYDVQQGEwJ1 czELMAkGA1UECBMCY2ExCzAJBgNVBAcTAmxhMQ0wCwYDVQQDEwR0ZXN0MB4XDTE2 MDMxNTA4MTAzMFoXDTE2MDYxMzA4MTAzMFowNjELMAkGA1UEBhMCdXMxCzAJBgNV BAgTAmNhMQswCQYDVQQHEwJsYTENMAsGA1UEAxMEdGVzdDCCASIwDQYJKoZIhvcN AQEBBQADggEPADCCAQoCggEBAICCtlreMdhLQ5eNQu736TrDKrmTMjsrXjtkbFXj Cxf4VyHmL4nCq9EkM1ZKHRxAQjIhl0A8+aa4o06t0Rz8tv+ViQQmKu8h4Ey77KTM urIr1zezXWBOyOaV6Pyh5OJ8/hWuj9y/Pi/dBP96sH+o9wylpwICRUWPAG0mF7dX eRC4iBtf4BKswtH2ZjYYX6wbccFl65aVA09Cn739EFZj0ccQi10/rRHtbHlhhKnj iy+b10S6ps2XAXtUWfZEEJuN/mvUJ+YnEkZw30wHrENwq5QFiSpdpHFlNR8CasPn WUUmdV+JBFzTMsz3TwWxplOjB3YacsCO0imU+5l+AQ51CnkCAwEAAaMhMB8wHQYD VR0OBBYEFOGefUBGquEX9Ujak34PyRskHk+WMA0GCSqGSIb3DQEBCwUAA4IBAQB3 1eLfNeq45yO1cXNl0C1IQLknP2WXg89AHEbKkUOA1ZKTOizNYJIHW5MYJU/zScu0 yBobhTDe5hDTsATMa9sN5CPOaLJwzpWV/ZC6WyhAWTfljzZC6d2rL3QYrSIRxmsp /J1Vq9WkesQdShnEGy7GgRgJn4A8CKecHSzqyzXulQ7Zah6GoEUD+vjb+BheP4aN hiYY1OuXD+HsdKeQqS+7eM5U7WW6dz2Q8mtFJ5qAxjY75T0pPrHwZMlJUhUZ+Q2V FfweJEaoNB9w9McPe1cAiE+oeejZ0jq0el3/dJsx3rlVqZN+lMhRJJeVHFyeb3XF lLFCUGhA7hxn2xf3x1JW -----END CERTIFICATE-----

6.3. Maven Configuration

Next, we don't want the JKS file to be picked up by the maven filtering process – so we'll make sure to exclude it in the pom.xml:

   src/main/resources true  *.jks    

If we're using Spring Boot, we need to make sure that our JKS file is added to application classpath via the Spring Boot Maven Plugin – addResources:

   org.springframework.boot spring-boot-maven-plugin  true    

6.4. Authorization Server

Now, we will configure Keycloak to use our Keypair from mytest.jks, by adding it to the realm definition JSON file's KeyProvider section as follows:

{ "id": "59412b8d-aad8-4ab8-84ec-e546900fc124", "name": "java-keystore", "providerId": "java-keystore", "subComponents": {}, "config": { "keystorePassword": [ "mypass" ], "keyAlias": [ "mytest" ], "keyPassword": [ "mypass" ], "active": [ "true" ], "keystore": [ "src/main/resources/mytest.jks" ], "priority": [ "101" ], "enabled": [ "true" ], "algorithm": [ "RS256" ] } },

Here we have set the priority to 101, greater than any other Keypair for our Authorization Server, and set active to true. This is done to ensure that our Resource Server would pick this particular Keypair from the jwk-set-uri property we specified earlier.

Auch diese Konfiguration ist spezifisch für Keycloak und kann für andere OAuth Server-Implementierungen unterschiedlich sein.

7. Fazit

In diesem kurzen Artikel haben wir uns darauf konzentriert, unser Spring Security OAuth2-Projekt für die Verwendung von JSON-Web-Tokens einzurichten.

Die vollständige Implementierung dieses Tutorials finden Sie auf GitHub.