Spring boot integrates JWT to realize user login authentication

Time:2020-11-18

JWT introduction

What is JWT

JWT is the abbreviation of JSON web token, which is based on theJSON(RFC 7519). A concise, self-contained method is defined for communication between two partiesJSONThe formal security of the object. Because of the existence of a digital signature, this information is trusted and can be used by JWTHMACAlgorithm orRSAThe public and private secret key pair of.

JWT request process

Spring boot integrates JWT to realize user login authentication

  1. Users use account number and password to initiate post request;
  2. The server uses the private key to create a JWT;
  3. The server returns the JWT to the browser;
  4. The browser sends the JWT string in the request header like the server;
  5. The server verifies the JWT;
  6. Returns the response resource to the browser.

Main application scenarios of JWT

Authentication in this scenario, once the user has completed the login, JWT is included in each subsequent request, which can be used to verify the user’s identity and to verify the access rights of routes, services and resources. Because of its low cost, it can be easily transmitted in different domain name systems, so it is widely used in single sign on (SSO). Information exchange between the two sides of communication using JWT to encode the data is a very safe way, because its information is signed, it can ensure that the information sent by the sender is not forged.

JWT data structure

JWT is composed of three pieces of information.Together, it forms the JWT string.

And: the header of jwtheader: signature.

Spring boot integrates JWT to realize user login authentication

Header

The header part is a JSON object, which describes the metadata of JWT. It usually looks like the following.

{
  "alg": "HS256",
  "typ": "JWT"
}

In the code above,algAttribute represents the algorithm of the signature. The default is HMAC sha256 (written as hs256);typAttribute indicates the type of the token. JWT token is written asJWT

Finally, convert the above JSON object into a string using the base64 URL algorithm.

Payload

The payload part is also a JSON object, which is used to store the valid information that needs to be passed. Valid information consists of three parts:

  1. Declaration registered in the standard
  2. Public statement
  3. Private statement

Statement registered in the standard (recommended but not mandatory)

  • ISS (issuer): issuer
  • Exp (expiration time): the expiration time must be greater than the issuing time
  • Subject: subject
  • Aud (audience): audience
  • NBF (not before): effective time
  • IAT (issued at): issuing time
  • JTI (JWT ID): number, unique identification of JWT, mainly used as one-timetokenTo avoid replay attacks.

Public statement:

Any information can be added to the public statement. Generally, the relevant information of the user or other necessary information required by the business can be added. However, it is not recommended to add sensitive information as this part can be decrypted on the client side.

Private statement:

Private statements are defined by both providers and consumers. It is generally not recommended to store sensitive information becausebase64It is symmetric decoding, which means that this part of information can be classified as plaintext information.

The JSON object is also converted to a string using the base64 URL algorithm.

Signature

The signature part is the signature of the first two parts to prevent data tampering.

First, you need to specify a secret. This key is only known to the server and cannot be disclosed to users. Then, the signature algorithm specified in the header (HMAC sha256 by default) is used to generate the signature according to the following formula.

HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)

After the signature is calculated, the header, payload and signature are combined into a string, and the “dot” is used between each part(.)Separated, it can be returned to the user.

Base64URL

As mentioned earlier, the algorithm for header and payload serialization is base64url. This algorithm is basically similar to Base64 algorithm, but with some small differences.

As a token, JWT may be put into the URL in some cases (for exampleapi.example.com/?token=xxx)。 Base64 has three characters+/and=, which has a special meaning in the URL, so it should be replaced:=Omitted+replace with-/replace with_。 This is the base64 URL algorithm.

How to use JWT

After receiving the JWT returned by the server, the client needs to save it locally. After that, every time the client communicates with the server, it will bring this JWT. The general practice is to put the header information of the HTTP requestAuthorizationField.

Authorization: Bearer <token>

In this way, in each request, the server can get JWT in the request header for parsing and authentication.

