前端Token管理(获取、过期处理、异常处理及优化)

本文实例代码使用的是vue+axiosÏ

什么是Token

Token是服务端生成的一串字符串,以作客户端进行请求的一个令牌,当第一次登录后,服务器生成一个Token便将此Token返回给客户端,以后客户端只需带上这个Token前来请求数据即可,无需再次带上用户名和密码。

前端Token管理(获取、过期处理、异常处理及优化)_第1张图片

请求后台返回的登录数据一般情况如下

{
    access_token:"加密的字符串",
    expires_in:"7200",
    refresh_token:"加密的字符串",
}
  • access_token (访问令牌,用于资源访问)
  • refresh_token ( 当访问令牌失效,使用这个令牌重新获取访问令牌)
  • expire_in( access_tokenÏ过期时间)

基本使用

因为业务模式多种多样所以使用方法也是有很多的(ps:主要是后端想要什么,我们就给什么),比较常见的是Header中携带Token。

Token获取及使用

前端Token管理(获取、过期处理、异常处理及优化)_第2张图片

  • 接口封装
    /**
     * 用户请求模块
     */
    export const login = (dataÏ) => request({
      url: '/front/user/login',
      method: 'POST',
      data: qs.stringify(data)
    })
    
  • VueX基本使用
    export default new Vuex.Store({
      state: {
        // 初始化
        user: null
      },
      mutations: {
        // 设置用户登录信息
        setUser(state, payload) {
          //因目前后端返回的是json字符串,所以我转义了一下
          payload = JSON.parse(payload)
          //如果pyload中没有过期时间并且存在过期时间长度
          if (!payload.expires_at && payload.expires_in) {
            //设置过期时间
            payload.expires_at = new Date().getTime() + payload.expires_in * 1000
          }
          //赋值
          state.user = payload
        }
      },
      actions: {
      },
      modules: {
      }
    })
    
  • 登录页面使用(伪代码)
    import Vue from 'vue'
    import { login } from '@/services/user'
    export default Vue.extend({
      name: 'LoginIndex',
      data() {
        return {
          formData: {
            phone: '18201288771',
            password: '111111'
          }
        }
      },
      methods: {
        async submit() {
          try {
            const { data } = await login(this.formData)
            // 处理请求结果
            if (data.state !== 1) {
              this.$message.error(data.message)
            } else {
              // 使用vuex中的setUser共享登录信息
              this.$store.commit('setUser', data.content)
              this.$message.success('登录成功')
            }
          } catch (error) {}
          this.isLoading = false
        }
      }
    })
    
  • axios请求拦截器
    // 请求拦截器,每一个请求都会经过此拦截器。
    request.interceptors.request.use((config) => {
      // 在请求的header中设置token
      config.headers.Authorization = store.state?.user?.access_token
      return config
    }, (error) => {
      return Promise.reject(error)
    })
    

前端Token管理(获取、过期处理、异常处理及优化)_第3张图片

优化——授权过期登录重新返回页面

前端Token管理(获取、过期处理、异常处理及优化)_第4张图片

request.js中

// 跳转至首页封装
const redirectLogin = () => {
  router.push({
    name: 'login',
    query: {
      // 通过参数传 登录成功后的跳转地址
      redirect: router.currentRoute.fullPath
    }
  })
}

登录页面

 methods: {
    // 登录请求方法
    async submit() {
      try {
        const { data } = await login(this.formData)
        // 处理请求结果
        if (data.state !== 1) {
          //..... 登录失败处理逻辑
        } else {
          //..... 登录成功处理逻辑
          // 登录成功后进行路由跳转
          this.$router.push((this.$route.query.redirect as string) || '/')
        }
      } catch (error) {}
    }
}

优化——页面刷新Token丢失

export default new Vuex.Store({
  state: {
    // 初始化时从本地存储中获取
    user: JSON.parse(window.localStorage.getItem('user') || 'null')
  },
  mutations: {
    //设置用户登录信息
    setUser(state, payload) {
     //因目前后端返回的是json字符串,所以我转义了一下
      payload = JSON.parse(payload)
      //如果pyload中没有过期时间并且存在过期时间长度
      if (!payload.expires_at && payload.expires_in) {
        //设置过期时间
        payload.expires_at = new Date().getTime() + payload.expires_in * 1000
      }
      //赋值
      state.user = payload
      //每次设置用户登录信息都存储值本地存储
      window.localStorage.setItem('user', JSON.stringify(payload))
         }
  },
  actions: {
  },
  modules: {
  }
})

