likes
comments
collection
share

【Taro】【微信小程序】token 无感刷新

作者站长头像
站长
· 阅读数 4

⌈本文是作者学习过程中的笔记总结,若文中有不正确或需要补充的地方,欢迎在评论区中留言⌋🤖

一、【实现思路】🚩

  1. 小程序端登录时,除了返回用户信息,还需返回两个 token 信息
    • accessToken:用于验证用户身份
    • refreshToken:用于刷新 accessToken
  2. 当请求返回状态码为401(即 accessToken 过期)时,使用 refreshToken 发起刷新请求
  3. 刷新成功会拿到新的 accessToken 和 refreshToken
  4. 然后使用新的 accessToken 将失败的请求重新发送

👉实现思路中的后三步,微信小程序中是在请求的 success 回调函数中做的处理,Taro 中则是设置了响应拦截器,在拦截器中做的对应处理,本文仅讨论有区别的这部分

二、【前端代码】🚩

1. ⌈封装的请求方法⌋🍥

  • 接收5个参数:url、method、params、needToken、header(点击展开)
    • url: 请求地址,是部分地址(例如:/auth/login),后面处理时会将其与设置的 baseUrl(例如:http://localhost:4000/api/v1) 进行拼接
    • method:请求方法,默认值为 'GET'
    • params:请求参数,数据格式为 object,例如: {name: 'test'}
    • needToken:是否需要需要携带 token(即是否需要身份验证),默认值为 false
    • header:请求头信息,数据格式为 object(例如: {'Content-Type': 'application/json'}),默认值为 null
  • 需要携带 token 的请求,先从本地存储中取出 accessToken 信息,然后将其赋值给 header 中的 Authorization 属性(注意:首字母要大写)。后端接口在验证 token 时,会根据 req.headers.authorization.split(' ')[1] 获取到请求头中传递的 accessToken 信息

  • 先通过 Taro.addInterceptor 设置拦截器,然后调用 Taro.request 发送请求。这样的话,当请求真正发送之前以及获取到响应信息时,都会先进入到拦截器中,我们就是在这里进行的 token 刷新操作

  • 具体代码(点击展开)
    import Taro from '@tarojs/taro'
    import { baseUrl } from '../config'
    import responseInterceptor from '../http/interceptors'
    
    // 添加拦截器
    Taro.addInterceptor(responseInterceptor)
    
    // 封装的请求方法
    const request = (url, method = 'GET', params = {}, needToken = false, header = null) => {
      const {contentType = 'application/json'} = header || {}
      if (url.indexOf(baseUrl) === -1) url = baseUrl + url
    
      const option = {
        url,
        method,
        data: method === 'GET' ? {} : params,
        header: {'Content-Type': contentType}
      }
    
      // 处理 token
      if (needToken) {
        const token = Taro.getStorageSync('accessToken')
    
        if (token) {
          option['header']['Authorization'] = token
        } else {
          Taro.setStorageSync('profile', null)
          Taro.showToast({
            title: '请登录',
            icon: 'error',
            duration: 2000
          })
    
          return
        }
      }
    
      // 发起请求
      return Taro.request(option)
    }
    
    export default request
    
    

2. ⌈拦截器⌋🍥

拦截器是一个函数,接受 chain 对象作为参数。chain 对象中含有 requestParams 属性,代表请求参数。拦截器最后需要调用 chain.proceed(requestParams) 以调用下一个拦截器或者发起请求。Taro 中的这个拦截器没有请求拦截器和响应拦截器之分,具体看你是在调用 chain.proceed(requestParams) 之前还是之后做的操作。具体说明可查阅官方文档

  • 拦截器中先调用 chain.proceed(requestParams) 发送请求,其返回的是一个 promise 对象,所以可以在 .then 中做响应处理

  • .then 中先判断响应状态码,这里我们只讨论 401 token 过期的情况

  • 当 token 过期时,获取本地存储的 refreshToken,然后调用对应后端接口刷新 token(点击展开)
    • 在刷新请求发送前,需要先判断是否已经有刷新请求被发送且正在处理中(基于 isTokenRefreshing 标识)
    • 如果有,则不必再重复发送刷新请求,但是需要把本次因为 401 token 过期而导致失败的请求存起来(放入 failedRequests 数组中),等待当前正在处理的 token 刷新请求完成后,使用新的 accessToken 重新发送本次请求
    • 如果没有,则发送刷新请求,同时修改 isTokenRefreshing 标识的值为 true
  • 等待刷新请求完成,将返回的新 accessToken 和 refreshToken 存储起来

  • 然后将 failedRequests 中因为等待 token 刷新而存储起来的失败请求,基于新的 accessToken 重新发送

  • 最后将本次因为 401 token过期导致失败的请求,基于新的 accessToken 重新发送(点击展开)
    • 本次操作正常进行 token 刷新请求,说明本次请求也是 token 过期了,而且因为 isTokenRefreshing 标识为 false, 没有将本次失败的请求存入 failedRequests 中
  • 具体代码(点击展开)
    import Taro from '@tarojs/taro'
    import { statusCode } from '../config'
    import request from './request'
    
    // 标识 token 刷新状态
    let isTokenRefreshing = false
    
    // 存储因为等待 token 刷新而挂起的请求
    let failedRequests = []
    
    // 设置响应拦截器
    const responseInterceptor = chain => {
      // 先获取到本次请求的参数,后面会使用到
      let {requestParams} = chain
    
      // 发起请求,然后进行响应处理
      return chain.proceed(requestParams)
        .then(res => {
          switch (res.statusCode) {
            // 404
            case statusCode.NOT_FOUND:
              return Promise.reject({message: '请求资源不存在'})
            // 502
            case statusCode.BAD_GATEWAY:
              return Promise.reject({message: '服务端出现了问题'})
            // 403
            case statusCode.FORBIDDEN:
              return Promise.reject({message: '没有权限访问'})
            // 401
            case statusCode.AUTHENTICATE:
              // 获取 refreshToken 发送请求刷新 token
              // 刷新请求发送前,先判断是否有已发送的请求,如果有就挂起,如果没有就发送请求
              if (isTokenRefreshing) {
                const {url: u, method, params, header} = requestParams
                return failedRequests.push(() => request(u, method, params, true, header))
              }
    
              isTokenRefreshing = true
              const url = '/auth/refresh-token'
              const refreshToken = Taro.getStorageSync('refreshToken')
              return request(url, 'POST', {refreshToken}, false)
                .then(response => {
                  // 刷新成功,将新的 accesToken 和 refreshToken 存储到本地
                  Taro.setStorageSync('accessToken', `Bearer ${response.accessToken}`)
                  Taro.setStorageSync('refreshToken', response.refreshToken)
    
                  // 将 failedRequests 中的请求使用刷新后的 accessToken 重新发送
                  failedRequests.forEach(callback => callback())
                  failedRequests = []
    
                  // 再将之前报 401 错误的请求重新发送
                  const {url: u, method, params, header} = requestParams
                  return request(u, method, params, true, header)
                })
                .catch(err => Promise.reject(err))
                .finally(() => {
                  // 无论刷新是否成功,都需要将 isTokenRefreshing 重置为 false
                  isTokenRefreshing = false
                })
            // 500
            case statusCode.SERVER_ERROR:
              // 刷新 token 失败
              if (res.data.message === 'Failed to refresh token') {
                Taro.setStorageSync('profile', null)
                Taro.showToast({
                  title: '请登录',
                  icon: 'error',
                  duration: 2000
                })
                return Promise.reject({message: '请登录'})
              }
    
              // 其他问题导致失败
              return Promise.reject({message: '服务器错误'})
            // 200
            case statusCode.SUCCESS:
              return res.data
            // default
            default:
              return Promise.reject({message: ''})
          }
        })
        .catch(error => {
          console.log('网络请求异常', error, requestParams)
          return Promise.reject(error)
        })
    }
    
    export default responseInterceptor
    
    

【源码】🚩

【说明】🚩

  • 文中涉及到的代码都是作者本人的书写习惯与风格,若有不合理的地方,欢迎指出

  • 有不清楚的内容,欢迎在评论区中讨论,也可加入作者的学习群一起交流学习(点击展开) 【Taro】【微信小程序】token 无感刷新
  • 如果本文对您有帮助,烦请动动小手点个赞,谢谢
转载自:https://juejin.cn/post/7300592516759306291
评论
请登录