A guide to avoiding pitfalls to make you proficient in JWT~

Video tutorial portal: Two-hour minimalist introduction to JWT: JWT practical application and anti-pit guide~_哔哩哔哩_bilibili Two-hour minimalist introduction to JWT: JWT practical application and anti-pit guide~ A total of 12 videos, including: 01 .Course introduction and pre-knowledge points, 02.JWT concept, 03.JWT composition, etc., more exciting videos of UP master, please pay attention to UP account. https://www.bilibili.com/video/BV1gk4y177DS/?vd_source=aced8a6167c51fbe68b3d65b157734eb

1. Pre-knowledge points

The explanation of JWT in this course involves the following related knowledge points, which need to be prepared in advance:

  • Java Web

  • Spring/SpringMVC/SpringBoot

  • Spring Security

2. Introduction to JWT

2.1 Concept

Official website: JSON Web Tokens - jwt.io

Tidy up:

JSON Web Token, JWT for short, pronounced [dʒɒt] (pronunciation of jot), is an open data standard based on RFC 7519, which defines a loose and compact data combination method. Its function is: JWT is an encrypted data carrier, which can be used for data transmission between applications .

JWT generally covers user identity information, and the server can verify the validity of the information each time it is accessed.

2.2 JWT Composition

A JWT usually consists of three parts: HEADER (header), PAYLOAD (payload) and SIGNATURE (signature). The "." links are used between the three, and the format is as follows:

header.payload.signature

 

A simple JWT case:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9   //header
.eyJ1c2VyX2luZm8iOlt7ImlkIjoiMSJ9LHsibmFtZSI6ImRhZmVpIn0seyJhZ2UiOiIxOCJ9XSwiaWF0IjoxNjgxNTcxMjU3LCJleHAiOjE2ODI3ODM5OTksImF1ZCI6InhpYW9mZWkiLCJpc3MiOiJkYWZlaSIsInN1YiI6ImFsbHVzZXIifQ  //payload
.v1TxJ0mngnVx4t9O3uibAHPSLUyMM7sUM06w8ODYjuE //signature

 Note that there is a dot (“.”) between the three

2.2.1 Header Composition

The JWT header carries two pieces of information:

  • Claim type, the default is JWT

  • Commonly used algorithms for declaring encryption algorithms: HMAC, RSA, ECDSA, etc.

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

alg : Indicates the signature algorithm, the default is HMAC SHA256 (written as HS256);

typ : Indicates the type of token (token), and JWT tokens are uniformly written as JWT.

Use Base64 encryption to form the first part of JWT - header:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

2.2.2 Payload composition

The Payload part is also a JSON object, which is used to store valid information that actually needs to be delivered.

Standard load : There are many, it is recommended to use, but not mandatory, to supplement JWT information.

standard load introduce
iss (issuer) Issuer (who issued it)
exp (expiration time) The expiration time must be greater than the issue time
sub (subject) subject (what to do)
aud (audience) Audience (for whom) such as: http://www.xxx.com
nbf (Not Before) effective time
iat (Issued At) Issue time
jti (JWT ID) ID, the unique identifier of JWT

Custom payload : You can add any information, generally add user-related information or other necessary information required by the business. However, it is not recommended to add sensitive information, because this part can be decrypted on the client side.

{
    "user_info": [
      {
        "id": "1"
      },
      {
        "name": "dafei"
      },
      {
        "age": "18"
      }
    ],
    "iat": 1681571257,
    "exp": 1682783999,
    "aud": "xiaofei",
    "iss": "dafei",
    "sub": "alluser"
}

 Encrypted using Base64, which constitutes the second part of the JWT-payload:

eyJ1c2VyX2luZm8iOlt7ImlkIjoiMSJ9LHsibmFtZSI6ImRhZmVpIn0seyJhZ2UiOiIxOCJ9XSwiaWF0IjoxNjgxNTcxMjU3LCJleHAiOjE2ODI3ODM5OTksImF1ZCI6InhpYW9mZWkiLCJpc3MiOiJkYWZlaSIsInN1YiI6ImFsbHVzZXIifQ

