JWT explanation and practical application


1 Introduction

        This blog is based on the blog of a big boss. The purpose is to type it by hand so that I can remember it more deeply. At the same time, it is also just in case that big guy deletes the blog, and I won’t be able to read such a good article in the future. At the beginning, I didn’t notice JWT technology. Until the interview a few days ago, the interviewer asked this question, and I was confused on the spot, so I’ll make it up this time. Original article: JWT Detailed Explanation

2. What is JWT

        Before introducing JWT, let's review the process of using token for user authentication:

  • The client requests a login with a username and password.
  • After receiving the request, the server verifies the username and password.
  • After the verification is successful, the server will issue a token, and then return the token to the client.
  • After receiving the token, the client can store it, such as putting it in a cookie.
  • The client needs to carry this token every time it sends interest to the server, and it can be carried in a cookie or header.
  • After receiving the request, the server verifies the token carried in the client request, and returns the request data to the client if the verification is successful.

Compared with the traditional session authentication method, this token-based authentication method saves server resources and is more friendly to mobile terminals and distribution. Its advantages are as follows:

  • Support cross-domain access : cookies cannot cross domains, and tokens do not use cookies (provided that the token exists in the header), so there will be no information loss after crossing domains.
  • Stateless : the token mechanism does not need to store session information on the server side, because the token itself contains the information of all logged-in users, so it can reduce the pressure on the server side.
  • More applicable to CDN : All data on the server can be requested through the content distribution network.
  • More suitable for mobile terminals : when the client is a non-browser platform, cookies are not supported, and it will be much simpler to use token authentication at this time.
  • There is no need to consider CSRF : Since cookies are not considered, CSRF will not occur in the token authentication method, so there is no need to consider CSRF defense.

And JWT is a specific implementation of the token mentioned above, the full name is called JSON Web Token , the official website address: https://jwt.io/

In layman's terms, JWT is essentially a string, which saves user information in a JSON string, and then encodes a JWT Token, and this JWT Token has signature information, which can be verified after receiving It has been tampered with, so it can be used to securely transmit information between parties as JSON objects. The verification process of JWT is as follows:

  • The client sends its user name and password to the backend interface in the form of a form. This process is generally a POST request. The recommended way is to use SSL encrypted transmission to avoid tampering of sensitive information.
  • After the backend checks the user name and password, the data containing the user information is used as the JWT Payload, which is encoded and concatenated with Base64 in the JWT Header, and then signed to form a JWT Token (such as a string of 111.aaa.nnn).
  • The backend returns the JWT Token string to the client as a result of successful login. The client can save the returned result in the browser, and delete the saved JWT Token when logging out.
  • The client will put the JWT Token into the Authorization attribute in the HTTP request header every time it requests.
  • The server checks the JWT Token sent by the client and the validity of the verifier, such as checking whether the signature is correct, whether it has expired, whether the recipient of the token is itself, and so on.
  • After the verification is passed, the server parses out the user information contained in the JWT Token, performs other logical operations, and returns the result.

3. Why use JWT

        Before talking about why we use JWT, let's talk about the disadvantages of Session. We all know that HTTP itself is a stateless protocol, which means that if the user provides a user name and password to our application for user authentication, the HTTP protocol will not record the authenticated state after the authentication is passed, then the next request , the user needs to perform an authentication, because according to the HTTP protocol, we don't know which user sent the request, so in order for our application to identify which user sent the request, we can only log in to the server after the user successfully logs in for the first time. Store a user's login information, and this login information will be passed to the browser in response, telling it to be saved as a cookie, so that it can be sent to our user when the next request is made, so that our application can identify which user the request comes from This is the traditional session-based authentication process.
insert image description here
Then, the traditional session authentication will have the following problems:

  • The login information of each user will be saved in the session of the server. As the number of users increases, the overhead of the server will increase significantly.
  • Since the session exists in the physical memory of the server, this method will fail in a distributed system. Although the session consent can be saved in redis, this will undoubtedly increase the complexity of the system, and for applications that do not require redis, an extra cache middleware will be introduced in vain.
  • Because the session is dependent on the cookie, there are often no cookies for non-browser clients or mobile terminals, so the way of using the session will be invalid.
  • Because the essence of session authentication is based on cookies, if cookies are intercepted, users are vulnerable to cross-site request forgery attacks. And if the browser disables cookies, this method will also fail.
  • It is even more inapplicable in a system where the front-end and back-end are separated, the back-end deployment is complicated, the request sent by the client often passes through multiple middleware to reach the server, and the cookie information about the session will be forwarded multiple times.
  • Since it is based on cookies, and cookies cannot cross domains, session authentication cannot cross domains.

