Menggunakan JWT dengan Spring Security OAuth

1. Gambaran keseluruhan

Dalam tutorial ini, kita akan membincangkan bagaimana mendapatkan pelaksanaan Spring Security OAuth2 kami untuk menggunakan Token Web JSON.

Kami juga terus membina artikel Spring REST API + OAuth2 + Angular dalam siri OAuth ini.

2. Pelayan Pengesahan OAuth2

Sebelumnya, tumpukan Spring Security OAuth menawarkan kemungkinan menyiapkan Server Pengesahan sebagai Aplikasi Musim Semi. Kami kemudian harus mengkonfigurasinya untuk menggunakan JwtTokenStore supaya kami dapat menggunakan token JWT.

Walau bagaimanapun, timbunan OAuth sudah tidak digunakan lagi oleh Spring dan sekarang kami akan menggunakan Keycloak sebagai Pelayan Pengesahan kami.

Oleh itu, kali ini, kami akan menetapkan Pelayan Pengesahan kami sebagai pelayan Keycloak tertanam dalam aplikasi Spring Boot . Ini mengeluarkan token JWT secara lalai, jadi tidak perlu konfigurasi lain dalam hal ini.

3. Pelayan Sumber

Sekarang, mari kita lihat bagaimana mengkonfigurasi Pelayan Sumber kami untuk menggunakan JWT.

Kami akan melakukan ini dalam fail 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

JWT merangkumi semua maklumat dalam Token. Oleh itu, Pelayan Sumber perlu mengesahkan tandatangan Token untuk memastikan data belum diubah. The jwk-set-uri harta mengandungi kunci awam yang pelayan boleh digunakan untuk tujuan ini .

The penerbit-uri mata harta kepada asas Kebenaran Server URI, yang juga boleh digunakan untuk mengesahkan iss tuntutan, sebagai langkah keselamatan tambahan.

Juga, jika sifat jwk-set-uri tidak ditetapkan, Pelayan Sumber akan berusaha menggunakan penerbit-ui untuk menentukan lokasi kunci ini, dari titik akhir metadata Pelayan Pengesahan.

Yang penting, menambahkan mandat properti penerbit-uri bahawa kita seharusnya menjalankan Server Pengesahan sebelum kita dapat memulakan aplikasi Pelayan Sumber .

Sekarang mari kita lihat bagaimana kita dapat mengkonfigurasi sokongan JWT menggunakan konfigurasi 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(); } }

Di sini, kita mengatasi konfigurasi Http Security lalai. Oleh itu, kita perlu menyatakan secara jelas bahawa kita mahu ini berperilaku sebagai Pelayan Sumber dan bahawa kita akan menggunakan Token Akses berformat JWT menggunakan kaedah masing-masing oauth2ResourceServer () dan jwt () .

Konfigurasi JWT di atas adalah yang disediakan oleh contoh Spring Boot lalai. Ini juga boleh disesuaikan seperti yang akan kita lihat sebentar lagi.

4. Tuntutan Tersuai dalam Token

Mari sekarang sediakan beberapa infrastruktur untuk dapat menambahkan beberapa tuntutan tersuai dalam Access Token yang dikembalikan oleh Server Pengesahan . Tuntutan standard yang disediakan oleh rangka kerja semuanya baik dan baik, tetapi selalunya kita memerlukan beberapa maklumat tambahan dalam token untuk digunakan di pihak Pelanggan.

Mari kita ambil contoh tuntutan khusus, organisasi , yang akan mengandungi nama organisasi pengguna tertentu.

4.1. Konfigurasi Pelayan Pengesahan

Untuk ini, kita perlu menambahkan beberapa konfigurasi ke fail definisi wilayah kita, baeldung-realm.json :

  • Tambahkan organisasi atribut ke pengguna kami [dilindungi e-mel] :
    "attributes" : { "organization" : "baeldung" },
  • Tambah protokol yang dipanggil organisasi untuk konfigurasi 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" } }],

Untuk penyediaan Keycloak yang berdiri sendiri, ini juga dapat dilakukan menggunakan konsol Admin.

Selain itu, penting untuk diingat bahawa konfigurasi JSON di atas adalah khusus untuk Keycloak, dan boleh berbeza untuk pelayan OAuth yang lain .

Dengan konfigurasi baru ini berjalan, kita akan mendapatkan organisasi atribut tambahan = baeldung , dalam muatan token untuk [dilindungi e-mel] :

{ 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. Gunakan Token Akses di Pelanggan Sudut

Seterusnya, kami ingin memanfaatkan maklumat Token dalam aplikasi Angular Client kami. Kami akan menggunakan perpustakaan angular2-jwt untuk itu.

Kami akan memanfaatkan tuntutan organisasi di AppService kami , dan menambahkan fungsi getOrganization :

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

Fungsi ini memanfaatkan JwtHelperService dari perpustakaan angular2-jwt untuk menyahkod Token Akses dan mendapatkan tuntutan tersuai kami. Sekarang yang perlu kita lakukan ialah memaparkannya di AppComponent kami :

@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. Akses Tuntutan Tambahan di Pelayan Sumber

Tetapi, bagaimana kita dapat mengakses maklumat tersebut dari sisi Pelayan Sumber?

5.1. Akses Tuntutan Pelayan Pengesahan

Itu sangat mudah: kita hanya perlu mengekstraknya dari org.springframework.security.oauth2.jwt.Jwt 's AuthenticationPrincipal , seperti yang akan kita lakukan untuk atribut lain dalam 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. Konfigurasi untuk Menambah / Menghapus / Menamakan Semula Tuntutan

Sekarang, bagaimana jika kita mahu menambahkan lebih banyak tuntutan di sisi Pelayan Sumber? Atau alih keluar atau menamakan semula beberapa?

Let's say we want to modify the organization claim coming in from the Authentication Server to get the value in uppercase. Additionally, if the claim is not present on a user, we need to set its value as unknown.

To achieve this, first, we'll have to add a class that implements the Converter interface and uses MappedJwtClaimSetConverter to convert claims:

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

Second, in our SecurityConfig class, we need to add our own JwtDecoder instance to override the one provided by Spring Boot and set our OrganizationSubClaimAdapter as its claims converter:

@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.

Sekali lagi, konfigurasi ini khusus untuk Keycloak dan mungkin berbeza untuk pelaksanaan OAuth Server yang lain.

7. Kesimpulannya

Dalam artikel ringkas ini, kami memberi tumpuan untuk menyiapkan projek Spring Security OAuth2 kami untuk menggunakan JSON Web Tokens.

Pelaksanaan penuh tutorial ini boleh didapati di GitHub.