当前位置:   article > 正文

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

前端登录及登录过期

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

什么是Token

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

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

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

基本使用

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

Token获取及使用

  • 接口封装
    /**
     * 用户请求模块
     */
    export const login = (dataÏ) => request({
      url: '/front/user/login',
      method: 'POST',
      data: qs.stringify(data)
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
  • 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: {
      }
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
  • 登录页面使用(伪代码)
    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
        }
      }
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
  • axios请求拦截器
    // 请求拦截器,每一个请求都会经过此拦截器。
    request.interceptors.request.use((config) => {
      // 在请求的header中设置token
      config.headers.Authorization = store.state?.user?.access_token
      return config
    }, (error) => {
      return Promise.reject(error)
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

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

在这里插入图片描述

request.js中
// 跳转至首页封装
const redirectLogin = () => {
  router.push({
    name: 'login',
    query: {
      // 通过参数传 登录成功后的跳转地址
      redirect: router.currentRoute.fullPath
    }
  })
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
'
运行
登录页面
 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) {}
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

优化——页面刷新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: {
  }
})
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

过期维护

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

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

请求发起前拦截

// 跳转首页逻辑
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
})
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74

请求发起后拦截

//相应拦截器
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))
        })
      })
    }
  }
  //... 其他异常捕获
})
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39

结束语

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

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

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/从前慢现在也慢/article/detail/947411
推荐阅读
相关标签
  

闽ICP备14008679号