Speaking of sessions, I remembered that the interviewer asked me a question during the interview: If cookies are disabled, how can I find the session?
        Answer: There are two common solutions. The first is URL rewriting, which is to append the session id directly after the URL path. The first is the form hidden field, adding a hidden field to the form so that the session id can be passed to the server when the form is submitted. for example:

<form name="testform" action="/xxx"> 
     <input type="hidden" name="jsessionid" value="ByOK3vjFD75aPnrF7C2HmdnV6QZcEbzWoWiBYEnLerjQ99zWpBng!-145788764"/>
     <input type="text"> 
</form>

Advantages of JWTs:

  • Concise: JWT Token has a small amount of data and fast transmission speed.
  • Since JWT Token is stored on the client in JSON encrypted form, JWT is cross-language, and in principle any web form supports it.
  • There is no need for the server to save session information, that is to say, it does not depend on cookies and sessions, so it does not have the disadvantages of traditional session authentication, and is especially suitable for distributed microservices.
  • Single sign-on friendly. If session is used for identity authentication, it is difficult to achieve single sign-on because cookies cannot be cross-domain. However, if the token is used for verification, the token can be stored in the memory of any location on the client, not necessarily a cookie, so these problems will not occur without relying on cookies.
  • Applicable to the mobile terminal: If the session is used for authentication, a piece of information needs to be saved on the server, and this method will rely on cookies, so it is not applicable to the mobile terminal.

4. The structure of JWT

        JWT consists of three parts: header (header), payload (payload) and signature (Signature). During transmission, the three parts of the JWT will be Base64-encoded and then concatenated with . to form the final transmitted string.
insert image description here
4.1 Header:
        The JWT header is a JSON object describing the JWT metadata. The alt attribute indicates the algorithm applicable to the signature, and the default is HMAC SHA256 (ie HS256); the typ attribute indicates the type of the token, and the JWT token is uniformly written as JWT. Finally, apply the Base64 URL algorithm to convert the above JSON object into a string and save it.

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

4.2 Payload
        The payload part is the subject part of the JWT, and it is also a JSON object that contains the data that needs to be passed. JWT specifies default fields for selection.

iss:发行人
exp:到期时间
sub:主题
aud:用户
nbf:在此之前不可用
iat:发布时间
jti:JWT ID用于标识该JWT

In addition to the above default fields, we can also customize private fields. Generally, put the user information data in the payload, as follows

{
    
    
  "sub": "1234567890",
  "name": "Helen",
  "admin": true
}

Note: By default, JWT is unencrypted, but uses the base64 algorithm. After getting the JWT string, it can be converted back to the original JSON data. Anyone can interpret its content, so do not build a private information field. For example, the user's password must be Cannot save to JWT.
4.3 Signature
        The signature hash part is to sign the above two parts of the data. It is also necessary to use the base64-encoded header and payload data to generate a hash through the specified algorithm to ensure that the data will not be tampered with. First you need to specify a key. This key is only kept on the server and cannot be disclosed to users. Then use the signature algorithm specified in the header to generate a signature. After the signature hash is calculated, the three parts of the JWT header, payload, and signature hash form a string, and each part is separated by . to form the entire JWT object.

Pay attention to the role of each part of JWT, after the server receives the JWT Token sent by the client:

  • The header and payload can directly use base64 to parse out the original text, obtain the hash signature algorithm from the header, and obtain valid data from the payload.
  • Because the signature uses an irreversible encryption algorithm, the original text cannot be parsed. Its function is to verify whether the token has been tampered with. After the server obtains the encryption algorithm in the header, it uses the algorithm plus the secretKey to encrypt the header and payload, and compares whether the encrypted data is consistent with the one sent by the client. Note that the secretKey can only be saved in the server, and its meaning is different for different encryption algorithms. Generally, for the MD5 digest encryption algorithm, the secretKey actually represents the salt value.

5. Types of JWT

        In fact, JWT (JSON Web Token) refers to a specification that allows us to use JWT to transfer safe and reliable information between two organizations. The specific implementation of JWT can be divided into the following types:

  • nonsecure JWT: unsigned, insecure JWT
  • JWS: signed JWT
  • JWE: The encrypted JWT of the payload part