2.2.3 signature composition

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

First, you need to specify a key (secret). This key is known only to the server and cannot be disclosed to the user. Then, use the signature algorithm specified in the Header (the default is HMAC SHA256), and generate a signature according to the following formula.

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

After calculating the signature, combine the three parts of Header, Payload and Signature into a string, and separate each part with a "dot" ( .), and then return it to the user.

Because of the existence of this key, even if the caller secretly modifies the content of the first two parts, there will be inconsistent signatures in the verification process, so the security is guaranteed.

Use Base64 encryption to form the third part of JWT - signature:

l6JdYARw4IHmjliSbh9NP6ji1L15qVneWTJU5noQ-k8

2.3 Generate/parse JWT online

2.3.1 Coding tools

Address: JWT Token online encoding generation - ToolTT online toolbox

2.3.2 Decoding Tool

Address: JWT Online Decoding - Development Toolbox

Three, JWT characteristics

3.1 Stateless

JWT does not need to store any state on the server, and the client can carry JWT to access the server, thus making the server stateless. In this way, the server can more easily scale and load balance.

3.2 Customizable

The payload part of JWT can be customized, and any data in JSON format can be stored. This means that we can use JWT to implement some custom functions, such as storing user preferences, configuration information, and so on.

3.3 Strong scalability

JWT has a standard specification, so it is easy to share and parse across different platforms and languages. In addition, developers can customize claims (claims) as needed to achieve more flexible functions.

3.4 Good debugging

Since the content of the JWT exists in the form of a Base64 encoded string, it is very easy to debug and analyze.

3.5 Security depends on key management

The security of a JWT depends on key management. If the key is compromised or mismanaged, then the JWT will be vulnerable to attack. Therefore, when using JWT, we must pay attention to key management, including generation, storage, update, distribution and so on.

3.6 Cannot be revoked

Since JWT is stateless, once a JWT is issued, it cannot be revoked. If a user is logged out or disabled during JWT authentication, there is no way for the server to prevent the user from continuing to use a previously issued JWT. Therefore, developers need to design additional mechanisms to revoke JWT, such as using a blacklist or setting a short-term validity period, etc.

3.7 need to be cached to the client

Since JWT contains user information and authorization information, it generally needs to be cached by the client, which means that JWT has the risk of being stolen.

3.8 Payload size is limited

Since the JWT needs to be transmitted to the client, there is also a limit to the payload size. It is generally not recommended that the payload exceed 1KB, as it will affect performance.

4. Advantages and disadvantages of JWT

4.1 Advantages

  • Stateless: JWT itself does not need to be stored on the server, so stateless authentication and authorization can be achieved.

  • Extensibility: The load of JWT can be customized, so any information can be added according to requirements.

  • Reliability: JWT uses digital signatures to ensure security, so it is reliable.

  • Cross-platform: JWT supports multiple programming languages ​​and operating systems, so it is cross-platform.

  • Efficiency: Since JWT does not need to query the database, it is efficient.

4.2 Disadvantages

  • Security depends on key management: The security of JWT depends on the management of keys. If the key is leaked or mismanaged, then JWT will be attacked.

  • Cannot revoke tokens: Since JWTs are stateless, once a JWT has been issued, it cannot be revoked.

  • Need to be transmitted to the client: Since the JWT contains user information and authorization information, the JWT needs to be transmitted to the client, which means that the JWT has the risk of being stolen by an attacker.

  • Payload size is limited: Since the JWT needs to be transmitted to the client, there is also a limit to the payload size.

5. JWT Application Scenarios

5.1 One-time verification

You can use jwt to send an activation email after successful registration or if other businesses require email activation.

reason:

JWT timeliness: Make the link timeliness (for example, activate within 2 hours),

JWT immutability: preventing tampering to activate other accounts

5.2 Stateless authentication for RESTful API

Use JWT as the identity credential for RESTful api: when the user identity verification is successful, the client will bring JWT with each interface access, and the server will verify the legitimacy of JWT (whether it has been tampered with/expired, etc.)

5.3 Information exchange

