[Javascript] I am not satisfied with the Token non-perceptual refreshing scheme on the Internet, so I thought about it myself and it feels pretty good~

?Foreword

Imagine that there is a super large form page, the user finally fills it out, and then clicks submit. At this time, the request interface actually returns 401, and then jumps to the login page. . . Then the user must have ten thousand idiots in his heart~~~
Therefore, it is necessary to implement token-less refresh in the project~

In the past few days, I have implemented a solution for token non-perceptual refresh in the project. In fact, I also looked at the solutions on the Internet, and I know that such solutions are already popular, but I feel that they are not in line with the effect I want. Mainly This is reflected in the following aspects:

  • The logic is all written in the interceptor, and the coupling is high, which is not good.
  • The interface retry mechanism is not very good.
  • The logical processing of interface concurrency is not very good.

Why don’t I want this set of logic to be coupled in the interceptor?

On the one hand, it’s because I want to write a set of code that can be used in many projects to minimize the intrusion of the code.
On the other hand, because I think the token-insensitive refresh involves interface retransmission, I understand it is from the interface dimension, and this set of logic should not be placed in the response interceptor. . I understand that after resending, it will be an independent new interface request. I don’t want two independent interface requests to overlap with each other~

So I decided to write a plan myself and share it with everyone. I hope you can give me your opinions and make progress together~
Warm reminder: You need to have some Promise basics

Ideas

In fact, the general idea is the same, but the implementation may be different ~ that is, two tokens are needed

  • accessToken: ordinary token, short validity period
  • refreshToken: refresh token, long-lasting

accessToken is used as a token for interface requests. When accessToken expires, refreshToken will be used to request the backend and re-obtain a valid one. accessToken, and then let the interface re-initiate the request, so as to achieve the effect of user non-aware token refresh
It is divided into several steps:
1. When logging in, get accessToken and refreshToken and save them together.
2. When requesting the interface, use accessToken to request
3. If accessToken expires, the backend will return 401
4. When 401 occurs, the front end will use refreshToken to request the back end to give a valid accessToken
5. After re-obtaining a valid accessToken, re-initiate the request just now
6. Repeat 1/2/3/4/5

Some people may ask: What if refreshToken also expires?
Good question, if refreshToken also expires, then it has really expired, and you can only jump to the login page~

Nodejs simulation token

In order to facilitate the demonstration for everyone, I used express to simulate the back-end token caching and acquisition. The code is as shown below (the complete code is at the end of the article). Since this is just for demonstration, I set up

  • accessToken: expires in 10 seconds
  • refreshToken: expires in 30 seconds
const express = require("express");
const server = express();
// Enable CORS
server.use((req, res, next) => {<!-- -->
  res.setHeader("Access-Control-ALlow-Origin", "*"); // Allow requests from any origin
  res.setHeader("Access-Control-ALLow-Methods", "GET,POST,PUT, DELETE"); // Allowed HTTP methods
  res.setHeader("Access-Control-Allow-Headers", "Content-Type,Authorization"); // Allowed request headers
  next();
});
// Ordinary token
let accessToken = null;
//Refresh token
let refreshToken = null;
// Ordinary token, expires in 10s
const ACCESS_EXPIRES = 10 * 1000;
//Refresh token, 50s period
const REFRESH_EXPIRES = 30 * 1000;
// Simulate server-side cache accessToken
const getAccessToken = (() => {<!-- -->
  let timer = null;
  return () => {<!-- -->
    if (timer) return accessToken;
    accessToken = `accessToken ${<!-- -->new Date().getTime()}`;
    timer = setTimeout(() => {<!-- -->
      timer = null;
      accessToken = null;
    }, ACCESS_EXPIRES);
    return accessToken;
  };
})();
// Simulate server-side cache refreshToken
const getRefreshToken = (() => {<!-- -->
  let timer = null;
  return () => {<!-- -->
    if (timer) return refreshToken;
    refreshToken = `refreshToken ${<!-- -->new Date() - getTime()}`;
    timer = setTimeout(() => {<!-- -->
      timer = null;
      refreshToken = null;
    }, REFRESH_EXPIRES);
    return refreshToken;
  };
})();
//Login interface
server.post("/login", (req, res) => {<!-- -->
  // Send two tokens to the front end
  res.send({<!-- -->
    accessToken: getAccessToken(),
    refreshToken: getRefreshToken(),
  });
});
//Test interface
server.get("/test", (req, res) => {<!-- -->
  const _accessToken = req.headers.authorization;
  if (_accessToken !== accessToken) {<!-- -->
    return res.status(401).json({<!-- --> error: "Unauthorized" });
  }
  res.send({<!-- -->
    name: "test",
  });

  //Ogitori token interface
  server.get("/token", (req, res) => {<!-- -->
    const _refreshToken = req.headers.authorization;
    // If refreshToken expires, it means it has really expired.
    if (_refreshToken !== refreshToken) {<!-- -->
      return res.status(401).json({<!-- --> error: "Unauthorized" });
    }
    res.send({<!-- --> accessToken: getAccessToken() });
  });
});

