Encapsulate the axios interceptor to achieve user-insensitive refresh access_token

foreword

When working on a project recently, it involved a single sign-on, that is, the login page of the project. It used a login page shared by the company, and the logic was unified on this page. In the end, users only need to log in once, and they can visit all the websites of the company in the logged-in state

Single sign-on (Single Sign On,
SSO for short) is one of the more popular enterprise business integration solutions at present. It is used between multiple application systems. Users only need to log in once to access all mutually trusted application systems.

Among them, this article talks about how to manage access_token and refresh_token after login, mainly to encapsulate the axios interceptor, which is recorded here.

need

  • Pre-scenario
    insert image description here
    1. To enter a certain page of the project http://xxxx.project.com/profile, you need to log in. If you are not logged in, you will be redirected to the SSO login platform. The login URL at this time is http://xxxxx.com/ login?app_id=project_name_id&redirect_url=http://xxxx.project.com/profile, where app_id is defined by agreement on the background side, and redirect_url is the callback address specified after successful authorization.
    2. After entering the account password and it is correct, it will be redirected back to the page you entered at the beginning, and a parameter?code=XXXXX in the address bar, which is http://xxxx.project.com/profile?code=XXXXXX, The code value is invalid after being used once, and expires within 10 minutes.
    3. Obtain the code value immediately and then request an api /access_token/authenticate with the parameter { verify_code: code }, and the api has its own app_id and app_secret A fixed value parameter, through which to request the authorized API, and get the return value after the request is successful { access_token: "xxxxxxx", refresh_token: "xxxxxxxx", expires_in: xxxxxxxx }, save the access_token and refresh_token in the cookie (localStorage is also available) , at this point the user is logged in successfully.
    4. The access_token is in the standard JWT format. It is an authorization token. It can be understood that it is used to verify the identity of the user. It is a parameter that must be passed in when the application calls the API to access and modify user data (put it in the headers of the request), and expires after 2 hours. That is to say, after completing the first three steps, you can call the APIs that require the user to log in; but if you do nothing, after two hours quietly, and then request these APIs, the access_token will be reported as expired. Call failed.
    5. Then you can’t let the user log out after 2 hours. The solution is to take the expired access_token and refresh_token (the refresh_token expiration time is generally longer, such as one month or longer) to request /refresh api after two hours, and return The result is { access_token: “xxxxx”, expires_in: xxxxx }, exchange for a new access_token, the expiration time of the new access_token is also 2 hours, and save it in the cookie again, continue to keep logging in and call the user api in a cycle. Refresh_token can continue to exchange for a new access_token next time within a limited expiration time (such as one week or one month, etc.), but after the limited time, even if the real meaning has expired, you have to re-enter the account password to log in.

The company website login expiration time is only two hours (token expiration time), but we want users who are active within a month not to log in again, so there is such a need, which prevents users from entering their account passwords to log in again.
Why use a refresh_token to update the access_token? First of all, access_token will be associated with certain user permissions. If the user authorization is changed, this access_token also needs to be refreshed to associate new permissions. If there is no refresh_token, the access_token can also be refreshed, but the user must enter the login user name and password every time it is refreshed. , what a hassle. With the refresh_token, this trouble can be reduced. The client directly uses the refresh_token to update the access_token without additional operations by the user.
Having said so much, some people may complain that an access_token is enough for a login, and it is so troublesome to add a refresh_token, or some companies refresh_token is arranged by the background and does not require front-end processing. However, the pre-scene is there, and the requirements are based on this scene.

  • need

1. When the access_token expires, you need to use refresh_token to request a new access_token. The front end needs to refresh the access_token without the user's perception. For example, when a user initiates a request, if it is judged that the access_token has expired, then it must first call the refresh token interface to obtain a new access_token, and then re-initiate the user request.
2. If multiple user requests are initiated at the same time, the first user request calls the refresh token interface. When the interface has not returned, the rest of the user requests still initiate refresh token interface requests, resulting in multiple requests. These requests How to deal with it is the content of our article.

Idea
Option 1

Written in the request interceptor, before the request, first use the expires_in field returned by the initial request to determine whether the access_token has expired, if it has expired, the request will be suspended, and the access_token will be refreshed before continuing the request.
Advantages: Can save http requests
Disadvantages: Because the local time is used to judge, if the local time is tampered with, there is a risk of verification failure

Option II

Write it in the response interceptor to intercept the returned data. Initiate a user request first, if the interface returns that the access_token expires, refresh the access_token first, and then try again.
Advantages: No need to judge time
Disadvantages: It will consume one more http request
Here I choose option two.

accomplish

Axios is used here, which is intercepted after the request, so the axios response interceptor axios.interceptors.response.use() method is used.
Method introduction

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

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

export const getToken = () => 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)
}
  • request.js
import axios from 'axios'
import {
    
     getToken, setToken, getRefreshToken } from '@utils/auth'

// 刷新 access_token 的接口
const refreshToken = () => {
    
    
  return instance.post('/auth/refresh', {
    
     refresh_token: getRefreshToken() }, true)
}

// 创建 axios 实例
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 过期或无效,返回 401 状态码,在此处理逻辑
    return Promise.reject(error)
})