JWTs are a great way to securely transfer information between parties (inter-project/inter-service). Because JWTs can be signed: e.g. using a public/private key pair, you can be sure the sender is who they say they are. Also, since the signature is calculated using the header and payload, you can also verify that the content has not been tampered with.

5.4 JWT Token Login

JWT token login is also an application scenario, but it is also the place where JWT is criticized the most, because JWT tokens are insecure.

1> JWT token storage and client, easy to leak and be destroyed by forged identities.

2> Once the JWT is issued, it cannot be revoked. When the destruction is in progress, the backend cannot immediately prohibit it.

The above problems can be avoided by monitoring abnormal JWT access, setting blacklist + forced offline, etc. to avoid losses.

6. JWT Practical Cases

6.1 Case 1: Email activation

6.1.1 Requirements and Analysis

Requirement: When the user registers successfully, an activation link is sent to the specified email address, and the account is activated when the user clicks the activation link.

analyze:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.3.2.RELEASE</version>
    <relativePath/>
</parent>
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <scope>compile</scope>
    </dependency>

    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-api</artifactId>
        <version>0.11.5</version>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-impl</artifactId>
        <version>0.11.5</version>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-jackson</artifactId> 
        <version>0.11.5</version>
        <scope>runtime</scope>
    </dependency>
</dependencies>

1> Design a registration interface/regist, and simulate the activation link after the request is successful (the essence of the link is that the activation interface parameter is: jwt)

2> Design the activation interface/active, the receiving parameter is jwt

6.1.2 Code implementation

Step 1: Create project: mail-active-demo

Step 2: Import related dependencies

Specifically which JWT toolkit to choose, you can see the official website recommendation: JSON Web Token Libraries - jwt.io

Choose here: jjwt

 

Step 3: Code Writing

User registration entity class

package com.langfeiyes.mail.entity;

public class User {
    private Long id;
    private String username;
    private String password;
    private int state;


    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public int getState() {
        return state;
    }

    public void setState(int state) {
        this.state = state;
    }
}

JWT constant class:

package com.langfeiyes.mail.util;

/**
 * 常量类
 */
public class JwtConstant {

    // 基本url
    public static final String BASE_DOMAIN_URL = "http://localhost:8080/";

    //jwt密码
    public static final String JWT_SECRET = "langfeiyesabcdefghijklmnopqrstuvwxyz11111111111";
    //jwt失效时间,单位秒
    public static final Long JWT_EXPIRATION = 24 * 60 * 60 * 1000L;
    //jwt 创建时间
    public static final String JWT_CREATE_TIME = "jwt_create_time";

    //jwt 用户信息-key
    public static final String USER_INFO_KEY = "user_info_key";
    //jwt 用户信息-id
    public static final String USER_INFO_ID = "user_info_id";
    //jwt 用户信息-username
    public static final String USER_INFO_USERNAME = "user_info_username";

}

JWT tool class:

package com.langfeiyes.mail.util;

import com.langfeiyes.mail.entity.User;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * JwtToken工具类
 */
public class JwtTokenUtil {


    /**
     * 从数据声明生成令牌
     *
     * @param claims 数据声明
     * @return 令牌
     */
    public static String createToken(Map<String, Object> claims) {
        String token = Jwts.builder()
                //.setHeader(new HashMap<>())
                //.setAudience("Audience")
                //.setIssuer("Issuer")
                //.setSubject("Subject")
                //.setNotBefore(new Date())
                //.setIssuedAt(new Date())
                //.setId("jwt id")
                .setClaims(claims)//把荷载存储到里面
                .setExpiration(generateExpirationDate())//设置失效时间
                .signWith(Keys.hmacShaKeyFor(Decoders.BASE64.decode(JwtConstant.JWT_SECRET))) //签名
                .compact();
        return token;
    }

    /**
     * 从令牌中获取数据声明
     *
     * @param token 令牌
     * @return 数据声明
     */
    public static Claims parseToken(String token){
        Claims claims=null;
        try{
            claims = Jwts.parserBuilder()
                    .setSigningKey(Decoders.BASE64.decode(JwtConstant.JWT_SECRET))
                    .build()
                    .parseClaimsJws(token)
                    .getBody();
        }catch (Exception e){
            e.printStackTrace();
        }
        return claims;
    }