server.listen(8888, () => {<!-- -->
  console.log("Successfully started port: 8888");
});

Front-end simulation request

First create a constants.ts to store some constants (complete source code at the end of the article)

// constants.ts
//Key stored in LocalStorage
export const LOCAL ACCESS KEY ='access_token';
export const LOCAL REFRESH KEY = 'refresh token';
//Requested baseUrl
export const BASE URL = 'http://localhost:8888';// path
export const LOGIN_URL = '/login';
export const TEST_URL = '/test';
export const FETCH TOKEN URL = '/token';

Then we need to simply encapsulate axios and simulate:

  • After simulating login, obtain the double token and store it
  • The accessToken expired after 10 seconds of simulation.
  • refreshToken expires after 30 seconds of simulation

Ideally, if the user is unaware, the console should print

test-1
test-2
test-3
test-4

Printing test-1 and test-2 is easier to understand
Test-3 and test-4 are printed because although the accessToken has expired, I use refreshToken to re-obtain a valid accessToken, and then re-initiate requests for 3 and 4, so test-3 and test-4 will be printed as usual.

Test-5 and test-6 will not be printed because the refreshToken has expired at this time, so both tokens have expired at this time, and any request will not succeed~
But we see that the current situation is that only test-1 and test-2 are printed.

Don’t worry, we will implement the function of token non-perceptual refresh next~

Implementation

My expectation is to encapsulate a class that provides the following functions:

  • 1. Can use refreshToken to obtain new accessToken
  • 2. Not coupled with axios interceptor
  • 3. When a new accessToken is obtained, the request that just failed can be re-initiated, seamlessly, and achieve an imperceptible effect.
  • 4. When there are multiple requests concurrently, intercept them and do not allow them to obtain the accessToken multiple times.

In response to these points, I did the following things:

  • 1. The class provides a method to initiate a request and obtain a new accessToken with refreshToken.
  • 2. Provide a wrapper high-order function to perform additional processing on each request.
  • 3/4. Maintain a promise. This promise will only be fulfilled when a new accessToken is requested.

And this class also needs to support configuration, and the following parameters can be passed in:
baseUrl: base url
url: URL for requesting new accessToken
getRefreshToken: function to obtain refreshToken
unauthorizedCode: Unauthorized status code, default 401
onSuccess: callback after successful acquisition of new accessToken
onError: Callback after failure to obtain new accessToken

Finally, the final effect was achieved and these four texts were printed.

Complete code

constants.ts

// constants.ts

// key stored in localStorage
export const LOCAL_ACCESS_KEY = 'access_token';
export const LOCAL_REFRESH_KEY = 'refresh_token';

//Requested baseUrl
export const BASE_URL = 'http://localhost:8888';
// path
export const LOGIN_URL = '/login';
export const TEST_URL = '/test';
export const FETCH_TOKEN_URL = '/token';

retry.ts

// retry.ts

import {<!-- --> Axios } from 'axios';

export class AxiosRetry {<!-- -->
  // Maintain a promise
  private fetchNewTokenPromise: Promise<any> | null = null;

  // Some necessary configuration
  private baseUrl: string;
  private url: string;
  private getRefreshToken: () => string | null;
  private unauthorizedCode: string | number;
  private onSuccess: (res: any) => any;
  private onError: () => any;

  constructor({<!-- -->
    baseUrl,
    url,
    getRefreshToken,
    unauthorizedCode = 401,
    onSuccess,
    onError,
  }: {<!-- -->
    baseUrl: string;
    url: string;
    getRefreshToken: () => string | null;
    unauthorizedCode?: number | string;
    onSuccess: (res: any) => any;
    onError: () => any;
  }) {<!-- -->
    this.baseUrl = baseUrl;
    this.url = url;
    this.getRefreshToken = getRefreshToken;
    this.unauthorizedCode = unauthorizedCode;
    this.onSuccess = onSuccess;
    this.onError = onError;
  }