5.1 nonsecure JWT
Signed JWT is not used, that is, unsafe JWT. The header part does not specify the signature algorithm, and there is no Signature part.
5.2 JWS
        JWS, that is, JWT Signature, its structure is based on the previous nonsecure JWT, the signature algorithm is declared in the header, and the signature is added at the end. Creating a signature is to ensure that jwt cannot be tampered with by others. The JWT we usually use is generally JWS. In order to complete the signature, in addition to the header information and payload information, the key of the algorithm is also required, which is the secretKey. There are generally two types of encryption algorithms:

  • Symmetric encryption: secretKey refers to the encryption key, which can generate signatures and verify signatures
  • Asymmetric encryption: secretKey refers to the private key, which is only used to generate a signature and cannot be used to verify the signature (the public key is used for signature verification)

The key or key pair of JWT is generally called JSON Web Key, or JWK. So far, there are three signature algorithms for jwt:

  • HMAC [Hash Message Authentication Code (Symmetric)]: HS256/HS384/HS512
  • RSASSA [RSA signature algorithm (asymmetric)] (RS256/RS384/RS512)
  • ECDSA [Elliptic Curve Data Signature Algorithm (Asymmetric)] (ES256/ES384/ES512)

6. Application in actual development

6.1 java-jwt
introduces dependencies

<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.10.3</version>
</dependency>

generate token

public class JWTTest {
    
    
    @Test
    public void testGenerateToken(){
    
    
        // 指定token过期时间为10秒
        Calendar calendar = Calendar.getInstance();
        calendar.add(Calendar.SECOND, 10);

        String token = JWT.create()
                .withHeader(new HashMap<>())  // Header
                .withClaim("userId", 21)  // Payload
                .withClaim("userName", "baobao")
                .withExpiresAt(calendar.getTime())  // 过期时间
                .sign(Algorithm.HMAC256("!34ADAS"));  // 签名用的secret

        System.out.println(token);
    }
}

Parse the JWT string

public class JWTTest {
    
    
    @Test
    public void testGenerateToken(){
    
    
        // 指定token过期时间为10秒
        Calendar calendar = Calendar.getInstance();
        calendar.add(Calendar.SECOND, 10);

        String token = JWT.create()
                .withHeader(new HashMap<>())  // Header
                .withClaim("userId", 21)  // Payload
                .withClaim("userName", "baobao")
                .withExpiresAt(calendar.getTime())  // 过期时间
                .sign(Algorithm.HMAC256("!34ADAS"));  // 签名用的secret

        System.out.println(token);
    }
}

6.2 jjwt-root
introduces dependencies

<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>
public class JwtUtils {
    
    
    // token时效:24小时
    public static final long EXPIRE = 1000 * 60 * 60 * 24;
    // 签名哈希的密钥,对于不同的加密算法来说含义不同
    public static final String APP_SECRET = "ukc8BDbRigUDaY6pZFfWus2jZWLPHO";

    /**
     * 根据用户id和昵称生成token
     * @param id  用户id
     * @param nickname 用户昵称
     * @return JWT规则生成的token
     */
    public static String getJwtToken(String id, String nickname){
    
    
        String JwtToken = Jwts.builder()
                .setHeaderParam("typ", "JWT")
                .setHeaderParam("alg", "HS256")
                .setSubject("baobao-user")
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRE))
                .claim("id", id)
                .claim("nickname", nickname)
            	// HS256算法实际上就是MD5加盐值,此时APP_SECRET就代表盐值
                .signWith(SignatureAlgorithm.HS256, APP_SECRET)
                .compact();

        return JwtToken;
    }

    /**
     * 判断token是否存在与有效
     * @param jwtToken token字符串
     * @return 如果token有效返回true,否则返回false
     */
    public static boolean checkToken(String jwtToken) {
    
    
        if(StringUtils.isEmpty(jwtToken)) return false;
        try {
    
    
            Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
        } catch (Exception e) {
    
    
            e.printStackTrace();
            return false;
        }
        return true;
    }

    /**
     * 判断token是否存在与有效
     * @param request Http请求对象
     * @return 如果token有效返回true,否则返回false
     */
    public static boolean checkToken(HttpServletRequest request) {
    
    
        try {
    
    
            // 从http请求头中获取token字符串
            String jwtToken = request.getHeader("token");
            if(StringUtils.isEmpty(jwtToken)) return false;
            Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
        } catch (Exception e) {
    
    
            e.printStackTrace();
            return false;
        }
        return true;
    }

    /**
     * 根据token获取会员id
     * @param request Http请求对象
     * @return 解析token后获得的用户id
     */
    public static String getMemberIdByJwtToken(HttpServletRequest request) {
    
    
        String jwtToken = request.getHeader("token");
        if(StringUtils.isEmpty(jwtToken)) return "";
        Jws<Claims> claimsJws = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
        Claims claims = claimsJws.getBody();
        return (String)claims.get("id");
    }
}