    /**
     * 生成token失效时间
     */
    private static Date generateExpirationDate() {
        //失效时间是当前系统的时间+我们在配置文件里定义的时间
        return new Date(System.currentTimeMillis()+JwtConstant.JWT_EXPIRATION);
    }

    /**
     * 根据token获取用户名
     */
    public static String getUserName(String token){
        Claims claims = parseToken(token);
        return getValue(claims, JwtConstant.USER_INFO_USERNAME);
    }

    /**
     * 验证token是否有效
     */
    public static boolean validateToken(String token){
        //claims 为null 意味着要门jwt被修改
        Claims claims = parseToken(token);
        return claims != null &&!isTokenExpired(token);
    }

    /**
     * 判断token是否已经失效
     * @param token
     * @return
     */
    public static boolean isTokenExpired(String token) {
        //先获取之前设置的token的失效时间
        Date expireDate=getExpiredDate(token);
        return expireDate.before(new Date()); //判断下,当前时间是都已经在expireDate之后
    }

    /**
     * 根据token获取失效时间
     * 也是先从token中获取荷载
     * 然后从荷载中拿到到设置的失效时间
     * @param token
     * @return
     */
    private static Date getExpiredDate(String token) {
        Claims claims=parseToken(token);
        return claims.getExpiration();
    }


    /**
     * 刷新我们的token:重新构建jwt
     */
    public static String refreshToken(String token){
        Claims claims=parseToken(token);
        claims.put(JwtConstant.JWT_CREATE_TIME,new Date());
        return createToken(claims);
    }

    /**
     * 根据身份信息获取键值
     *
     * @param claims 身份信息
     * @param key 键
     * @return 值
     */
    public static String getValue(Claims claims, String key){
        return claims.get(key) != null ? claims.get(key).toString():null;
    }
}

interface:

package com.langfeiyes.mail.controller;

import com.langfeiyes.mail.util.JwtConstant;
import com.langfeiyes.mail.entity.User;
import com.langfeiyes.mail.util.JwtTokenUtil;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;

@RestController
public class UserController {
    //模拟需要缓存的用户库
    //key: url, value: 要激活用户
    private static Map<String, User> map = new HashMap<>();

    @GetMapping("/regist")
    public String regist(User user){
        //假装成功
        System.out.println("注册成功");
        user.setId(new Random().nextLong());
        //创建jwt
        Map<String, Object> claims = new HashMap<>();
        claims.put(JwtConstant.USER_INFO_ID, user.getId());
        claims.put(JwtConstant.USER_INFO_USERNAME, user.getUsername());
        claims.put(JwtConstant.JWT_CREATE_TIME, new Date());
        String jwt = JwtTokenUtil.createToken(claims);

        //缓存jwt
        map.put(jwt, user);
        return JwtConstant.BASE_DOMAIN_URL + "/active?jwt=" + jwt;
    }


    @GetMapping("/active")
    public String active(String jwt){
        User user = map.get(jwt);
        if(user != null && JwtTokenUtil.validateToken(jwt)){
            map.remove(jwt);
            return "执行激活逻辑...";
        }else{
            return "参数不合法...";
        }
    }
}

Step 4: Start the project

package com.langfeiyes.mail;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class App {
    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }
}

Step 5: Test

The browser initiates 2 requests:

register

http://localhost:8080/regist?username=dafei&password=666

activation

http://localhost:8080//active?jwt=eyJhbGciOiJIUzUxMiJ9.eyJjcmVhdGVfdGltZSI6MTY4MTYxNzA2MDg3NSwiaWQiOjEsInVzZXJuYW1lIjoiZGFmZWkiLCJleHAiOjE2ODE3MDM0NjB9.vQcsXUaEictz3QgjUBKwAV1qlou9yFCSMo4H6OaArz1ReEFzXt6klziHqonvsEfkv9aYdDc6G-vKVO9Zh1kcXw

6.2 Case 2: JWT token login

6.2.1 Requirements and Analysis