Features of JWT

  1. JWT is not encrypted by default, but can also be encrypted. After the original token is generated, it can be encrypted again with the key.
  2. When JWT is not encrypted, secret data cannot be written to JWT.
  3. JWT can be used not only for authentication, but also for authentication. Effective use of JWT can reduce the number of times the server queries the database.
  4. The biggest drawback of JWT is that because the server does not save the session state, it is unable to revoke a token or change the permissions of a token during use. That is, once the JWT is issued, it will remain valid until it expires, unless the server deploys additional logic.
  5. JWT itself contains authentication information, once leaked, anyone can get all rights of the token. In order to reduce embezzlement, the validity period of JWT should be set short. For some important permissions, users should be authenticated again.
  6. In order to reduce the embezzlement, JWT should not use the HTTP protocol, but use the HTTPS protocol.

Simple encapsulation based on nimbus Jose JWT

Nimbus Jose JWT is the most popular JWT open source library. Based on Apache 2.0 open source protocol, it supports all standard JWs and JWe algorithms. Nimbus Jose JWT supports the use of symmetric encryption (HMAC) and asymmetric encryption (RSA) algorithms to generate and parse JWT tokens.

Next, we encapsulate nimbus Jose JWT to support the following functions:

  1. Support the use of HMAC and RSA algorithms to generate and parse JWT tokens
  2. It supports private information as payload directly, and standard information + private information as payload. Built in support of the latter.
  3. It provides tool class and extensible interface for user-defined extension development.

Add dependency in POM

First of all, we are in pom.xml The nimbus Jose JWT dependency is introduced.

<dependency>
  <groupId>com.nimbusds</groupId>
  <artifactId>nimbus-jose-jwt</artifactId>
  <version>8.20</version>
</dependency>

JwtConfig

This class is used for unified management of related parameter configurations.

public class JwtConfig {

    //JWT default key in HTTP header
    private String tokenName = JwtUtils.DEFAULT_TOKEN_NAME;

    //HMAC key, used to support HMAC algorithm
    private String hmacKey;

    //JKS key path, used to support RSA algorithm
    private String jksFileName;

    //JKS key cipher is used to support RSA algorithm
    private String jksPassword;

    //Certificate password to support RSA algorithm
    private String certPassword;

    //JWT standard information: issuer - ISS
    private String issuer;

    //JWT standard information: subject - sub
    private String subject;

    //JWT standard information: audience aud
    private String audience;

    //JWT standard information: effective time - NBF, how long will it take effect in the future
    private long notBeforeIn;
    
    //JWT standard information: effective time - NBF, which time is effective
    private long notBeforeAt;

    //JWT standard information: expiration time - exp, how long will it expire in the future
    private long expiredIn;

    //JWT standard information: expiration time - exp, which time does it expire
    private long expiredAt;
}

hmacKeyField is used to support HMAC algorithm. As long as the field is not empty, this value is used as HMAC’s key to sign and verify JWT.

jksFileNamejksPasswordcertPasswordThree fields are used to support RSA algorithm. The program will read the certificate file as RSA key to sign and verify JWT.

Several other fields are used to set the standard information that needs to be carried in the payload.

JwtService

Jwtservice is an interface to provide JWT signature and verification. The built-in hmacjwtserviceimpl provides the implementation of HMAC algorithm and rsajwtserviceimpl provides the implementation of RSA algorithm. The two algorithms are different in the way of obtaining the key, which is also proposed as the interface method. Later, if you want to customize the implementation, you only need to write a specific implementation class.

public interface JwtService {

    /**
     *Get key
     *
     * @return
     */
    Object genKey();

    /**
     *Sign the message
     *
     * @param payload
     * @return
     */
    String sign(String payload);

    /**
     *Verify and return information
     *
     * @param token
     * @return
     */
    String verify(String token);
}
public class HMACJwtServiceImpl implements JwtService {

    private JwtConfig jwtConfig;

    public HMACJwtServiceImpl(JwtConfig jwtConfig) {
        this.jwtConfig = jwtConfig;
    }

