preface

In daily development, the client and server usually use HTTP protocol to communicate, but HTTP is stateless, unable to record user identity information and behavior.

Session tracking technology is a solution to maintain HTTP state between client and server. We are familiar with Cookie + Session, URL rewriting, Token and so on.

Cookie stores the SessionID in the browser, and the actual Session content is stored in the server. At present, the projects are separated from the front and back ends + micro-services, so the Session sharing problem will be faced. As the number of users increases, the overhead will be larger. URL rewriting is transmitted through clear text, which is insecure and vulnerable to hijacking.

Advantages of Token:

  • The Token supports cross-domain access, but the Cookie cannot.
  • The Token supports multiple platforms, while the Cookie supports only some Web terminals.

What is JWT?

JWT, which stands for Json Web Token, is a JSON-based Token specification for making a claim on the network.

Official explanation:

The JWT consists of three parts: hand, payload, and signature. Each part is connected by a.

xxxx . yyyy . zzzz

1, the HEAD,

The header is a JSON object that stores the description data type (JWT) and signature algorithm (HSA256, RSA256). The head is encoded in Base64UrlEncode to generate the head.

Code: eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9 decoding: {"alg": "RS256"."typ": "JWT"
}
Copy the code

2, PAYLOAD

The payload holds some valid declarations for transport, either using official declarations or custom declarations. Payload is generated after the Base64UrlEncode is encoded. Declarations can be divided into three types:

  1. Registered claims:

Official predefined, optional, but recommended, interactive declarations (note that use of these declarations can only be three characters).

The name of the role
iss (issuer) Issued by people
sub (subject) The theme
aud (audience) The audience
exp (expiration time) Expiration time
nbf (Not Before) Effect of time
iat (Issued At) The issuance of time
jti (JWT ID) Serial number
  1. Public claims:

Reserved for JWT user customization. However, be careful not to use keywords defined in the IANA JSON Web Token Registry.

  1. Private claims:

Reserved for user customization of THE JWT for sending messages agreed upon by the transport parties. (– _ –) Do you understand the difference between public claims and private claims?

Code:  eyJhdWQiOiLopb_pl6jpmL_mtaoiLCJkYXRhIjoiXCLopb_pl6jpmL_mtapcIiIsImlzcyI6IkhBTkdIVUFfQURNSU4iLCJleHAiOjE2MjIzNjA4NDEsIml HdCI6MTYyMjM2MDg0MX0 decode: {"aud": "Simon Alang"."data": "\" Simon Lang \"."iss": "HANGHUA_ADMIN"."exp": 1622360841."iat": 1622360841
}
Copy the code

3, SIGNATURE

Data signature is a core part of JWT, which is complex and cannot be de-coded.

HS256 encryption: signature = HMACSHA256(base64UrlEncode(header) +"."+base64UrlEncode(payload), secret ); RS256 encryption: signature = RSASHA256(base64UrlEncode(header) +"." +base64UrlEncode(payload), publicKey, privateKey)
Copy the code

Signature Can be a symmetric or asymmetric encryption algorithm, such as HS256 and RS256.

  • Symmetric encryption: the encryptor and decryptor use the same secret key to encrypt and decrypt data.

  • Asymmetric encryption: the encryptor uses the private key for encryption and tells the decryptor the public key for decryption.

4. JWT executes logic

The logic is clear. When a user logs in for the first time, he/she authenticates his/her identity by transferring his/her account password. After successful authentication, the server generates a Token to respond to the user. The user only needs to transfer the Token in subsequent requests, and the server only needs to verify the Token to confirm the identity.

Dual tokens guarantee active users

Active users

If the validity period of the Token is too long for identity authentication, the Token is insecure. If the Settings are too short, users frequently log in again, and the programmer’s ancestral graves may be destroyed. So how do you define valid time? This brings us to the concept of user activity.

The system divides the users into active users and inactive users. Inactive users need to re-log in after their token expires because they use the token infrequently and re-log in after the token is deactivated. Their feelings are not so strong.

Active users should not log in directly after their tokens expire. Instead, they should decide whether to reactivate their tokens based on their active time. When the conditions are met, they should directly activate their tokens to bring the best experience to the users.

If the token validity period is at, the active user duration is RT, and the active time is updated with each operation of the client.

  • Rt is equal to at

In this case, of course, you can identify as active users. If the token expires and the user logs in again, the user experience is really bad.

  • Let’s say rt is greater than at

Since RT is an active user when it is equal to AT, it can be regarded as an activist.

  • When RT < at

This situation is complicated. We cannot define what proportion of RT in AT is active user, and we cannot predict when the user will request again after the token expires, so it is defined as inactive user.

AccessToken and refreshToken