Requirement: Design two interfaces /login and /list to realize login and list logic. Note that login verification must be performed when accessing the /list interface

Requirements: use Spring security + JWT

Analysis :

1> Design 2 interfaces, /login login successfully creates JWT response to the client

2> Design a login check interceptor to perform login interception when accessing the /list interface

Code design :

 

6.2.2 Code implementation

Step 1: Create a project: security-jwt-demo

Step 2: Import related dependencies

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.3.2.RELEASE</version>
    <relativePath/>
</parent>
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <scope>compile</scope>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>

    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-api</artifactId>
        <version>0.11.5</version>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-impl</artifactId>
        <version>0.11.5</version>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-jackson</artifactId>
        <version>0.11.5</version>
        <scope>runtime</scope>
    </dependency>
</dependencies>

 

Step 3: Write the Code

Entity class: User--login subject

package com.langfeiyes.jwt.entity;

public class User {
    private Long id;
    private String username;
    private String password;
    private int state;

    public User() {
    }

    public User(String username, String password) {
        this.username = username;
        this.password = password;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public int getState() {
        return state;
    }

    public void setState(int state) {
        this.state = state;
    }
}

Constant class: JwtConstant--configure jwt password, expiration time

package com.langfeiyes.jwt.util;

/**
 * 常量类
 */
public class JwtConstant {

    // 基本url
    public static final String BASE_DOMAIN_URL = "http://localhost:8080/";

    //jwt密码
    public static final String JWT_SECRET = "langfeiyesabcdefghijklmnopqrstuvwxyz11111111111";
    //jwt失效时间,单位秒
    public static final Long JWT_EXPIRATION = 24 * 60 * 60 * 1000L;
    //jwt 创建时间
    public static final String JWT_CREATE_TIME = "jwt_create_time";

    //jwt 用户信息-key
    public static final String USER_INFO_KEY = "user_info_key";
    //jwt 用户信息-id
    public static final String USER_INFO_ID = "user_info_id";
    //jwt 用户信息-username
    public static final String USER_INFO_USERNAME = "user_info_username";

}

Tool class-JwtTokenUtil--JWT method operation

package com.langfeiyes.jwt.util;

import com.langfeiyes.jwt.entity.User;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * JwtToken工具类
 */
public class JwtTokenUtil {


    /**
     * 从数据声明生成令牌
     *
     * @param claims 数据声明
     * @return 令牌
     */
    public static String createToken(Map<String, Object> claims) {
        String token = Jwts.builder()
                //.setHeader(new HashMap<>())
                //.setAudience("Audience")
                //.setIssuer("Issuer")
                //.setSubject("Subject")
                //.setNotBefore(new Date())
                //.setIssuedAt(new Date())
                //.setId("jwt id")
                .setClaims(claims)//把荷载存储到里面
                .setExpiration(generateExpirationDate())//设置失效时间
                .signWith(Keys.hmacShaKeyFor(Decoders.BASE64.decode(JwtConstant.JWT_SECRET))) //签名
                .compact();
        return token;
    }

    /**
     * 从令牌中获取数据声明
     *
     * @param token 令牌
     * @return 数据声明
     */
    public static Claims parseToken(String token){
        Claims claims=null;
        try{
            claims = Jwts.parserBuilder()
                    .setSigningKey(Decoders.BASE64.decode(JwtConstant.JWT_SECRET))
                    .build()
                    .parseClaimsJws(token)
                    .getBody();
        }catch (Exception e){
            e.printStackTrace();
        }
        return claims;
    }

    /**
     * 生成token失效时间
     */
    private static Date generateExpirationDate() {
        //失效时间是当前系统的时间+我们在配置文件里定义的时间
        return new Date(System.currentTimeMillis()+JwtConstant.JWT_EXPIRATION);
    }

    /**
     * 根据token获取用户名
     */
    public static String getUserName(String token){
        Claims claims = parseToken(token);
        return getValue(claims, JwtConstant.USER_INFO_USERNAME);
    }

    /**
     * 验证token是否有效
     */
    public static boolean validateToken(String token){
        //claims 为null 意味着要门jwt被修改
        Claims claims = parseToken(token);
        return claims != null &&!isTokenExpired(token);
    }

