preface

A recent project involved a single sign-on (SSO), that is, the project’s login page, using a common login page for the company and unifying the processing logic on that page. Finally, users only need to log in once, so that they can access all the websites owned by the company.

Single sign-on (SSO) is one of the most popular solutions for enterprise business integration. It is used between multiple application systems. Users only need to log in once to access all trusted application systems.

This article describes how to manage access_token and refresh_token after login, mainly encapsulating the AXIOS interceptor, which is documented here.

demand

  • Pre scenario

  1. Enter the project need to log in to http://xxxx.project.com/profile, a page without login to jump to the SSO login platform, At this time login url for http://xxxxx.com/login?app_id=project_name_id&redirect_url=http://xxxx.project.com/profile, App_id is defined by the convention on the background side, and redirect_URL is the callback address specified after successful authorization.

  2. Once you have entered your account password correctly, you will be redirected to the page you started with, with a parameter in the address bar. Code = XXXXX, is http://xxxx.project.com/profile?code=XXXXXX, the value of the code is the use of an invalid, namely after expiration and 10 minutes

  3. Get the code immediately and request an API /access_token/authenticate with {verify_code: Code}, and the API has two fixed values of app_id and app_secret, through which to request the authorization API, the return value is {access_token: “XXXXXXX “, refresh_token: “XXXXXXXX “, expires_in: XXXXXXXX}, save the access_token and refresh_token in the cookie (localStorage also works), then the user can log in successfully.

  4. Access_token is a standard JWT authorization token that authenticates the user’s identity. It is a parameter that must be passed by an application to invoke API to access or modify user data. It expires after 2 hours. That is, after the first three steps, you can call an API that requires the user to log in. However, if you do nothing and then request these apis two hours later, the access_token will expire and the call will fail.

  5. The solution is to request the refresh API after 2 hours with the expired access_token and refresh_token (refresh_token usually expires after a longer time, such as a month or more). The return result is {access_token: “XXXXX “, expires_in: Access_token = access_token; access_token = access_token; access_token = access_token; Refresh_token can be exchanged for a new Access_token within the specified expiration time (for example, one week or one month). However, after the expiration time, you need to re-enter the account password to log in.

The login expiration time of the company website is only two hours (token expiration time), but the company also wants to prevent frequent active users from logging in again, so that users do not need to re-enter their accounts and passwords.

Why update access_token with a refresh_token? First of all, access_token is associated with certain user permissions. If the user authorization is changed, the access_token also needs to be refreshed to associate with new permissions. If there is no refresh_token, the Access_token can also be refreshed. But every time refresh to the user input login user name and password, much trouble. With refresh_token, this hassle can be reduced, and the client can update the Access_token directly with refresh_Token without any additional action by the user.

Having said so much, some people may joke that it is so troublesome for a login to use access_token and add refresh_token, or some companies can arrange refresh_token in the background and do not need front-end processing. However, the front end scenario is there, and the requirements are based on that scenario.

  • demand
  1. When the Access_Token expires, refresh_Token needs to be used to obtain a new access_token, and the front-end needs to refresh the Access_token without the user’s awareness. For example, when a user initiates a request, if the access_token has expired, the user should call the refresh token interface to get the new access_token, and then initiate the request again.

  2. If multiple user requests are made at the same time, the first user requests to invoke the refresh token interface. When the interface has not returned, the rest of the user requests also initiate the refresh token interface request, which will result in multiple requests. How to handle these requests is the content of this article.

Train of thought

Plan a

Write in the request interceptor, before the request, first use the original request return field expires_in field to determine whether the access_token has expired, if the expiration, the request will be suspended, first refresh the access_token before continuing the request.

  • Advantages: Saves HTTP requests
  • Disadvantages: Because the local time is used, the verification may fail if the local time is tampered

Scheme 2

Write in the response interceptor to intercept the returned data. If the interface returns an expired access_Token, refresh the access_token and try again.

  • Advantages: No need to judge the time
  • Disadvantages: Consumes one more HTTP request

Here I choose plan two.

implementation

Used here axios which do is request to intercept, so use the axios response of the interceptor axios. Interceptors. Response. Use () method

Methods to introduce

  • @utils/auth.js
import Cookies from 'js-cookie'

const TOKEN_KEY = 'access_token'
const REGRESH_TOKEN_KEY = 'refresh_token'

export const getToken = (a)= > Cookies.get(TOKEN_KEY)  export const setToken = (token, params = {}) = > {  Cookies.set(TOKEN_KEY, token, params) }  export const setRefreshToken = (token) = > {  Cookies.set(REGRESH_TOKEN_KEY, token) } Copy the code
  • request.js
import axios from 'axios'
import { getToken, setToken, getRefreshToken } from '@utils/auth'

Refresh the access_token interface
const refreshToken = (a)= > {
 return instance.post('/auth/refresh', { refresh_token: getRefreshToken() }, true) }  // Create an axios instance const instance = axios.create({  baseURL: process.env.GATSBY_API_URL,  timeout: 30000. headers: {  'Content-Type': 'application/json'. } })  instance.interceptors.response.use(response= > {  return response }, error => {  if(! error.response) { return Promise.reject(error)  }  // Token expired or invalid, return 401 status code, handle logic here  return Promise.reject(error) })  // Add access_token to the request header const setHeaderToken = (isNeedToken) = > {  const accessToken = isNeedToken ? getToken() : null  if (isNeedToken) { // API requests need to carry an access_token  if(! accessToken) { console.log('Skip back to login page if access_token is not present')  }  instance.defaults.headers.common.Authorization = `Bearer ${accessToken}`  } }  // Some apis do not require user authorization, so they do not carry access_token; This parameter is not carried by default. If transmission is required, set the third parameter to true export const get = (url, params = {}, isNeedToken = false) = > { setHeaderToken(isNeedToken)  return instance({  method: 'get'. url,  params,  }) }  export const post = (url, params = {}, isNeedToken = false) = > {  setHeaderToken(isNeedToken)  return instance({  method: 'post'. url,  data: params,  }) } Copy the code

Next, modify the response interceptor for AXIos in Request.js

instance.interceptors.response.use(response => {
    return response
}, error => {
    if(! error.response) {        return Promise.reject(error)
 }  if (error.response.status === 401) {  const { config } = error  return refreshToken().then(res=> {  const { access_token } = res.data  setToken(access_token)  config.headers.Authorization = `Bearer ${access_token}`  return instance(config)  }).catch(err => {  console.log('Sorry, your login status has expired, please log in again! ')  return Promise.reject(err)  })  }  return Promise.reject(error) }) Copy the code

By convention, returning the 401 status code indicates that the access_Token is expired or invalid. If the result of a request is expired, the user requests to refresh the access_token interface. If the request succeeds, enter then, reset the configuration, refresh the access_token, and re-initiate the original request.

But if refresh_token is also expired, the request also returns 401. The function refreshToken() is not included in the catch of the refreshToken() function, because the refreshToken() method uses the same instance repeatedly in response to the logic of the interceptor 401, but it refreshes the access_token itself. Therefore, the interface needs to be excluded, that is:

if (error.response.status === 401 && !error.config.url.includes('/auth/refresh')) {}
Copy the code

If the access_token has not expired, it will return normally. When it expires, AXIOS does an internal refresh of the token and re-initiates the original request.

To optimize the

Prevents multiple token refresh

If the token is expired, the interface that requests to refresh the access_token returns an interval of time. If another request is sent at this time, the interface to refresh the access_token is refreshed again, resulting in multiple refreshes of the Access_token. Therefore, we need to make a judgment and define a flag to determine whether the access_token is currently in the refreshed state. If it is in the refreshed state, no other requests will be allowed to invoke the interface.

let isRefreshing = false // Flags whether tokens are being refreshed
instance.interceptors.response.use(response= > {
    return response
}, error => {
    if(! error.response) { return Promise.reject(error)  }  if (error.response.status === 401 && !error.config.url.includes('/auth/refresh')) {  const { config } = error  if(! isRefreshing) { isRefreshing = true  return refreshToken().then(res= > {  const { access_token } = res.data  setToken(access_token)  config.headers.Authorization = `Bearer ${access_token}`  return instance(config)  }).catch(err= > {  console.log('Sorry, your login status has expired, please log in again! ')  return Promise.reject(err)  }).finally((a)= > {  isRefreshing = false  })  }  }  return Promise.reject(error) }) Copy the code

The process of initiating multiple requests simultaneously

The above approach is not enough, because if multiple requests are launched at the same time, and the token expires, the first request enters the refreshing token method, the other requests do not do any logical processing, simply return failure, and finally only execute the first request, which is obviously unreasonable.

For example, three requests are made at the same time. The first request enters the process of refreshing the token, and the second and third requests are saved until the token is updated.

Here, we define an array of requests to hold pending requests, and then return a Promise. As long as the resolve method is not called, the request will be in wait state. Once the token is updated, the function is looped through the array, executing the resolve reissue requests one by one.

let isRefreshing = false // Flags whether tokens are being refreshed
let requests = [] // An array of requests to resend

instance.interceptors.response.use(response= > {
    return response
}, error => {  if(! error.response) { return Promise.reject(error)  }  if (error.response.status === 401 && !error.config.url.includes('/auth/refresh')) {  const { config } = error  if(! isRefreshing) { isRefreshing = true  return refreshToken().then(res= > {  const { access_token } = res.data  setToken(access_token)  config.headers.Authorization = `Bearer ${access_token}`  // Execute the array method again after token refresh  requests.forEach((cb) = > cb(access_token))  requests = [] // The request is cleared again  return instance(config)  }).catch(err= > {  console.log('Sorry, your login status has expired, please log in again! ')  return Promise.reject(err)  }).finally((a)= > {  isRefreshing = false  })  } else {  // Return the Promise that resolve is not executed  return new Promise(resolve= > {  // Store resolve as a function and wait to refresh  requests.push(token= > {  config.headers.Authorization = `Bearer ${token}`  resolve(instance(config))  })  })  }  }  return Promise.reject(error) }) Copy the code

The final request.js code

import axios from 'axios'
import { getToken, setToken, getRefreshToken } from '@utils/auth'

Refresh the access_token interface
const refreshToken = (a)= > {
 return instance.post('/auth/refresh', { refresh_token: getRefreshToken() }, true) }  // Create an axios instance const instance = axios.create({  baseURL: process.env.GATSBY_API_URL,  timeout: 30000. headers: {  'Content-Type': 'application/json'. } })  let isRefreshing = false // Flags whether tokens are being refreshed let requests = [] // An array of requests to resend  instance.interceptors.response.use(response= > {  return response }, error => {  if(! error.response) { return Promise.reject(error)  }  if (error.response.status === 401 && !error.config.url.includes('/auth/refresh')) {  const { config } = error  if(! isRefreshing) { isRefreshing = true  return refreshToken().then(res= > {  const { access_token } = res.data  setToken(access_token)  config.headers.Authorization = `Bearer ${access_token}`  // Execute the array method again after token refresh  requests.forEach((cb) = > cb(access_token))  requests = [] // The request is cleared again  return instance(config)  }).catch(err= > {  console.log('Sorry, your login status has expired, please log in again! ')  return Promise.reject(err)  }).finally((a)= > {  isRefreshing = false  })  } else {  // Return the Promise that resolve is not executed  return new Promise(resolve= > {  // Store resolve as a function and wait to refresh  requests.push(token= > {  config.headers.Authorization = `Bearer ${token}`  resolve(instance(config))  })  })  }  }  return Promise.reject(error) })  // Add access_token to the request header const setHeaderToken = (isNeedToken) = > {  const accessToken = isNeedToken ? getToken() : null  if (isNeedToken) { // API requests need to carry an access_token  if(! accessToken) { console.log('Skip back to login page if access_token is not present')  }  instance.defaults.headers.common.Authorization = `Bearer ${accessToken}`  } }  // Some apis do not require user authorization, so they do not need to carry an access_token; This parameter is not carried by default. If transmission is required, set the third parameter to true export const get = (url, params = {}, isNeedToken = false) = > { setHeaderToken(isNeedToken)  return instance({  method: 'get'. url,  params,  }) }  export const post = (url, params = {}, isNeedToken = false) = > {  setHeaderToken(isNeedToken)  return instance({  method: 'post'. url,  data: params,  }) } Copy the code


Reference article:

  • Juejin. Cn/post / 684490…


  • Ps: Personal technical blog Github warehouse, if you feel good welcome star, give me a little encouragement to continue writing ~