前端自动刷新令牌

前端自动刷新令牌

前言

1. 技术选型

咱们在实际项目中选用了JWT这种认证方式.前端

  1. 简单了解JWTios

    JWTJson Web Token, 用户登录后, 将非私密用户信息放置在token中携带给前端并加密, 以后每一笔请求携带token, 后端解密token便可取得用户信息axios

    最大好处: 后端无状态, 能够平滑横向扩张, 且token较难解密后端

    最大弊端: 后端较难控制token失效跨域

    更多详情能够在掘金进行搜索: JWT浏览器

  2. 是否须要前端刷新令牌?并发

    其实并非必定的测试

    • 当前端后端同域下, 选用Cookie+HttpOnly进行令牌传递, 可让前端无需操做任何令牌, 当令牌过时后主动由后端刷新并放回Cookie便可
    • (咱们项目)当跨域状况下, (实际测试中)后端在Set-Cookie时, Chrome浏览器会发生没法正常处理Cookie的问题, 所以放弃Cookie传递, 使用请求头携带token

      第一张图是Chrome, 第二张图是FireFox, 相同请求网站

    Chrome

    FireFox

    • 所以选用localStorage存储token, 由前端处理放在请求头中进行认证ui

    • 最后一个问题: 因为token有效期短, 须要有人刷新token

    小插曲: 其实能够交给后端刷新令牌, 当token过时后, 后端刷新而后放回请求头, 前端主动根据返回请求头进行更新便可. 问题在后端遇到并发时token会混乱.

    最终缘由由前端刷新: 吵不事后端, 只能接下需求_(:з」∠)_

  3. http请求插件 不过多介绍, 选用了axios

  4. axios使用介绍 主要使用了拦截器interceptors, 因为使用了Promise, 能够随意在请求先后进行各类延时操做.

功能点介绍

  1. 登录后将登录token储存

  2. axios请求前将全部token放入请求头

  3. 退出登陆时清空token存储

  4. 当任意接口返回401时, 尝试刷新token, 若成功, 则更新token储存, 若失败则跳转登录

  5. 因为并发的存在, 须要考虑如下状况:

    • 发起一笔请求时, 若已经在尝试刷新token, 将此请求拦截, 并在成功刷新后, 更新请求头并从新发送
    • 当收到一笔401请求时, 若已经在尝试刷新token, 将此请求拦截, 并在成功刷新后, 更新请求头并从新发送
  6. 加载页面时, 尝试加载token

转化为代码逻辑

  1. 登陆后,将token存储进localStorage, 并更新全部axios实例的默认请求头

  2. null

  3. 退出登陆时, 将localStoragetoken清除, 并清除全部axios实例的默认请求头

  4. axios请求失败且状态码为401

    • 拦截该笔请求, 并注册一个刷新Token完成事件
      • 当触发token刷新成功时, 将该笔请求更新token, 而后从新发送
      • 当触发token刷新失败时, 将该笔请求返回失败, 交由Promise:reject进一步处理
    • 在没有其余刷新token请求时, 尝试刷新token, 并在刷新后触发刷新Token完成事件, 若刷新成功, 则将token存储进localStorage, 并更新全部axios实例的默认请求头
  5. axios请求前, 检查是否正在刷新token, 若正在刷新token, 注册一个刷新Token完成事件

    • 当触发token刷新成功时, 将该笔请求更新token, 而后继续发送
    • 当触发token刷新失败时, 将该笔请求返回失败, 抛弃请求
  6. 加载页面时, 从localStorage中获取token

