Utilizzo di JWT con Spring Security OAuth

1. Panoramica

In questo tutorial, discuteremo come ottenere la nostra implementazione OAuth2 di Spring Security per utilizzare i token Web JSON.

Stiamo anche continuando a costruire sulla parte superiore dell'articolo Spring REST API + OAuth2 + Angular in questa serie OAuth.

2. Il server di autorizzazione OAuth2

In precedenza, lo stack OAuth di Spring Security offriva la possibilità di configurare un server di autorizzazione come applicazione Spring. Abbiamo quindi dovuto configurarlo per utilizzare JwtTokenStore in modo da poter utilizzare i token JWT.

Tuttavia, lo stack OAuth è stato deprecato da Spring e ora utilizzeremo Keycloak come server di autorizzazione.

Quindi questa volta configureremo il nostro server di autorizzazione come un server Keycloak incorporato in un'app Spring Boot . Emette token JWT per impostazione predefinita, quindi non è necessaria alcuna altra configurazione a questo proposito.

3. Resource Server

Ora, diamo un'occhiata a come configurare il nostro Resource Server per utilizzare JWT.

Lo faremo in un file application.yml :

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

I JWT includono tutte le informazioni all'interno del token. Quindi il server delle risorse deve verificare la firma del token per assicurarsi che i dati non siano stati modificati. Il JWK-set-uri proprietà contiene la chiave pubblica che il server può utilizzare per questo scopo .

La proprietà issuer-uri punta all'URI del server di autorizzazione di base, che può essere utilizzato anche per verificare l' attestazione iss , come misura di sicurezza aggiuntiva.

Inoltre, se la proprietà jwk-set-uri non è impostata, Resource Server tenterà di utilizzare issuer-ui per determinare la posizione di questa chiave, dall'endpoint dei metadati del server di autorizzazione.

È importante sottolineare che l'aggiunta della proprietà issuer-uri impone di avere il server di autorizzazione in esecuzione prima di poter avviare l'applicazione Resource Server .

Ora vediamo come possiamo configurare il supporto JWT usando la configurazione Java:

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

Qui, stiamo sovrascrivendo la configurazione di sicurezza Http predefinita. Quindi dobbiamo specificare esplicitamente che vogliamo che si comporti come un server di risorse e che utilizzeremo token di accesso formattati JWT utilizzando rispettivamente i metodi oauth2ResourceServer () e jwt () .

La configurazione JWT sopra è ciò che ci fornisce l'istanza Spring Boot predefinita. Questo può anche essere personalizzato come vedremo a breve.

4. Reclami personalizzati nel token

Configuriamo ora alcune infrastrutture per poter aggiungere alcune attestazioni personalizzate nel token di accesso restituito dal server di autorizzazione . Le affermazioni standard fornite dal framework sono tutte valide, ma la maggior parte delle volte avremo bisogno di alcune informazioni extra nel token da utilizzare dal lato client.

Facciamo un esempio di un'attestazione personalizzata, organizzazione , che conterrà il nome dell'organizzazione di un determinato utente.

4.1. Configurazione del server di autorizzazione

Per questo, dobbiamo aggiungere un paio di configurazioni al nostro file di definizione del realm, baeldung-realm.json :

  • Aggiungere un attributo organizzazione per il nostro utente [email protected] :
    "attributes" : { "organization" : "baeldung" },
  • Aggiungi un protocolMapper chiamato organizzazione alla configurazione jwtClient :
    "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" } }],

Per una configurazione di Keycloak autonoma, questa operazione può essere eseguita anche utilizzando la Console di amministrazione.

Inoltre, è importante ricordare che la configurazione JSON sopra è specifica per Keycloak e può differire per altri server OAuth .

Con questa nuova configurazione attiva e funzionante, avremo un attributo extra organization = baeldung , nel payload del token per [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. Usa il token di accesso nel client angolare

Successivamente, vorremo utilizzare le informazioni sul token nella nostra applicazione Angular Client. Per quello useremo la libreria angular2-jwt.

Utilizzeremo l' attestazione dell'organizzazione nel nostro AppService e aggiungeremo una funzione getOrganization :

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

Questa funzione fa uso di JwtHelperService dalla libreria angular2-jwt per decodificare il token di accesso e ottenere la nostra richiesta personalizzata. Ora tutto ciò che dobbiamo fare è visualizzarlo nel nostro AppComponent :

@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. Accedere a ulteriori attestazioni nel Resource Server

Ma come possiamo accedere a queste informazioni dal lato server risorse?

5.1. Accedere alle attestazioni del server di autenticazione

È davvero semplice: dobbiamo solo estrarlo da org.springframework.security.oauth2.jwt.Jwt 's AuthenticationPrincipal , come faremmo per qualsiasi altro attributo in 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. Configurazione per aggiungere / rimuovere / rinominare attestazioni

Ora, cosa succede se vogliamo aggiungere più attestazioni sul lato server risorse? O rimuoverne o rinominarne alcuni?

Supponiamo di voler modificare l'attestazione dell'organizzazione in arrivo dal server di autenticazione per ottenere il valore in maiuscolo. Inoltre, se l'attestazione non è presente su un utente, è necessario impostarne il valore come sconosciuto .

Per ottenere ciò, in primo luogo, dovremo aggiungere una classe che implementa l' interfaccia del convertitore e utilizza MappedJwtClaimSetConverter per convertire le attestazioni :

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; } }
    

In secondo luogo, nella nostra classe SecurityConfig , dobbiamo aggiungere la nostra istanza JwtDecoder per sovrascrivere quella fornita da Spring Boot e impostare OrganizationSubClaimAdapter come convertitore di attestazioni :

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

Ora quando premiamo la nostra API / user / info per l'utente [email protected] , otterremo l' organizzazione come SCONOSCIUTA .

Notare che l'override del bean JwtDecoder predefinito configurato da Spring Boot deve essere eseguito con attenzione per garantire che tutta la configurazione necessaria sia ancora inclusa.

6. Caricamento delle chiavi da un archivio chiavi Java

Nella nostra configurazione precedente, abbiamo utilizzato la chiave pubblica predefinita del server di autorizzazione per verificare l'integrità del nostro token.

Possiamo anche utilizzare una coppia di chiavi e un certificato memorizzati in un file Java Keystore per eseguire il processo di firma.

6.1. Genera file JKS Java KeyStore

Generiamo prima le chiavi, e più specificamente un file .jks , utilizzando lo strumento da riga di comando keytool :

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

Il comando genererà un file chiamato mytest.jks che contiene le nostre chiavi: le chiavi Pubblica e Privata.

Assicurati anche che keypass e storepass siano gli stessi.

6.2. Esporta chiave pubblica

Successivamente, dobbiamo esportare la nostra chiave pubblica dal JKS generato, possiamo usare il seguente comando per farlo:

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

Una risposta di esempio sarà simile a questa:

-----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. Configurazione Maven

Successivamente, non vogliamo che il file JKS venga prelevato dal processo di filtraggio Maven, quindi ci assicureremo di escluderlo nel 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.

Anche in questo caso, questa configurazione è specifica per Keycloak e può differire per altre implementazioni di OAuth Server.

7. Conclusione

In questo breve articolo ci siamo concentrati sulla configurazione del nostro progetto Spring Security OAuth2 per utilizzare i token Web JSON.

L'implementazione completa di questo tutorial può essere trovata in oltre su GitHub.