    /**
     * 判断token是否已经失效
     * @param token
     * @return
     */
    public static boolean isTokenExpired(String token) {
        //先获取之前设置的token的失效时间
        Date expireDate=getExpiredDate(token);
        return expireDate.before(new Date()); //判断下,当前时间是都已经在expireDate之后
    }

    /**
     * 根据token获取失效时间
     * 也是先从token中获取荷载
     * 然后从荷载中拿到到设置的失效时间
     * @param token
     * @return
     */
    private static Date getExpiredDate(String token) {
        Claims claims=parseToken(token);
        return claims.getExpiration();
    }


    /**
     * 刷新我们的token:重新构建jwt
     */
    public static String refreshToken(String token){
        Claims claims=parseToken(token);
        claims.put(JwtConstant.JWT_CREATE_TIME,new Date());
        return createToken(claims);
    }

    /**
     * 根据身份信息获取键值
     *
     * @param claims 身份信息
     * @param key 键
     * @return 值
     */
    public static String getValue(Claims claims, String key){
        return claims.get(key) != null ? claims.get(key).toString():null;
    }
}

Interface Response Class - R

package com.langfeiyes.jwt.util;

public class R {
    private int code;
    private String msg;
    private Object data;

    public R() {
    }

    public R(int code, String msg, Object data) {
        this.code = code;
        this.msg = msg;
        this.data = data;
    }

    public static R ok(){
        return new R(200, "操作成功", null);
    }
    public static R ok(Object data){
        return new R(200, "操作成功", data);
    }
    public static R fail(){
        return new R(500, "操作失败", null);
    }
    public static R fail(Object data){
        return new R(500, "操作失败", data);
    }
    public static R fail(String msg){
        return new R(500, msg, null);
    }

    public int getCode() {
        return code;
    }

    public String getMsg() {
        return msg;
    }

    public Object getData() {
        return data;
    }
}

Login filter: JwtLoginFilter--do jwt login

package com.langfeiyes.jwt.filter;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.langfeiyes.jwt.util.JwtConstant;
import com.langfeiyes.jwt.util.JwtTokenUtil;
import com.langfeiyes.jwt.util.R;
import org.springframework.security.core.Authentication;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

public class JwtLoginFilter extends UsernamePasswordAuthenticationFilter {
    private AuthenticationManager authenticationManager;
    public JwtLoginFilter(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;

        this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/login", "GET"));
    }



    /**
     * 拦截登录。获取表单的用户名与密码
     */
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        //使用 请求体 传递登录参数,更加安全
        String username = request.getParameter("username");
        String password = request.getParameter("password");
        return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
    }

    /**
     * 登录成功后调用的方法
     * 返回token
     */
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {

        //security 登录成功封装实体验证
        UserDetails userDetails = (UserDetails) authResult.getPrincipal();
        //根据用户名生成token
        //创建jwt
        Map<String, Object> claims = new HashMap<>();
        claims.put(JwtConstant.USER_INFO_USERNAME, userDetails.getUsername());
        claims.put(JwtConstant.JWT_CREATE_TIME, new Date());
        String jwt = JwtTokenUtil.createToken(claims);

        String token = JwtTokenUtil.createToken(claims);
        response.setHeader("token", token);
        response.setContentType("application/json;charset=utf-8");
        response.getWriter().print(new ObjectMapper().writeValueAsString(R.ok("登录成功")));
    }

    /**
     * 登录失败后调用的方法
     */
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
        response.setContentType("application/json;charset=utf-8");
        response.getWriter().print(new ObjectMapper().writeValueAsString(R.fail(failed.getMessage())));
    }
}

Login Intercept Filter-JwtAuthFilter--Login Check--Permission Check

package com.langfeiyes.jwt.filter;

import com.langfeiyes.jwt.util.JwtTokenUtil;
import io.jsonwebtoken.Jwt;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.util.StringUtils;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;