After the user logs in for the first time, they get accessToken(short duration) and refreshToken(long duration), and judge whether accessToken is expired with each request.

When the Access_token expires, check whether refreshToken is expired. If not, refresh the access_token to obtain a new access_token. If both expire, you need to log in again.

According to the analysis of active users, when RT >= AT, users are active during the AT time. Set accessToken to the active time of the user (rt), and refresh RT when RT <= refresh_Time. Therefore, it can be considered that the user is active at [accessToken creation start point, 2 * accessToken valid duration]

Suggestion: refreshToken time >= 2 * accessToken time.

Auth0 solution

JWT is just a specification, like an interface in Java, that cannot be used directly and requires a concrete implementation library that implements the specification. JJWT is often used in development, and auth0 is rumored to be more efficient in the underlying implementation. Note that auth0 is not OAuth2, so don’t get confused.

First, add maven dependencies, the latest version of which is 3.16.0.

<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.16.0</version>
</dependency>
Copy the code

HS256 algorithm

HS256 is a symmetric encryption algorithm, which is relatively simple and easy to use. The online examples are also very detailed, so you can find information by yourself if you are interested. Let’s focus on asymmetric encryption algorithms.

RS256 algorithm

  • 1. Generate a key pair

To issue a Token, you must first generate a PublicKey and a PrivateKey. The JAVa.security.interfaces package of the JDK provides key pair types for the RS algorithm. We directly build a POJO class for the depository key pair.

public class RSA256Key {

    private RSAPublicKey publicKey;
    private RSAPrivateKey privateKey;

    public RSA256Key(a) {}public RSA256Key(RSAPublicKey publicKey, RSAPrivateKey privateKey) {
        this.publicKey = publicKey;
        this.privateKey = privateKey; }} *** omits getter and setter***Copy the code

Then write a tool class for key generation. According to the official information, the instance of the key pair can be used repeatedly. Therefore, I intend to use a singleton double check lock to control key object generation.

If the concurrency is too large, you can add a custom thread pool to generate.

    // Digital signature
    public static final String KEY_ALGORITHM = "RSA";

    // The length of the RSA key
    public static final int KEY_SIZE = 1024;

    // Unique key instance
    private static volatile RSA256Key rsa256Key;

  /** * Generate public/private key ** The double check lock guarantees the creation of a unique key instance, so there is only a unique instance after creation. * New instances are created only after they have been reclaimed by the JVM@return
     * @throws NoSuchAlgorithmException
     */
    public static RSA256Key generateRSA256Key(a) throws NoSuchAlgorithmException {

        // First check: the singleton mode only needs to create an instance, if there is an instance, do not need to continue to compete for the lock,
        if (rsa256Key == null) {
            // Double check lock for RSA256Key singleton
            synchronized(RSA256Key.class) {
                // Second check: prevent spin thread in lock contention, when get system resources, create instances repeatedly
                if (rsa256Key == null) {
                    // Random number source required for key generation
                    KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance(KEY_ALGORITHM);
                    keyPairGen.initialize(KEY_SIZE);
                    // generate the KeyPair KeyPair through KeyPairGenerator
                    KeyPair keyPair = keyPairGen.generateKeyPair();
                    // Obtain the public and private keys
                    RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
                    RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
                    rsa256Key = newRSA256Key(); rsa256Key.setPublicKey(publicKey); rsa256Key.setPrivateKey(privateKey); }}}return rsa256Key;
    }
Copy the code

The singleton’s double check lock ensures that the RSAPublicKey object is generated uniquely. When threads enter the generateRSA256Key() method to verify that the instance object is empty, the fastest thread takes the lock resource and blocks subsequent threads.

KeyPairGenerator is the core class for key generation, which generates keys according to our custom key length KEY_SIZE. RSA256Key RSA256Key RSA256Key RSA256Key RSA256Key RSA256Key RSA256Key RSA256Key RSA256Key Although synchronized blocked part of the thread, when the RSA256Key was instantiated before the value was assigned, it happened that a new line just detected the RSA256Key and directly jumped to the subsequent logic, because the key instance value was empty and an exception was reported

  • 2. Issue the Token