过期维护

过期维护存前端存在两种方式

  • 在请求发起前拦截每个请求,判断token的有效时间是否已经过期。若已过期,则将请求挂起,先刷新token后在继续请求。
    • 优点:请求前拦截,节省请求及流量
    • 缺点:需要后端额外提供过期时间字段,若本地时间与服务器时间不一致可能存在拦截失败。
  • 不在请求前拦截,而是拦截返回后的数据。先放弃请求,接口返回过期后,先刷新token,在进行一次重试。
    • 优点:不需要额外的token过期字段及判断时间
    • 缺点:会消耗多一次请求,耗流量

请求发起前拦截

前端Token管理(获取、过期处理、异常处理及优化)_第5张图片

// 跳转首页逻辑
const redirectLogin = () => {
  router.push({
    name: 'login',
    query: {
      redirect: router.currentRoute.fullPath
    }
  })
}

// 刷新token后的任务队列
let refreshTokenArray = []

/**
 * 刷新token,重新请求
 */
const refreshTokenFn = async () => {
  // 判断是否有刷新token
  const refreshToken = store.state?.user?.refresh_token || ''
  // 如果刷新token存在
  if (refreshToken) {
    // 使用重新创建的axios请求,防止递归调用
    const { data } = await axios.create()({
      method: 'POST',
      url: '/front/user/refresh_token',
      data: qs.stringify({
        refreshtoken: refreshToken
      })
    })
    //如果获取token失败 抛出异常
    if (!data.content) throw new Error('refreshToken is faild')
    // 重新设置token
    store.commit('setUser', data.content)
    return true
  }
  throw new Error('refreshToken not find')
}

// 请求拦截器
request.interceptors.request.use(async (config: Config) => {
  // 获取用户登录信息
  const user = store.state?.user
  // 判断access_token 是否过期且接口是否需要token
  if (config.isAuthToken && user.expires_at < new Date().getTime()) {
    // 是否正在执行刷新token
    if (!refreshTokenLoding) {
      try {
        //刷新token锁为true
        refreshTokenLoding = true
        await refreshTokenFn()
        // 执行获取token后的任务队列
        refreshTokenArray.forEach(item => item())
        //清空任务队列
        refreshTokenArray = []
        return config
      } catch (error) {
        // 如果刷新失败跳转登录页面
        redirectLogin()
      } finally {
        // 无论成功失败消除
        refreshTokenLoding = false
      }
    } else {
      // 如果这正在刷新,返回一个 Promise ,并向刷新token成功后执行队列push 函数.
      return new Promise(resolve => {
        refreshTokenArray.push(() => {
          // 返回config请求对象
          resolve(config)
        })
      })
    }
  }
  return config
})

请求发起后拦截

前端Token管理(获取、过期处理、异常处理及优化)_第6张图片

//相应拦截器
request.interceptors.response.use((response) => {
  // 2xx 会进入这里
  return response
}, async (error) => {
  // 判断是否是授权错误
  if (error.response === 401) {
    // 是否正在刷新
    if (!refreshTokenLoding) {
      refreshTokenLoding = true
      // 尝试使用 refresh_token 获取新的 access_token
      try {
        // 执行刷新token
        await refreshTokenFn()
        // 执行刷新后任务队列
        refreshTokenArray.forEach(item => item())
        //清除任务队列
        refreshTokenArray = []
        // 重发当前请求
        return request(error.config)
        // 如果成功 则重发上次请求
      } catch (error) {
        // 如果失败 跳转至登录
        redirectLogin()
      } finally {
        refreshTokenLoding = false
      }
    } else {
      // 如果当前正在请求
      return new Promise(resolve => {
        // 当前请求的config投递至刷新后的任务队列中
        refreshTokenArray.push(() => {
          resolve(request(error.config))
        })
      })
    }
  }
  //... 其他异常捕获
})

结束语

虽然token大家平常工作中都会使用,但是我见过太多的项目token使用上存在误区。例如为了避免token过期问题,让token的有效期为一周,还有些人甚至设置了一年(手动滑稽)。还有一些人只设置了拦截器,例如请求发现token过期或者后端返回了401,直接让用户跳转至登录页面,这样的用户体验真的很不优化。

代码地址: https://gitee.com/a20070322/edu-boss-fed

你可能感兴趣的:(杂项,javascript,typescript)