// 给请求头添加 access_token
const setHeaderToken = (isNeedToken) => {
    
    
  const accessToken = isNeedToken ? getToken() : null
  if (isNeedToken) {
    
     // api 请求需要携带 access_token 
    if (!accessToken) {
    
     
      console.log('不存在 access_token 则跳转回登录页')
    }
    instance.defaults.headers.common.Authorization = `Bearer ${
      
      accessToken}`
  }
}

// 有些 api 并不需要用户授权使用,则不携带 access_token;默认不携带,需要传则设置第三个参数为 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,
  })
}

Next, modify the response interceptor of 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('抱歉,您的登录状态已失效,请重新登录!')
            return Promise.reject(err)
        })
    }
    return Promise.reject(error)
})

It is agreed to return a 401 status code to indicate that the access_token has expired or is invalid. If the user initiates a request and the return result is that the access_token is expired, then request to refresh the access_token interface. If the request is successful, enter then, reset the configuration, refresh the access_token and re-initiate the original request.

But if the refresh_token also expires, the request also returns 401. At this time, the debugger will find that the function cannot enter the catch of refreshToken(). That is because the same instance instance is used inside the refreshToken() method to repeat the processing logic of the response interceptor 401, but the function itself refreshes the access_token. Therefore, the interface needs to be excluded, namely:

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

The above code has already achieved a senseless refreshing of the access_token. When the access_token has not expired, it will return normally;

Optimization - prevent token from being refreshed multiple times

If the token is expired, there is also a certain time interval for the interface requesting to refresh the access_token to return. If there are other requests sent at this time, the interface for refreshing the access_token will be executed again, resulting in multiple refreshes of the access_token.
Therefore, we need to make a judgment, define a flag to judge whether it is currently in the state of refreshing access_token, if it is in the state of refreshing, no other requests are allowed to call this interface.

let isRefreshing = false // 标记是否正在刷新 token
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('抱歉,您的登录状态已失效,请重新登录!')
                return Promise.reject(err)
            }).finally(() => {
    
    
                isRefreshing = false
            })
        }
    }
    return Promise.reject(error)
})

The above method of initiating multiple requests
at the same time is not enough, because if multiple requests are initiated at the same time, when the token expires, the first request enters the refresh token method, and other requests enter without any logical processing, simply return failure, and finally Only the first request is executed, which obviously doesn't make sense.
For example, three requests are initiated at the same time, the first request enters the process of refreshing the token, the second and third requests need to be saved, and the request is re-initiated after the token is updated.
Here, we define an array requests to save the waiting requests, and then return a Promise. As long as the resolve method is not called, the request will be in the waiting state, so we can know that the array actually stores functions; wait until the token is updated , execute the function through the array loop, that is, execute the resolve resend request one by one.

let isRefreshing = false // 标记是否正在刷新 token
let requests = [] // 存储待重发请求的数组

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}`
                // token 刷新后将数组的方法重新执行
                requests.forEach((cb) => cb(access_token))
                requests = [] // 重新请求完清空
                return instance(config)
            }).catch(err => {
    
    
                console.log('抱歉,您的登录状态已失效,请重新登录!')
                return Promise.reject(err)
            }).finally(() => {
    
    
                isRefreshing = false
            })
        } else {
    
    
            // 返回未执行 resolve 的 Promise
            return new Promise(resolve => {
    
    
                // 用函数形式将 resolve 存入,等待刷新后再执行
                requests.push(token => {
    
    
                    config.headers.Authorization = `Bearer ${
      
      token}`
                    resolve(instance(config))
                })  
            })
        }
    }
    return Promise.reject(error)
})

Final request.js code

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

// 刷新 access_token 的接口
const refreshToken = () => {
    
    
  return instance.post('/auth/refresh', {
    
     refresh_token: getRefreshToken() }, true)
}

// 创建 axios 实例
const instance = axios.create({
    
    
  baseURL:  process.env.GATSBY_API_URL,
  timeout: 30000,
  headers: {
    
    
    'Content-Type': 'application/json',
  }
})

let isRefreshing = false // 标记是否正在刷新 token
let requests = [] // 存储待重发请求的数组

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}`
                // token 刷新后将数组的方法重新执行
                requests.forEach((cb) => cb(access_token))
                requests = [] // 重新请求完清空
                return instance(config)
            }).catch(err => {
    
    
                console.log('抱歉,您的登录状态已失效,请重新登录!')
                return Promise.reject(err)
            }).finally(() => {
    
    
                isRefreshing = false
            })
        } else {
    
    
            // 返回未执行 resolve 的 Promise
            return new Promise(resolve => {
    
    
                // 用函数形式将 resolve 存入,等待刷新后再执行
                requests.push(token => {
    
    
                    config.headers.Authorization = `Bearer ${
      
      token}`
                    resolve(instance(config))
                })  
            })
        }
    }
    return Promise.reject(error)
})

// 给请求头添加 access_token
const setHeaderToken = (isNeedToken) => {
    
    
  const accessToken = isNeedToken ? getToken() : null
  if (isNeedToken) {
    
     // api 请求需要携带 access_token 
    if (!accessToken) {
    
     
      console.log('不存在 access_token 则跳转回登录页')
    }
    instance.defaults.headers.common.Authorization = `Bearer ${
      
      accessToken}`
  }
}

// 有些 api 并不需要用户授权使用,则无需携带 access_token;默认不携带,需要传则设置第三个参数为 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,
  })
}

おすすめ

転載: blog.csdn.net/zhengcaocao/article/details/127089787