    @Override
    public String genKey() {
        String key = jwtConfig.getHmacKey();
        if (JwtUtils.isEmpty(key)) {
            throw new KeyGenerateException(JwtUtils.KEY_GEN_ERROR, new NullPointerException("HMAC need a key"));
        }
        return key;
    }

    @Override
    public String sign(String info) {
        return JwtUtils.signClaimByHMAC(info, genKey(), jwtConfig);
    }

    @Override
    public String verify(String token) {
        return JwtUtils.verifyClaimByHMAC(token, genKey(), jwtConfig);
    }
}
public class RSAJwtServiceImpl implements JwtService {

    private JwtConfig jwtConfig;

    private RSAKey rsaKey;

    public RSAJwtServiceImpl(JwtConfig jwtConfig) {
        this.jwtConfig = jwtConfig;
    }

    private InputStream getCertInputStream() throws IOException {
        //Read the certificate path in the configuration file
        String jksFile = jwtConfig.getJksFileName();
        if (jksFile.contains("://")) {
            //Read from local file
            return new FileInputStream(new File(jksFile));
        } else {
            //Read from classpath
            return getClass().getClassLoader().getResourceAsStream(jwtConfig.getJksFileName());
        }
    }

    @Override
    public RSAKey genKey() {
        if (rsaKey != null) {
            return rsaKey;
        }
        InputStream is = null;
        try {
            KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
            is = getCertInputStream();
            keyStore.load(is, jwtConfig.getJksPassword().toCharArray());
            Enumeration<String> aliases = keyStore.aliases();
            String alias = null;
            while (aliases.hasMoreElements()) {
                alias = aliases.nextElement();
            }
            RSAPrivateKey privateKey = (RSAPrivateKey) keyStore.getKey(alias, jwtConfig.getCertPassword().toCharArray());
            Certificate certificate = keyStore.getCertificate(alias);
            RSAPublicKey publicKey = (RSAPublicKey) certificate.getPublicKey();
            rsaKey = new RSAKey.Builder(publicKey).privateKey(privateKey).build();
            return rsaKey;
        } catch (IOException | CertificateException | UnrecoverableKeyException
                | NoSuchAlgorithmException | KeyStoreException e) {
            e.printStackTrace();
            throw new KeyGenerateException(JwtUtils.KEY_GEN_ERROR, e);
        } finally {
            if (is != null) {
                try {
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    @Override
    public String sign(String payload) {
        return JwtUtils.signClaimByRSA(payload, genKey(), jwtConfig);
    }

    @Override
    public String verify(String token) {
        return JwtUtils.verifyClaimByRSA(token, genKey(), jwtConfig);
    }
}

JwtUtils

The implementation class of jwtservice is relatively concise, because the main methods are provided in jwtutils. The following is the signature and verification implementation of the two algorithms when payload contains only private information. These methods can be used to implement their own extensions.

/**
     *Use HMAC algorithm to sign information (payload contains only private information)
     *
     * @param info
     * @param key
     * @return
     */
    public static String signDirectByHMAC(String info, String key) {
        try {
            JWSHeader jwsHeader = new JWSHeader.Builder(JWSAlgorithm.HS256)
                    .type(JOSEObjectType.JWT)
                    .build();

            //Build a payload
            Payload payload = new Payload(info);

            //Combine the head with the load
            JWSObject jwsObject = new JWSObject(jwsHeader, payload);

            //Create a key
            JWSSigner jwsSigner = new MACSigner(key);

            //Signature
            jwsObject.sign(jwsSigner);

            //Generating token
            return jwsObject.serialize();
        } catch (JOSEException e) {
            e.printStackTrace();
            throw new PayloadSignException(JwtUtils.PAYLOAD_SIGN_ERROR, e);
        }
    }

    /**
     *Signature information using RSA algorithm (payload contains only private information)
     *
     * @param info
     * @param rsaKey
     * @return
     */
    public static String signDirectByRSA(String info, RSAKey rsaKey) {
        try {
            JWSSigner signer = new RSASSASigner(rsaKey);
            JWSObject jwsObject = new JWSObject(
                    new JWSHeader.Builder(JWSAlgorithm.RS256).keyID(rsaKey.getKeyID()).build(),
                    new Payload(info)
            );
            //Encrypt
            jwsObject.sign(signer);

            return jwsObject.serialize();
        } catch (JOSEException e) {
            e.printStackTrace();
            throw new PayloadSignException(JwtUtils.PAYLOAD_SIGN_ERROR, e);
        }
    }

    /**
     *Using HMAC algorithm to verify token (payload contains only private information)
     *
     * @param token
     * @param key
     * @return
     */
    public static String verifyDirectByHMAC(String token, String key) {
        try {
            JWSObject jwsObject = JWSObject.parse(token);
            //Create an unlock key
            JWSVerifier jwsVerifier = new MACVerifier(key);
            if (jwsObject.verify(jwsVerifier)) {
                return jwsObject.getPayload().toString();
            }
            throw new TokenVerifyException(JwtUtils.TOKEN_VERIFY_ERROR, new NullPointerException("Payload can not be null"));
        } catch (JOSEException | ParseException e) {
            e.printStackTrace();
            throw new TokenVerifyException(JwtUtils.TOKEN_VERIFY_ERROR, e);
        }
    }

    /**
     *Using RSA algorithm to verify token (payload contains only private information)
     *
     * @param token
     * @param rsaKey
     * @return
     */
    public static String verifyDirectByRSA(String token, RSAKey rsaKey) {
        try {
            RSAKey publicRSAKey = rsaKey.toPublicJWK();
            JWSObject jwsObject = JWSObject.parse(token);
            JWSVerifier jwsVerifier = new RSASSAVerifier(publicRSAKey);
            //Validation data
            if (jwsObject.verify(jwsVerifier)) {
                return jwsObject.getPayload().toString();
            }
            throw new TokenVerifyException(JwtUtils.TOKEN_VERIFY_ERROR, new NullPointerException("Payload can not be null"));
        } catch (JOSEException | ParseException e) {
            e.printStackTrace();
            throw new TokenVerifyException(JwtUtils.TOKEN_VERIFY_ERROR, e);
        }
    }

JwtException

By defining a unified exception class, we can shield nimbus Jose JWT and other exceptions such as loading certificate error. When other projects integrate our packaged library, we can easily handle the exception.

In different stages of jwtservice implementation, we encapsulate different jwtexception subclasses to facilitate external processing according to needs. If the exception is keygenerateexception, it will be handled as a server processing error; if the exception is tokenverifyexception, it will be handled as token validation failure without permission.

JwtContext

JWT is used for user authentication. After token verification is completed, the program needs to obtain the current login user information. Jwtcontext provides a method to save information through thread local variables.

public class JwtContext {

    private static final String KEY_TOKEN = "token";
    private static final String KEY_PAYLOAD = "payload";

    private static ThreadLocal<Map<Object, Object>> context = new ThreadLocal<>();

    private JwtContext() {}

    public static void set(Object key, Object value) {
        Map<Object, Object> locals = context.get();
        if (locals == null) {
            locals = new HashMap<>();
            context.set(locals);
        }
        locals.put(key, value);
    }

    public static Object get(Object key) {
        Map<Object, Object> locals = context.get();
        if (locals != null) {
            return locals.get(key);
        }
        return null;
    }

    public static void remove(Object key) {
        Map<Object, Object> locals = context.get();
        if (locals != null) {
            locals.remove(key);
            if (locals.isEmpty()) {
                context.remove();
            }
        }
    }

    public static void removeAll() {
        Map<Object, Object> locals = context.get();
        if (locals != null) {
            locals.clear();
        }
        context.remove();
    }

    public static void setToken(String token) {
        set(KEY_TOKEN, token);
    }

    public static String getToken() {
        return (String) get(KEY_TOKEN);
    }

    public static void setPayload(Object payload) {
        set(KEY_PAYLOAD, payload);
    }

    public static Object getPayload() {
        return get(KEY_PAYLOAD);
    }
}

@AuthRequired

In project practice, not all methods in controller must pass token, and @ authrequired annotation is used to distinguish whether methods need to verify token.

/**
 *The method applied to the controller to identify whether to intercept for JWT verification
 */
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface AuthRequired {

    boolean required() default true;
}

Spring boot integrated JWT instance

With the above packaged library, we integrate JWT into the springboot project. After creating the spring boot project, we write the following main classes.

JwtDemoInterceptor

In the spring boot project, requests and responses can be intercepted by customizing the implementation class of handlerinterceptor. We create a new jwtdemointerceptor class to intercept.

public class JwtDemoInterceptor implements HandlerInterceptor {

    private static final Logger logger = LoggerFactory.getLogger(JwtDemoInterceptor.class);

    private static final String PREFIX_BEARER = "Bearer ";

    @Autowired
    private JwtConfig jwtConfig;

    @Autowired
    private JwtService jwtService;

    /**
     *Preprocessing callback method to realize the preprocessing of the processor (such as checking login). The third parameter is the responding processor, and the controller is defined
     *Return value:
     *True means to continue the process (such as calling the next interceptor or or processor);
     *False means that the process is interrupted (such as login check failure), and other interceptors or processors will not be called. In this case, we need to generate a response through response.
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //If it is not mapped to a method, it passes through the
        if(!(handler instanceof HandlerMethod)){
            return true;
        }

        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();
        //Check whether there is @ authrequired annotation. If yes and required() is false, skip it
        if (method.isAnnotationPresent(AuthRequired.class)) {
            AuthRequired authRequired = method.getAnnotation(AuthRequired.class);
            if (!authRequired.required()) {
                return true;
            }
        }

        String token = request.getHeader(jwtConfig.getTokenName());

        logger.info("token: {}", token);

        if (StringUtils.isEmpty(token) || token.trim().equals(PREFIX_BEARER.trim())) {
            return true;
        }

        token = token.replace(PREFIX_BEARER, "");

        String payload = jwtService.verify(token);

        //Local token in thread
        JwtContext.setToken(token);
        JwtContext.setPayload(payload);
        return true;
    }

    /**
     *The postprocessing callback method implements the post-processing of the processor (but before rendering the view). At this time, we can process the model data or view through modelandview (model and view object), and modelandview may be null.
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    /**
     *When the call back is finished, we can also monitor the output time of some call backs, such as when the call back is finished
     *However, only the aftercompletion of the interceptor whose prehandle returns true in the processor execution chain is called.
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        JwtContext.removeAll();
    }
}

preHandlepostHandleafterCompletionThe specific functions of the three methods can be seen in the comments on the code.

preHandleThe logic in this code is as follows:

  1. Intercept methods annotated by @ authrequired, as long as they are notrequired = falseThe token will be checked.
  2. The token is parsed from the request and verified. If an exception is validated, an exception is thrown in the method.
  3. If token verification is passed, relevant information will be set in thread local variables for subsequent programs to obtain processing.

afterCompletionThis code cleans up the thread variables.

InterceptorConfig

Define interceptorconfig. Through the @ configuration annotation, spring will load the class and complete the assembly.

addInterceptorsMethod, and intercept all requests.

jwtDemoConfigMethod is injected with jwtconfig and hmackey is set.

jwtDemoServiceMethod will generate a specific jwtservice according to the injected jwtconfig configuration. Here is hmacjwtserviceimpl.

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(jwtDemoInterceptor()).addPathPatterns("/**");
    }

    @Bean
    public JwtDemoInterceptor jwtDemoInterceptor() {
        return new JwtDemoInterceptor();
    }

    @Bean
    public JwtConfig jwtDemoConfig() {
        JwtConfig jwtConfig = new JwtConfig();
        jwtConfig.setHmacKey("cb9915297c8b43e820afd2a90a1e36cb");

        return jwtConfig;
    }

    @Bean
    public JwtService jwtDemoService() {
        return JwtUtils.obtainJwtService(jwtDemoConfig());
    }

}

Write test controller

@RestController
public class UserController {

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private JwtService jwtService;

    @GetMapping("/sign")
    @AuthRequired(required = false)
    public String sign() throws JsonProcessingException {

        UserDTO userDTO = new UserDTO();
        userDTO.setName("fatfoo");
        userDTO.setPassword("112233");
        userDTO.setSex(0);

        String payload = objectMapper.writeValueAsString(userDTO);

        return jwtService.sign(payload);
    }

    @GetMapping("/verify")
    public UserDTO verify() throws IOException {
        String payload = (String) JwtContext.getPayload();
        return objectMapper.readValue(payload, UserDTO.class);
    }
}

signMethod to sign the user information and return token@AuthRequired(required = false)The interceptor will not intercept it.

verifyMethod after the token passes the validation, the parsed information is obtained and returned.

Test with postman

Access the sign interface and return the signature token.

Spring boot integrates JWT to realize user login authentication

Add token information to header, request verify interface, and return user information.

Spring boot integrates JWT to realize user login authentication

Test RSA algorithm implementation

Above, we only set the hmackey parameter of jwtconfig, using HMAC algorithm for signature and verification. In this section, we demonstrate the implementation of RSA algorithm for signature and verification.

Generate signature file

Using the keytool tool of Java can easily generate certificate file.

➜  resources git:(master) ✗ keytool -genkey -alias jwt -keyalg RSA -keystore jwt.jks
Enter keystore password:
Keystore password too short - must be at least 6 characters
Enter keystore password: ronjwt
Enter the new password again: ronjwt
What's your first and last name?
  [Unknown]:  ron
What is the name of your organizational unit?
  [Unknown]:  ron
What is the name of your organization?
  [Unknown]:  ron
What is the name of your city or area?
  [Unknown]:  Xiamen
What is the name of your province?
  [Unknown]:  Fujian
What is the two letter country code for the unit?
  [Unknown]:  CN
Is CN = Ron, Ou = Ron, o = Ron, l = Xiaomen, St = Fujian, C = CN correct?
  [no]: Yes

Enter the key password of < JWT >
    (if the password is the same as that of the keystore, press enter)

Warning:
The JKS keystore uses a private format. "Keytool - importkeystore - srckeystore" is recommended jwt.jks  -destkeystore  jwt.jks  -Deststoretype pkcs12 "to the industry standard format pkcs12.

After the file is generated, copy it to the resource directory of the project.

Set jwtconfig parameter

Modify thejwtDemoConfigMethod, which are jksfilename, jkspassword and certpassword.

@Bean
public JwtConfig jwtDemoConfig() {
    JwtConfig jwtConfig = new JwtConfig();
//        jwtConfig.setHmacKey("cb9915297c8b43e820afd2a90a1e36cb");

    jwtConfig.setJksFileName("jwt.jks");
    jwtConfig.setJksPassword("ronjwt");
    jwtConfig.setCertPassword("ronjwt");
    return jwtConfig;
}

Do not set the hmackey parameter, otherwise hmacjwtserviceimpl will be loaded. becauseJwtUtils#obtainJwtServiceThe method is implemented as follows:

/**
 *Gets the factory method of the built-in jwtservice.
 *
 *HMAC algorithm is preferred
 *
 * @param jwtConfig
 * @return
 */
public static JwtService obtainJwtService(JwtConfig jwtConfig) {
    if (!JwtUtils.isEmpty(jwtConfig.getHmacKey())) {
        return new HMACJwtServiceImpl(jwtConfig);
    }

    return new RSAJwtServiceImpl(jwtConfig);
}

In this way, we can test the signature and verification of RSA algorithm. Run the program and use the postman test to see the difference.

– End –


This article is only the first part of integrating JWT with spring boot. In the future, we will continue to encapsulate this library, build spring boot starter, and customize @ enable annotation to facilitate the introduction in the project.

Please pay attention to my official account: Java (ID:craft4j)The first time to obtain knowledge dynamics.

If you are interested in the full source code of the project, you can reply in the official account.jwtTo get.