searchusermenu
  • 发布文章
  • 消息中心
点赞
收藏
评论
分享
原创

前端实现用户无感知页面刷新

2023-05-04 08:52:02
48
0

需求

当前前后端普遍使用token进行鉴权,当token过期后,用户需要重新登录,输入用户名和密码以获取新的token。一般token过期时间设置很短,对于比较活跃的用户体验感非常差。那么如何解决这个问题呢?

这里引入两个新的名词access_token和refresh_token。access_token为授权令牌,用于验证用户身份,是在调用API时需要传入的header请求参数。当access_token过期后,需要刷新,但是每次刷新都需要填写用户名密码,非常繁琐。而refresh_token可以解决这个问题,顾名思义,refresh_token为刷新token,用来刷新access_token,无需用户进行附加操作。

  1. 当access_token过期,前端拿着refresh_token去获取新的access_token,再重新发起请求,此时需要做到用户无感知。
  2. 当用户同时发起多个请求时,第一个请求会去调用刷新token接口,当该接口还没返回时,其他的请求也去调用了刷新token接口,此时会产生多个请求,前端需要避免这种情况出现。

方案

利用axios的响应拦截器对返回的数据做处理。不需要解析access_token和refresh_token拿到各自的过期时间去做判断,但是需要多发送一次http请求。

实现

  • @/utils/cookies
import Cookies from 'js-cookie'
export default Cookies

// Token
const tokenKey = 'access_token'
export const getToken = (): string | undefined => Cookies.get(tokenKey)
export const setToken = (token: string): unknown => Cookies.set(tokenKey, token)
export const removeToken = (): unknown => Cookies.remove(tokenKey)

// refreshToken
const refreshTokenKey = 'refresh_token'
export const getRefreshToken = (): unknown => Cookies.get(refreshTokenKey)
export const setRefreshToken = (token: string): unknown => Cookies.set(refreshTokenKey, token)
export const removeRefreshToken = (): unknown => Cookies.remove(refreshTokenKey)

const usernameKey = 'username'
export const getUsername = (): unknown => Cookies.get(usernameKey)
export const setUsername = (token: string): unknown => Cookies.set(usernameKey, token)
export const removeUsername = (): unknown => Cookies.remove(usernameKey)

export const clearToken = (): void => {
  removeToken()
  removeRefreshToken()
  removeUsername()
}
  • @/utils/request.js
/** 创建axios实例 */
const service = axios.create({
  baseURL: settings.apiBaseUrl,
  timeout: 5 * 3600 * 1000,
})

/** 响应拦截器拦截器 */
service.interceptors.response.use(
  (response: BaseAxiosResponse) => {
    const { config, data } = response
    if (data.code === 0) {
      return Promise.resolve(response)
    } else if (data.code === 400005) {
    	// token 过期处理逻辑
    } else {
      return Promise.reject(response?.data)
    }
  },
  error => {
    return Promise.reject(error)
  }
)

响应拦截器改造。当返回的code值为400005时,表示access_token无效。此时调用刷新接口重新获取access_token,并重新发起原请求。

/** 创建axios实例 */
const service = axios.create({
  baseURL: settings.apiBaseUrl,
  timeout: 5 * 3600 * 1000,
})

/** 响应拦截器拦截器 */
service.interceptors.response.use(
  (response: BaseAxiosResponse) => {
    const { config, data } = response
    if (data.code === 0) {
      return Promise.resolve(response)
    } else if (data.code === 400005) {
    	// token 过期处理逻辑
      const token = getRefreshToken()
      return refreshToken({ oldRefreshToken: token })
        .then(res => {
          const { accessToken, refreshToken } = res.data
          setToken(accessToken)
          setRefreshToken(refreshToken)
          config.headers.Authorization = accessToken
          return service(config)
        })
        .catch(err => {
          console.log('抱歉,您的登录状态已失效,请重新登录!')
          return Promise.reject(err)
        })
    } else {
      // refreshToken 失效
      if (config.url.includes(REFRESH_URL)) {
        clearToken()
        router.push('/login').catch(err => {
          console.log(err)
        })
      }
      return Promise.reject(response?.data)
    }
  },
  error => {
    return Promise.reject(error)
  }
)

当用户同时发起多个请求时,可能存在多次调用刷新token的接口,因此需要定义一个标记来判断当前是否处于刷新状态,如果处于刷新状态,则禁止其他请求调用刷新接口。

/** 创建axios实例 */
const service = axios.create({
  baseURL: settings.apiBaseUrl,
  timeout: 5 * 3600 * 1000,
})

let isRefreshing = false // 标记是否正在刷新 token

/** 响应拦截器拦截器 */
service.interceptors.response.use(
  (response: BaseAxiosResponse) => {
    const { config, data } = response
    if (data.code === 0) {
      return Promise.resolve(response)
    } else if (data.code === 400005) {
    	// token 过期处理逻辑
      if (!isRefreshing) {
        isRefreshing = true
        const token = getRefreshToken()
        return refreshToken({ oldRefreshToken: token })
          .then(res => {
            const { accessToken, refreshToken } = res.data
            setToken(accessToken)
            setRefreshToken(refreshToken)
            config.headers.Authorization = accessToken
            return service(config)
          })
          .catch(err => {
            router.push('/login').catch(err => {
              console.log(err)
            })
            return Promise.reject(err)
          })
          .finally(() => {
            isRefreshing = false
          })
    } else {
      // refreshToken 失效
      if (config.url.includes(REFRESH_URL)) {
        clearToken()
        router.push('/login').catch(err => {
          console.log(err)
        })
      }
      return Promise.reject(response?.data)
    }
  },
  error => {
    return Promise.reject(error)
  }
)

上述同时发起多个请求的方法可以进一步优化。

当发起多个请求,第一个请求进入刷新token的流程,需要将其他请求挂起,当token更新之后在重新发起请求。这里定义一个requests数组,暂存挂起的请求。

/** 创建axios实例 */
const service = axios.create({
  baseURL: settings.apiBaseUrl,
  timeout: 5 * 3600 * 1000,
})

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

/** 响应拦截器拦截器 */
service.interceptors.response.use(
  (response: BaseAxiosResponse) => {
    const { config, data } = response
    if (data.code === 0) {
      return Promise.resolve(response)
    } else if (data.code === 400005) {
    	// token 过期处理逻辑
      if (!isRefreshing) {
        isRefreshing = true
        const token = getRefreshToken()
        return refreshToken({ oldRefreshToken: token })
          .then(res => {
            const { accessToken, refreshToken } = res.data
            setToken(accessToken)
            setRefreshToken(refreshToken)
            config.headers.Authorization = accessToken
            // token 刷新后将数组的方法重新执行
            requests.forEach(cb => cb(accessToken))
            return service(config)
          })
          .catch(err => {
            router.push('/login').catch(err => {
              console.log(err)
            })
            return Promise.reject(err)
          })
          .finally(() => {
            isRefreshing = false
          })
      }else {
        // 返回未执行 resolve 的 Promise
        return new Promise(resolve => {
          // 用函数形式将 resolve 存入,刷新token之后回调执行
          requests.push(token => {
            config.headers.Authorization = token
            resolve(service(config))
          })
        })
      }
    } else {
      // refreshToken 失效
      if (config.url.includes(REFRESH_URL)) {
        clearToken()
        router.push('/login').catch(err => {
          console.log(err)
        })
      }
      return Promise.reject(response?.data)
    }
  },
  error => {
    return Promise.reject(error)
  }
)
0条评论
0 / 1000
张****翔
3文章数
0粉丝数
张****翔
3 文章 | 0 粉丝