实际代码

  1. 私有全局变量

    • isRefreshing: Boolean 是否正在刷新token
    • RefreshEvent: EventEmitter 刷新事件分发器
    • instances: AxiosInstance[] 全部封装好的axios实例,

      注意最好默认包含Axios即默认实例

    • REFRESH_URL: String 刷新URL
  2. 封装方法

    • 设置并返回token
    function setRefreshToken(refreshToken) {
      if (refreshToken !== undefined) {
        // 若携带参数, 则塞入localStorage中更新
        localStorage.setItem('ompJwtRefreshToken', refreshToken)
      } else {
        // 若没有携带参数, 则从localStorage中加载, 注意防范XSS攻击
        refreshToken = (
          localStorage.getItem('ompJwtRefreshToken', refreshToken) || ''
        ).replace(/[^.\-_a-zA-Z0-9]/g, '')
      }
      // 这里instances包含全部axios实例
      instances.forEach(instance => {
        // 设置默认请求头
        instance.defaults.headers['x-client-refresh-token'] = refreshToken
      })
      return refreshToken
    }
    复制代码
    • 刷新token

    插播小广告: 个人掘金主页

    function tryToRefreshToken() {
      // 约全局变量isRefreshing: 是否正在刷新token
      isRefreshing = true
      let refreshToken = localStorage.getItem('ompJwtRefreshToken') || ''
      refreshToken = refreshToken.replace(/[^.\-_a-zA-Z0-9]/g, '')
      // 没有refreshToken是没法刷新token的, 直接失败
      // 这里使用事件分发机制处理, 返回false表示刷新失败
      if (!refreshToken) return RefreshEvent.emit('refreshEnd', false)
      // ^_^ 友好提示, 防止用户觉得刷新中点击按钮没有反应
      Notice.open({
        title: '正在主动刷新, 尝试继续登录中……',
        duration: 0,
        name: 'refresh'
      })
      axios
        .get(REFRESH_URL)
        .then(res => {
          // 刷新成功
          Notice.close('refresh')
          Notice.open({
            title: '刷新成功, 将自动继续您以前的操做~'
          })
          // 注意触发事件
          RefreshEvent.emit('refreshEnd', true)
        })
        .catch(e => {
          // 刷新失败
          RefreshEvent.emit('refreshEnd', false)
          Notice.close('refresh')
          Notice.open({
            title: '刷新失败, 将自动为您跳转登陆页'
          })
          router.push({name: LOGIN_PAGE})
          return Promise.reject(e)
        })
    }
    复制代码
    • 请求前拦截器
    function preRequestInterceptor(config) {
      // 当正在刷新token时, 延时请求, 直到刷新完成
      if (isRefreshing && config.url !== REFRESH_URL) {
        // 经过返回Promise进行延迟操做
        return new Promise((resolve, reject) => {
          // 注册事件
          RefreshEvent.once('refreshEnd', result => {
            // 注意resolve(config)才能继续请求
            // 注意config中已经包含旧的token了, 而且不会自动刷新, 须要手动从新设置下
            if (result) {
                config.headers['x-client-refresh-token'] = setRefreshToken()
                resolve(config)
            }
            // 这里建议reject封装后的东西, 不然会出现reject形式不一致
            else reject(config)
          })
        })
      }
      return config
    }
    复制代码
    • 请求后拦截器
    function errorDeal(error) {
      if (error && error.response) {
        switch (error.response.status) {
          // 通常会有其余处理吧
          case 401:
            // 绑定事件
            // 重发事件避免重复处理(其实不会出现这种状况)
            if (error.config._retry) return Promise.reject(error)
            // 先注册事件!!! 再触发重试, 不然可能会注册失败哦~
            const re = new Promise((resolve, reject) => {
              // 一样注册事件, 用于延时请求
              RefreshEvent.once('refreshEnd', result => {
                if (result) resolve(error.config)
                else reject(error)
              })
            }).then(config =>
              // 两个注意点:
              // 1. 刷新token
              // 2. 请使用Axios.create({})出的实例, 避免此请求重复一次错误处理, 那样的话就会有两次错误处理
              config.headers['x-client-refresh-token'] = setRefreshToken()
              axiosRetry.request({
                ...config,
                // 绑定一些私有属性方便大家使用
                _retry: true
              })
            )
            if (!isRefreshing) tryToRefreshToken()
            // 注意返回Promise
            return re
        }
      }
      return Promise.reject(error)
    }
    复制代码
  3. 在各个地方触发各类方法:

    • 私有全局
    // 定义变量哟~
    // 注册刷新结束时间, 解除刷新态, 路由处理
    RefreshEvent.on('refreshEnd', result => {
      isRefreshing = false
      if (!result) {
        route.push({
          name: 'login'
        })
      }
    })
    // 绑定拦截器
    instances.forEach(instance => {
      instance.interceptors.request.use(preRequestInterceptor)
      instance.interceptors.response.use(undefined, errorDeal)
    })
    // 注册重试实例(即不注册拦截器)
    const axiosRetry = axios.create({})
    // 先触发一下, 以便从localStorage中进行加载
    setAccessToken()
    setRefreshToken()
    复制代码

写在最后

分享一下工做时缥缈的想法, 没有而后了.

若是文中出现错误, 还请提出哟, 我尽可能改~

插播小广告: 个人掘金主页

P.S. 坐标: 南京, 性别: ♂, 联系方式: ‭41620F678‬

此文在掘金原创, 其余网站请勿转载.

相关文章
相关标签/搜索