1
头图

A brief analysis of the correct posture for using JWT

For a long time, I have not used jwt correctly. After realizing this problem, I will share with you my truest thinking and summary. Welcome to discuss together.

status quo

Let me tell you how I used it before:

  1. After the login is successful, put the userId into the payload to generate jwt (valid for 8 hours), then send the jwt to the front end, and the front end stores it
  2. Every time the front end accesses the API, jwt needs to be carried in the header
  3. The backend parses the jwt first, and after getting the userId, the database queries the user's permission list (user-role-permission)
  4. After getting the user's permission list, match with the permissions required by the current interface. If the match is successful, the data will be returned, and if it fails, 401 will be returned.

jwt standard

First, let's check what the standard is like. First, refer to the description of the usage scenario on jwt.io :

  1. Authorization
  2. Information Exchange

For the above information, my personal understanding is two aspects:

  1. Allow users to access routes, services and resources. In my case, it is the permissions required by the interface. You can also log in through SSO. I don't need it here at the moment.
  2. The identity of the current user can be determined, in my case it is userId

optimization

The current usage has the following flaws:

  1. Every time the interface is called, the database query permission (user-role-permission) is required, which wastes resources
  2. After 8 hours of successful login, even if the user has been using the system, jwt will still be invalid and need to log in again

The first point is easy to say, put the permission list into the payload, and directly compare it with the permissions required by the interface after parsing.

Second point, extend the validity period to one week, one month? However, it still happens that the jwt is in use and it fails, and you need to log in again. It is not safe to set the time too long, because jwt itself is stateless, and what should you do if the permissions are changed? Does it take a long time to take effect? , so it seems that jwt must be refreshed.

The corresponding optimization point is here:

  1. Put the permission list into the payload, without having to go to the database every time
  2. Let users refresh jwt without feeling

refresh jwt scheme

Here is a reference to the discussion above on stackoverflow:

  1. jwt-refresh-token-flow
  2. JWT (JSON Web Token) automatic prolongation of expiration

Then I determined my refresh flow:

  1. After successful login, two tokens are issued: accessToken is valid for 1 hour, refreshToken is valid for 1 day
  2. 401 is returned after the accessToken is invalid, and the front end obtains a new accessToken and a new refreshToken through refreshToken
  3. After refreshToken expires, it returns 403 and needs to log in again

That is to say, after the login is successful, you can continue to operate within the validity period of refreshToken, and the validity period will be extended, and there will never be a situation where you suddenly need to log in again. The validity period of these two can be adjusted by yourself. What I consider here is that the accessToken should not be too long, otherwise the effective period after adjusting the permissions will be too short.

rear end adjustment

A new interface for refreshing the token is added. Most of the logic is the same as the login. After verifying the refreshToken, it returns a new accessToken and a new refreshToken

Front end adjustment

The main difficulty is in the front-end part, the refresh logic of the front-end:

  1. After the login is successful, the accessToken and refreshToken are stored in the front end, and each subsequent API call needs to carry the accessToken
  2. After one hour, the user continues to operate the backend and returns 401. At this time, the accessToken is invalid, and all requests at this stage are cached.
  3. Use refreshToken to get new accessToken and new refreshToken
  4. Use the new accessToken to re-initiate all cached requests just now
  5. One day later, the user operates again, and the backend returns 401. At this time, the accessToken is invalid, and all requests at this stage are cached.
  6. Use refreshToken to obtain, the backend returns 403, jump to the login page to log in again

What needs to be considered here is the concurrent request. It is necessary to cache all requests during the period after the accessToken expires, and continue all outstanding requests after obtaining a valid accessToken.

At present, I am using axios, the interceptor I use, and I will post some core code here:

 // 响应拦截器
axios.interceptors.response.use(
  response => {
    const data = response.data;
    // 没有code但是http状态为200表示外部请求成功
    if (!data.code && response.status === 200) return data;
    // 根据返回的code值来做不同的处理(和后端的私有约定)
    switch (data.code) {
      case 200:
        return data;
      default:
    }
    // 若不是正确的返回code,且已经登录,就抛出错误
    throw data;
  },
  err => {
    // 这里是返回 http 状态码不为 200和304 时候的错误处理
    if (err && err.response) {
      switch (err.response.status) {
        case 400:
          err.message = '请求错误';
          break;

        case 401:
          // accesstoken 错误
          if (router.currentRoute.path === '/login') {
            break;
          }
          // 判断是否有 refreshToken
          const root = useRootStore();
          if (!root.refreshToken) {
            logout();
            break;
          }
          // 进入刷新 token 流程
          // 本次请求的所有配置信息,包含了 url、method、data、header 等信息
          const config = err?.config;
          const requestPromise = new Promise(resolve => {
            addRequestList(() => {
              // 注意这里的createRequest函数执行的时候是在resolve开始执行的时候,并且返回一个新的Promise,这个新的Promise会代替接口调用的那个
              resolve(createRequest(config));
            });
          });
          refreshTokenRequest();
          // 这里很重要,因为本次请求 401 了,要返回给调用接口的方法返回一个新的请求
          return requestPromise;

        case 403:
          // 403 这里说明刷新token失败,登录已经到期,需要重新登录
          // 10 秒后清除所有缓存的请求
          setTimeout(() => {
            clearTempRequestList();
          }, 10000);
          logout();
          break;

        default:
      }
    }
    return Promise.reject(err);
  }
);

The logic code of the refresh part:

 import axios from 'axios';
import http from './index';
import { useRootStore } from '@/store/root';

// 临时的请求函数列表
const tempRequestList = [];

// 发起刷新token的标志位,防止重复刷新请求
let isRefreshing = false;

// 1min 内刷新过token标志位
// 为了防止并发的时候,刷新请求完毕,tempRequestList也已经清空,之后仍有请求返回403,造成重复刷新
let refreshTokenWithin1Minute = false;

const refreshTokenRequest = () => {
  if (isRefreshing) {
    return;
  }
  if (refreshTokenWithin1Minute) {
    for (const request of tempRequestList) {
      request();
    }
    tempRequestList.length = 0;
    return;
  }
  isRefreshing = true;
  refreshTokenWithin1Minute = true;
  const root = useRootStore();
  // 使用刷新token请求新的accesstoken和刷新token
  const params = {
    refreshToken: root.refreshToken
  };
  http.post('/api/v1/refresh-token', params).then(({ data }) => {
    root.updateAccessToken(data.token);
    root.updateRefreshToken(data.refreshToken);
    root.updateUserId(data.userId);
    for (const request of tempRequestList) {
      request();
    }
    // 1 min 后清除标志位
    setTimeout(() => {
      refreshTokenWithin1Minute = false;
    }, 60000);
    tempRequestList.length = 0;
    isRefreshing = false;
  });
};

const addRequestList = request => {
  tempRequestList.push(request);
};

const clearTempRequestList = () => {
  tempRequestList.length = 0;
};

const createRequest = config => {
  // 这里必须更新 header 中的 AccessToken
  const root = useRootStore();
  config.headers['Authorization'] = 'Bearer ' + root.accessToken;
  return axios(config);
};

export { refreshTokenRequest, createRequest, addRequestList, clearTempRequestList };

source code

The source code is provided at the front and back ends. You can use the source code to adjust the validity period of the two tokens for testing.

The source code of the backend part is here

The source code of the front-end part is here

There is also an online experience address


hezhongfeng
257 声望452 粉丝

coder