  requestWrapper<T>(request: () => Promise<T>): Promise<T> {<!-- -->
    return new Promise((resolve, reject) => {<!-- -->
      // Save the request function first
      const requestFn = request;
      return request()
        .then(resolve)
        .catch(err => {<!-- -->
          if (err?.status === this.unauthorizedCode & amp; & amp; !(err?.config?.url === this.url)) {<!-- -->
            if (!this.fetchNewTokenPromise) {<!-- -->
              this.fetchNewTokenPromise = this.fetchNewToken();
            }
            this.fetchNewTokenPromise
              .then(() => {<!-- -->
                // After successfully obtaining the token, re-execute the request
                requestFn().then(resolve).catch(reject);
              })
              .finally(() => {<!-- -->
                // Blanking
                this.fetchNewTokenPromise = null;
              });
          } else {<!-- -->
            reject(err);
          }
        });
    });
  }

  //Function to get token
  fetchNewToken() {<!-- -->
    return new Axios({<!-- -->
      baseURL: this.baseUrl,
    })
      .get(this.url, {<!-- -->
        headers: {<!-- -->
          Authorization: this.getRefreshToken(),
        },
      })
      .then(this.onSuccess)
      .catch(() => {<!-- -->
        this.onError();
        return Promise.reject();
      });
  }
}

index.ts

import {<!-- --> Axios } from 'axios';
import {<!-- -->
  LOCAL_ACCESS_KEY,
  LOCAL_REFRESH_KEY,
  BASE_URL,
  LOGIN_URL,
  TEST_URL,
  FETCH_TOKEN_URL,
} from './constants';
import {<!-- --> AxiosRetry } from './retry';

const axios = new Axios({<!-- -->
  baseURL: 'http://localhost:8888',
});

axios.interceptors.request.use(config => {<!-- -->
  const url = config.url;
  if (url !== 'login') {<!-- -->
    config.headers.Authorization = localStorage.getItem(LOCAL_ACCESS_KEY);
  }
  return config;
});

axios.interceptors.response.use(res => {<!-- -->
  if (res.status !== 200) {<!-- -->
    return Promise.reject(res);
  }
  return JSON.parse(res.data);
});

const axiosRetry = new AxiosRetry({<!-- -->
  baseUrl: BASE_URL,
  url: FETCH_TOKEN_URL,
  unauthorizedCode: 401,
  getRefreshToken: () => localStorage.getItem(LOCAL_REFRESH_KEY),
  onSuccess: res => {<!-- -->
    const accessToken = JSON.parse(res.data).accessToken;
    localStorage.setItem(LOCAL_ACCESS_KEY, accessToken);
  },
  onError: () => {<!-- -->
    console.log('refreshToken has expired, please go to the login page');
  },
});

const get = (url, options?) => {<!-- -->
  return axiosRetry.requestWrapper(() => axios.get(url, options));
};

const post = (url, options?) => {<!-- -->
  return axiosRetry.requestWrapper(() => axios.post(url, options));
};

const login = (): any => {<!-- -->
  return post(LOGIN_URL);
};
const test = (): any => {<!-- -->
  return get(TEST_URL);
};

// Simulate page function
const doing = async () => {<!-- -->
  // Simulate login
  const loginRes = await login();
  localStorage.setItem(LOCAL_ACCESS_KEY, loginRes.accessToken);
  localStorage.setItem(LOCAL_REFRESH_KEY, loginRes.refreshToken);

  // Simulate requests within 10 seconds
  test().then(res => console.log(`${<!-- -->res.name}-1`));
  test().then(res => console.log(`${<!-- -->res.name}-2`));

  // After simulating the request for 10 seconds, the accessToken becomes invalid.
  setTimeout(() => {<!-- -->
    test().then(res => console.log(`${<!-- -->res.name}-3`));
    test().then(res => console.log(`${<!-- -->res.name}-4`));
  }, 10000);

  //Request after 30s of simulation, refreshToken becomes invalid
  setTimeout(() => {<!-- -->
    test().then(res => console.log(`${<!-- -->res.name}-5`));
    test().then(res => console.log(`${<!-- -->res.name}-6`));
  }, 30000);
};

//Execute function
doing();

Conclusion

In the previous Token imperceptible refresh solution, we introduced the design ideas, implementation methods and precautions of the solution in detail. Through this method, we can realize automatic refresh of Token without affecting the user experience, therebyguaranteeing the security and stability of the system.
Of course, this solution also has somelimitations. For example, for some operations that require manual input of Token, this solution cannot implement automatic refresh. In addition, this solution may also cause some problems if the user logs in multiple times in a short period of time.
In response to these problems, we can take some additional measures to improve the program. For example, for operations that require manual input of a Token, we can automatically generate a Token when logging in and save it locally or in a database, and then automatically read and use it when an operation requires a Token. Token. In addition, we can also verify the Token when the user logs in. If it is found that the Token has expired or been tampered with, the user will be prompted to log in again and generate a new Token.
In general, the Token imperceptible refresh solution is a very practical technical means that can effectively ensure the security and stability of the system. Of course, we also need to continuously improve and improve it to adapt to changing application scenarios and needs.