I have used the official spring-security-jwt provided by Spring as an implementation of JWT. This toolkit is no longer maintained. And it is not particularly compatible with the latest Spring Security OAuth2 Client and Spring Authorization Server. So I took two days to re-implement JWT with these two new dependencies.

Nimbus Library

The JOSE library nimbus-jose-jwt from Nimbus is used by default in the latest Spring Security. This library is currently one of the most used JOSE class libraries and most of the transformation work has been done around this library.

The process of conversion

The process is roughly the same as the Spring Security hands-on dry run.

Load the certificate

The certificate still uses keytool to generate a 2048-length RSA key.

Here we used a more “violent” way to read KeyStore directly and then use public and private keys, this time the certificate loaded by KeyStore is transformed into JWK (Json Web Key) in JOSE specification.

JWT

JWT is defined in Spring Security as org.springframework.security.oauth2.jwt.Jwt object, and operations on JWT can be abstracted into two aspects.

Generating JWTs

The first is JWT generation, which is not provided by Spring Security itself, but only by the incubating Spring Authorization Server, which provides the abstract interface JwtEncoder for JWT generation.

1
2
3
4
@FunctionalInterface
public interface JwtEncoder {
   Jwt encode(JoseHeader headers, JwtClaimsSet claims) throws JwtEncodingException;
}

JWT’s Header and Claims are abstracted accordingly as JoseHeader and JwtClaimsSet.

So I used Nimbus to implement JwtEncoder, which is actually a port of the Spring Authorization Server implementation. Of course, it’s not a copy of the original, it’s just to make sure the facade is consistent, so that if the project matures, we can be seamlessly compatible.

Parsing JWT

Since there is JwtEncoder, there must be JwtDecoder. This is provided in the Spring Security OAuth2 Client and is also slightly modified. In addition, this decoder is not only responsible for parsing JWT strings into JWT objects, but it also takes on the validation function, where there is a delegating validator DelegatingOAuth2TokenValidator that we can flexibly customize to perform multiple JWT validation policies.

Token pairs

We all know that usually Tokens in JWTs come in pairs. Previously, we simply used a class to encapsulate the string form of accessToken and refreshToken. This time we use the OAuth2AccessTokenResponse provided by spring ecurity oauth2 core :

1
2
3
4
5
6
7
8
public final class OAuth2AccessTokenResponse {

   private OAuth2AccessToken accessToken;

   private OAuth2RefreshToken refreshToken;

   private Map<String, Object> additionalParameters;
}

This class expresses much richer and more flexible content. The corresponding json is as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
{
  "accessToken": {
    "tokenValue": "eyJraWQiOiJmZWxvcmRjbiIsInR5cCI6IkpXVCIsImFsZyI6IlJTMjU2In0.eyJzdWIiOiJhbGwiLCJhdWQiOiJyb290IiwiaXNzIjoiaHR0cHM6XC9cL2ZlbG9yZC5jbiIsInNjb3BlcyI6WyJST0xFX0FETUlOIiwiUk9MRV9BUFAiXSwiZXhwIjoxNjE2ODM4NTg4LCJpYXQiOjE2MTY4MzQ5ODgsImp0aSI6IjBiYTUwZjFhLTI0N2YtNDJlYi05NzZiLTkyZWM5NDg2YjA2MCJ9.dwUK4ZgqhalKWu5AA8ZqaHjD2WPerhiF8lmybZGAorbncWdfVk7iAKUdRZunUekZmab_FsVpwprWIQpqSLtp6tz28sI71gO2StEeye5Vv4JRZKys68q2LGOAqMVJnBisEl211b5ASHSlP1qleU_TDxO_rgems76ZFD-kc1KmyelsoiBhmT3aD2_A_3fUmH7mV0jnC0rHauzOpS0AWnuPJaXbGPqrWotkQ_oqly47jipfNsPl_PUY1urng1wSx4QyblS8UgK-n5wJABhSN550WlwNLuC10ZckbhE5gazM0mD86mA_Xepe7LY5rjGNvO-Cz9k44TaURnTdSBdyy_EOiQ",
    "issuedAt": {
      "epochSecond": 1616834988,
      "nano": 891000000
    },
    "expiresAt": {
      "epochSecond": 1616838588,
      "nano": 891000000
    },
    "tokenType": {
      "value": "Bearer"
    },
    "scopes": [
      "ROLE_ADMIN",
      "ROLE_APP"
    ]
  },
  "refreshToken": {
    "tokenValue": "eyJraWQiOiJmZWxvcmRjbiIsInR5cCI6IkpXVCIsImFsZyI6IlJTMjU2In0.eyJzdWIiOiJhbGwiLCJhdWQiOiJyb290IiwiaXNzIjoiaHR0cHM6XC9cL2ZlbG9yZC5jbiIsInNjb3BlcyI6WyJST0xFX0FETUlOIiwiUk9MRV9BUFAiXSwiaWF0IjoxNjE2ODM0OTg4LCJqdGkiOiI3N2RhODk3NC0xMjM0LTQ5NzctOWU1MS1hOGY2NTdjMzA2NjAifQ.O9YYxkevkrTke7GbK2R5LGphnJ9vd07yFSwPs2gEZ94ObPkIs1wJ5gvlNOIlni_BYMNO-nMB8TiX0w-RQSwo-sbVLqeUHqv6NEXXmPJiWVmXTFVJf2b6lqW5Re7clXGvkFMw14ptAF6cpThDEE5XF4eCI8CDKKPWqNxY-8NvokwIY3NMXB1ofuHHRqjMyVUwNjOv6eaTJFTwebPy6Saem9kvaL_X1v9Drok6azbg5DSP1zKnbVazTaOs4aBZd5Firib3r_BGXdaJWAgJKfpP61__muVdujgkppMVU8fC9pqfnb6IqEaAOIZ69lrezA1K0QFinOhgcC2YZFxFoLL-IQ",
    "issuedAt": {
      "epochSecond": 1616834988,
      "nano": 891000000
    },
    "expiresAt": null
  },
  "additionalParameters": {}
}

Summary

The rest is largely unchanged, keeping as much of the original flavor as possible, while being compatible with the future style of Spring Security. How can you be compatible and flexible at the same time during code iteration? The key is to have a unified entry abstraction and exit abstraction. If you can do this, the quality of your code will be significantly better.

Reference https://felord.cn/new-jwt.html