public class JwtAuthFilter extends BasicAuthenticationFilter {
    public JwtAuthFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }
    /**
     * 对HTTP请求头做处理
     */
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        //授权
        UsernamePasswordAuthenticationToken authRequest = getAuthentication(request);
        //授权失败
        if (authRequest == null) {
            chain.doFilter(request, response);
            return;
        }
        //如果有授权,放到权限上下文(容器)中
        SecurityContextHolder.getContext().setAuthentication(authRequest);
        chain.doFilter(request, response);
    }

    /**
     * 认证token是否合法,若合法,返回认证,否则返回null
     */
    private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
        //获取token
        String token = request.getHeader("token");

        if (!StringUtils.isEmpty(token) && JwtTokenUtil.validateToken(token)) {
            //从token中获取username
            String username = JwtTokenUtil.getUserName(token);

            return new UsernamePasswordAuthenticationToken(username, token, Arrays.asList(new SimpleGrantedAuthority("admin")));
        }
        return null;
    }
}

Security overall configuration class - JwtWebSecurityConfig

package com.langfeiyes.jwt.config;

import com.langfeiyes.jwt.filter.JwtAuthFilter;
import com.langfeiyes.jwt.filter.JwtLoginFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.DelegatingPasswordEncoder;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class JwtWebSecurityConfig extends WebSecurityConfigurerAdapter {



    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("dafei")
                .password("666")
                .roles("admin")
                .and()
                .passwordEncoder(NoOpPasswordEncoder.getInstance());

    }



    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .exceptionHandling()

                .and().authorizeRequests()
                .antMatchers("/login").permitAll()			//所有请求都可以访问
                .antMatchers("/list").authenticated()
                .and().logout()
                .and()
                .addFilter(new JwtLoginFilter(authenticationManager()))	//登录时的过滤器
                .addFilter(new JwtAuthFilter(authenticationManager()))	//验证JWT的过滤器
                .httpBasic();
    }

}

Access interface - UserController

package com.langfeiyes.jwt.controller;

import com.langfeiyes.jwt.util.R;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {

    @GetMapping("/list")
    public R list(){
        return R.ok("list....");
    }
}

Step 4: Start the project

package com.langfeiyes.jwt;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class App {
    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }
}

Step 5: Test

GET http://localhost:8080/login?username=dafei&password=666

<> 2023-04-16T230507.200.json
    