jjwt has undergone major changes after version 0.10, dependencies need to introduce multiple

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.2</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.11.2</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred -->
    <version>0.11.2</version>
    <scope>runtime</scope>
</dependency>

After jjwt0.10 version, the length of secretKey has the following mandatory requirements:

  • HS256: requires at least 256 bits (32 bytes)
  • HS384: requires at least 384 bits (48 bytes)
  • HS512: requires at least 512 bits (64 bytes)
  • RS256 and PS256: at least 2048 bits
  • RS384 and PS384: at least 3072 bits
  • RS512 and PS512: at least 4096 bits
  • ES256: at least 256 bits (32 bytes)
  • ES384: at least 384 bits (48 bytes)
  • ES512: at least 512 bits (64 bytes)

In the new version of jjwt, the previous signature and verification methods are all strings of the incoming key, which is outdated. The newest method requires a Key object to be passed in.

public class JwtUtils {
    
    
    // token时效:24小时
    public static final long EXPIRE = 1000 * 60 * 60 * 24;
    // 签名哈希的密钥,对于不同的加密算法来说含义不同
    public static final String APP_SECRET = "ukc8BDbRigUDaY6pZFfWus2jZWLPHOsdadasdasfdssfeweee";

    /**
     * 根据用户id和昵称生成token
     * @param id  用户id
     * @param nickname 用户昵称
     * @return JWT规则生成的token
     */
    public static String getJwtToken(String id, String nickname){
    
    
        String JwtToken = Jwts.builder()
                .setSubject("baobao-user")
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRE))
                .claim("id", id)
                .claim("nickname", nickname)
                // 传入Key对象
                .signWith(Keys.hmacShaKeyFor(APP_SECRET.getBytes(StandardCharsets.UTF_8)), SignatureAlgorithm.HS256)
                .compact();
        return JwtToken;
    }

    /**
     * 判断token是否存在与有效
     * @param jwtToken token字符串
     * @return 如果token有效返回true,否则返回false
     */
    public static Jws<Claims> decode(String jwtToken) {
    
    
        // 传入Key对象
        Jws<Claims> claimsJws = Jwts.parserBuilder().setSigningKey(Keys.hmacShaKeyFor(APP_SECRET.getBytes(StandardCharsets.UTF_8))).build().parseClaimsJws(jwtToken);
        return claimsJws;
    }
}

In the actual SpringBoot project, generally we can use the following process to log in:

  • After the login verification is passed, a corresponding random token is generated for the user (note that this token does not refer to jwt, it can be generated using algorithms such as uuid), and then this token is used as a part of the key, the user information is stored in Redis as value, and the expiration is set Time, this expiration time is the time when the login becomes invalid
  • Use the random token generated in step 1 as the JWT payload to generate a JWT string and return it to the front end
  • Each request after the front end carries the JWT string in the Authorization field in the request header
  • The backend defines an interceptor. Every time a frontend request is received, the JWT string is taken from the Authorization field in the request header and verified. After the verification is passed, the random token in the payload is parsed out, and then the random token is used Get the key, get the user information from Redis, if you can get it, it means the user has logged in
public class JWTInterceptor implements HandlerInterceptor {
    
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    
    
        String JWT = request.getHeader("Authorization");
        try {
    
    
            // 1.校验JWT字符串
            DecodedJWT decodedJWT = JWTUtils.decode(JWT);
            // 2.取出JWT字符串载荷中的随机token,从Redis中获取用户信息
            ...
            return true;
        }catch (SignatureVerificationException e){
    
    
            System.out.println("无效签名");
            e.printStackTrace();
        }catch (TokenExpiredException e){
    
    
            System.out.println("token已经过期");
            e.printStackTrace();
        }catch (AlgorithmMismatchException e){
    
    
            System.out.println("算法不一致");
            e.printStackTrace();
        }catch (Exception e){
    
    
            System.out.println("token无效");
            e.printStackTrace();
        }
        return false;
    }
}

Guess you like

Origin blog.csdn.net/m0_73845616/article/details/127603451