Auth0 is a good package for us. We only need to pass the private key to Algorithm’s static method RSA256 and pass the parameter withXXX() in JWT.

 /** * Issue Token ** withIssuer() Add a hop of data to the PAYLOAD => Token issuer * withClaim() Add a hop of data to the PAYLOAD => User-defined statement (key, Value) * withIssuedAt() Adds a data to PAYLOAD => Generation time * withExpiresAt() adds a data to PAYLOAD => expiration date * *@param data
     * @return
     * @throws NoSuchAlgorithmException
     */
    public static String creatTokenByRS256(Object data) throws NoSuchAlgorithmException {
        // Initialize the public/private key
        RSA256Key rsa256Key = SecretKeyUtil.generateRSA256Key();

        // Use the private key to generate RS algorithm objects for encryption
        Algorithm algorithm = Algorithm.RSA256(rsa256Key.getPrivateKey());

        return JWT.create()
                / / issue
                .withIssuer(ISSUER)
                / / the recipient
                .withAudience(data.toString())
                // Issue time
                .withIssuedAt(new Date())
                // Expiration time
                .withExpiresAt(DateUtil.addHours(2))
                // Related information
                .withClaim("data", JsonUtil.toJsonString(data))
                / / checkin
                .sign(algorithm);
    }
Copy the code


  • There is a point worth teasing (auth0 source code analysis below, not interested can skip)


    /**
     * Creates a new Algorithm instance using SHA256withRSA. Tokens specify this as "RS256".
     *
     * @param key the key to use in the verify or signing instance.
     * @return a valid RSA256 Algorithm.
     * @throws IllegalArgumentException if the Key Provider is null.
     * @deprecated use {@link #RSA256(RSAPublicKey, RSAPrivateKey)} or {@link #RSA256(RSAKeyProvider)}
     */
    @Deprecated
    public static Algorithm RSA256(RSAKey key) throws IllegalArgumentException {
        RSAPublicKey publicKey = key instanceof RSAPublicKey ? (RSAPublicKey) key : null;
        RSAPrivateKey privateKey = key instanceof RSAPrivateKey ? (RSAPrivateKey) key : null;
        return RSA256(publicKey, privateKey);
    }
    
    /**
     * Creates a new Algorithm instance using SHA256withRSA. Tokens specify this as "RS256".
     *
     * @param publicKey  the key to use in the verify instance.
     * @param privateKey the key to use in the signing instance.
     * @return a valid RSA256 Algorithm.
     * @throws IllegalArgumentException if both provided Keys are null.
     */
    public static Algorithm RSA256(RSAPublicKey publicKey, RSAPrivateKey privateKey) throws IllegalArgumentException {
        return RSA256(RSAAlgorithm.providerForKeys(publicKey, privateKey));
    }
 

Copy the code

Because we use RSAPublicKey and RSAPrivateKey to store the key, and both types inherit from RSAKey, so we can directly call RSA256(RSAKey key), just pass in the private key, logic will automatically null the public key, call the second method in sequence.

However, the method is marked @deprecated, indicating that it has been officially Deprecated. We can only call the second method directly, so we need to specify our own null value, and some people who do not know RS256 algorithm, will pass in both public and private keys.

     /**
     * Creates a new Algorithm instance using SHA256withRSA. Tokens specify this as "RS256".
     *
     * @param keyProvider the provider of the Public Key and Private Key for the verify and signing instance.
     * @return a valid RSA256 Algorithm.
     * @throws IllegalArgumentException if the provided Key is null.
     */
    public static Algorithm RSA256(RSAKeyProvider keyProvider) throws IllegalArgumentException {
        return new RSAAlgorithm("RS256"."SHA256withRSA", keyProvider);
    }
Copy the code

RSAKeyProvider is generated by calling the above two methods, and the Algorithm object is generated by calling the method.

  • 3. Verify the Token

The verification is as simple as issuing, and the Algorithm is generated through PublicKey, because I put the encryption and decryption on the server, saving a lot of unnecessary trouble.

  public static boolean verifierToken(String token) throws NoSuchAlgorithmException {

        // Obtain the public/private key
        RSA256Key rsa256Key = SecretKeyUtil.generateRSA256Key();

        // Generate RS256 algorithm objects based on the key pair
        Algorithm algorithm = Algorithm.RSA256(rsa256Key.getPublicKey());

        System.out.println("PublicKey: " + rsa256Key.getPublicKey().getPublicExponent());

        // When decrypting, use the gong key to generate algorithm objects
        JWTVerifier verifier = JWT.require(algorithm)
                                    .withIssuer(ISSUER)
                                    .build();

        try {
            // Authenticates the Token. Verifier automatically authenticates the Token
            DecodedJWT jwt = verifier.verify(token);
            return true;
        }catch (JWTVerificationException e){
            log.error("Token cannot be verified! " + e.getMessage());
            return false;
        }
Copy the code

DecodedJWT can be generated from the JWTVerifier object. To obtain specific TOken information, DecodedJWT can be used.

If you are interested in the underlying implementation of Auth0, clone it from gitHub and run it for yourself. Learn about other people’s source code design patterns and logical processing.

Source: github.com/auth0/java-…

The code involved in this article is in my GitHub: Simon Alang



GitHub:github.com/zhaojie777