###
GET http://localhost:8080/list
Accept: */*
token: eyJhbGciOiJIUzUxMiJ9.eyJjcmVhdGVfdGltZSI6MTY4MTY1NzUwNzE1NCwidXNlcm5hbWUiOiJkYWZlaSIsImV4cCI6MTY4MTc0MzkwN30.q8X7IisUgF8if299exu1jU-0hgZOFzgUABt9SynqQ2HdyVJJqfAZpywVmyvRLQ8-n5hLf-JtF2mjAbQBlfQZwg

<> 2023-04-16T231623.200.json

7. JWT Pit Avoidance Guide

7.1 Redis verification implements token leakage protection

Because JWT is stateless, when the JWT token is issued, it cannot be destroyed within the valid time, so there is a big hidden danger: token leakage

Pit avoidance : When issuing JWT tokens, a copy is also cached in Redis. When it is determined that a JWT has been leaked, the JWT in Redis is immediately removed. When the interface initiates a request, the user is forced to re-authenticate until the authentication succeeds.

7.2 Temporary inspection JWT restricts sensitive operations

Based on the stateless nature of JWT, the leakage may be large at the same time, some of which involve sensitive data changes, and perform temporary inspection operations.

Pitfall avoidance : When sensitive operations such as adding, modifying, deleting, uploading, and downloading are involved, the user's identity is forcibly checked, such as mobile phone verification codes, scanning QR codes, etc., to confirm that the operator is the user himself.

7.3 Abnormal JWT monitoring: Overclocking identification and restrictions

When the JWT token is stolen, there will generally be high-frequency system access. In view of this situation, the number of requests per unit time of the client is monitored, and when the number of requests per unit time exceeds a predetermined threshold value, it is determined that the user's JWT token is abnormal.

Pit avoidance : When it is judged that the JWT token is abnormal, directly restrict it (for example: IP current limit, JWT blacklist, etc.).

7.4 Region check to eliminate the possibility of JWT leakage

Generally, the range of user activities is fixed, which means that the access IP of the JWT client is relatively fixed. After the JWT is leaked, it may be possible to log in from a different place first.

Pitfall avoidance : Check JWT for remote access. During the validity period, frequent IP changes can be judged as JWT leaks.

7.5 Client distinguish check to prevent JWT leakage

For APP products, the general client is fixed, which is basically a mobile device (APP, tablet), which can be bound with the machine code of the device.

Pit avoidance : Bind JWT with machine code, store it with the server, and when the client initiates a request, check whether the client’s machine code matches the server’s machine code to determine whether the JWT has been leaked.

7.6 JWT token protection: limited time, limited number, limited frequency

Leakage of JWT tokens is unavoidable, but we can carry out leakage identification and do a good job of remediation after leakage to ensure system security.

Avoid pitfalls : Make reasonable restrictions on clients, such as limiting the number of JWT tokens for each client, access frequency, JWT token aging, etc., to reduce the risk of JWT token leakage.

8. Common interview questions

Q: What are JWTs? Explain its structure.

JWT is an open standard for securely transferring information over the web. It consists of three parts: header, payload and signature. The header contains the metadata of the token, the payload contains the actual information (such as user ID, role, etc.), and the signature is used to verify whether the token has been tampered with.

Q: What are the advantages of JWT? What are its advantages and disadvantages compared to traditional session-based authentication?

The advantages of JWT include stateless, extensible, cross-language, easy to implement and good security. In contrast, traditional session-based authentication needs to maintain session state on the server side, which makes the server load higher and is not suitable for distributed systems.

Q: What are the parts in the structure of JWT? What is the role of each part?

The structure of JWT consists of three parts: header, payload and signature. The header contains the token type and algorithm, the payload contains the actual information, and the signature is generated from the header, payload and key.

Q: How do JWTs work? What is the complete flow from inception to verification process?

The workflow of JWT is divided into three steps: generate token, send token, verify token. When generating the token, the server signs the header and payload with the secret key. When sending a token, send the token to the client. When validating the token, the client parses the header and payload from the token and verifies the signature using the same key.

Q: What is the signature of JWT? Why do you need to sign the JWT? How to verify signature of JWT?

The signature of JWT is generated from the header, payload and secret key to verify whether the token has been tampered with. The signature is generated using the HMAC algorithm or the RSA algorithm. When verifying the JWT's signature, the client generates a signature using the same key and algorithm, and compares the generated signature with the signature in the token.

Q: What is JWT's token refresh? Why is this feature needed?

Token refresh is a mechanism to solve the problem of needing to re-login after JWT expires. In token refresh, the server generates a new JWT and sends it to the client. The client replaces the old JWT with the new JWT, thereby extending the validity period of the token.

Q: Are JWTs encrypted? If yes, which parts are encrypted? If not, how does it ensure data security?

JWT itself is not encrypted, but can contain sensitive information in the payload. To protect this information, the payload can be encrypted using JWE (JSON Web Encryption). If it is not encrypted, you need to ensure that no sensitive information is included in the payload when generating the JWT.

Q: In JWT, how to deal with the issue of Token expiration? What are the ways to deal with it?

After the JWT expires, the client needs to obtain a new JWT again. Expiration issues can be resolved by including an expiration time in the JWT or using mechanisms such as refresh tokens.

Q: What is the relationship between JWT and OAuth2? What's the difference between them?

Both JWT and OAuth2 are open standards for authentication and authorization. JWT is an authentication mechanism while OAuth2 is an authorization mechanism. JWT is used to transfer information securely across different systems and OAuth2 is used to authorize third-party applications to access protected resources.

Q: In what scenarios is it appropriate to use JWT? What are its limitations?

JWT is more suitable for use in monolithic applications or microservice architectures. Its limitations include no revocation, larger tokens, and inability to handle concurrency. In cases where access control is required on a per-request basis or where tokens need to be revoked, JWT may not be the best choice.

Guess you like

Origin blog.csdn.net/langfeiyes/article/details/130624876