06-用vuex对token进行管理以及处理token过期问题

4,188 阅读4分钟

这里介绍对token的处理

  • 问题1:token数据或者其他数据,存在vuex仓库中,刷新会丢失状态 。
  • 问题2:数据只存在本地,数据变化了,相关的视图并不会更新。 Vuex 容器中的数据只是为了方便在其他任何地方能方便的获取登录状态数据, 但是页面刷新还是会丢失数据状态,所以我们还要把 数据进行持久化 以防止页面刷新丢失状态的问题。两种方式配合达到存储+响应式的功能。
    前端持久化常见的方式:Cookie,localstorage本地存储
  • 基本思路就是:
    • 用户登录成功之后,把token保存到本地的localstorage或者cookie中,在vuex中也存一份
    • 在vuex容器初始化的时候,优先使用本地存储的值

使用vuex管理token

在之前,我们在我们是在登录页面中发送的登录请求之后,获取到token,然后通过提交mutation的方式将token存到了vuex中,然后这里我们希望将登录token的异步操作,也封装成action,封装到vuex中,集中管理关于token操作。

vue封装action存token,我们在这里存俩份,一份存进vuex的user.js分支中的state里面,一份存进本地的cookie中或者localstorage里面都行

  • store/modules/user.js 中准备状态,用来存放获取到的token信息,创建设置状态的mutations方法以及异步actions方法
    const state = {
                 token: '' // token字符串
                   }
    const mutations = {
       // 保存token的方法
       setTokenFn(state, newToken) {
         // 获取到token的时候,设置给token的同时也让setToken存进cookie中
         state.token = newToken
         setToken(newToken)
       },
    }
    const actions = {
       // 登录
       async login(context, loginForm) {
         const { data } = await reqLogin(loginForm)
         if (data.success) context.commit('setTokenFn', data.data)
       },
    }
    const getters = {}
    
    export default {
     namespaced: true,
     state,
     mutations,
     getters
    }
    
  • 在登录页面中调用
        handleLogin() {
      // 预校验
      this.$refs.loginForm.validate(async valid => {
        if (valid) {
          // 开启登录按钮的loading
          this.loading = true
          try {
            // 发送请求
            await this.$store.dispatch('user/login', this.loginForm)
            console.log('请求成功')
            this.$router.push('/')
          } catch (error) {
            console.log(error)
          }
          // 无论成功还是失败都要把loading关闭
          this.loading = false
        }
      })
    }
    

添加token的getters

为了更好的让其他模块和组件获取到token数据和其他类似公共的数据的值,我们可以把这些数据添加到getters里,便于将来访问。可能你会说为什么不都写在一个store文件里,这是为了各模块之间的独立,使代码更清晰,所以都抽离出来了,在store/index.js中统一进行了挂载。

  • store/getters.js
    const getters = {
      sidebar: state => state.app.sidebar,
      device: state => state.app.device,
      token: state => state.user.token, // 把token提到getters中,方便调用
      name: state => state.user.userInfo.username, // 用户名字的映射
      staffPhoto: state => state.user.userInfo.staffPhoto
    }
    export default getters
    
  • store/index.js
    import Vue from 'vue'
    import Vuex from 'vuex'
    import getters from './getters'
    import app from './modules/app'
    import settings from './modules/settings'
    import user from './modules/user'
    import permission from './modules/permission'
    
    Vue.use(Vuex)
    
    const store = new Vuex.Store({
      modules: {
        app,
        settings,
        user,
        permission
      },
      getters // 这里是把getters挂载到全局,更加方便,如果放在modules里面,则在使用的时候还要加模块名
    })
    
    export default store
    

vuex持久化

刚刚在登录时,已经可以成功的将token存到vuex中,但是vuex刷新会丢失,所以我们需要结合web存储实现持久化。
我们可以使用localstorage或者cookie进行存储,然后在utils文件夹中新建auth.js或者storage.js或者cookie.js,意思就是本地存储的文件,把本地存储数据的方法写进去

  • utils/auth.js
import Cookies from 'js-cookie'

const TokenKey = 'qm-token'
const ThemeKey = 'qm-theme'

export function getToken() {
  return Cookies.get(TokenKey)
}

export function setToken(token) {
  return Cookies.set(TokenKey, token)
}

export function removeToken() {
  return Cookies.remove(TokenKey)
}

export function getTheme() {
  return Cookies.get(ThemeKey)
}

export function setTheme(Theme) {
  return Cookies.set(ThemeKey, Theme)
}

export function removeTheme() {
  return Cookies.remove(ThemeKey)
}

  • store/user.js 把token信息改为getToken(),一进来先从本地拿
const state = {
  // 一进来优先从缓存中取
  token: getToken() // token字符串
}
  • 在存token的时候,本地也存一份
const mutations = {
  // 设置token
  setToken(state, newToken) {
    state.token = newToken
    // 设置了 token 的同时, 同步到本地cookies中
    setToken(newToken)
  }
}

token过期的处理

没有绝对的安全, 所谓的安全处理, 就是提高攻击者攻击的难度, 对他造成了一定的麻烦, 我们这个网站就是安全的! 网站安全性就是高的! 所以: token 必须要有过期时间! 每隔一段时间, 必须有一个新的token, 旧的token失效。

  • token的过期问题 你登陆成功之后,接口会返回一个token值,这个值在后续请求时带上(就像是开门钥匙)。 但是,这个值一般会有有效期(具体是多长,是由后端决定),在我们的项目中,这个有效期是2小时。 如果,上午8点登陆成功,到了10:01分,则token就会失效,再去发请求时,就会报401错误。 思考:

  • token需要过期时间吗 ?

    token即是获取受保护资源的凭证,当然必须有过期时间。否则一次登录便可永久使用,认证功能就失去了其意义。非但必须有个过期时间,而且过期时间还不能太长,

    参考各个主流网站的token过期时间,一般1小时左右

    token一旦过期, 一定要处理, 不处理, 用户没法进行一些需要授权页面的使用了

  • token过期该怎么办?

    token过期,就要重新获取。

    那么重新获取有两种方式,一是重复第一次获取token的过程(比如登录,扫描授权等) ,这样做的缺点是用户体验不好,每一小时强制登录一次几乎是无法忍受的。

    那么还剩第二种方法,那就是主动去刷新token. 主动刷新token的凭证是refresh token,也是加密字符串,并且和token是相关联的。相比可以获取各种资源的token,refresh token的作用仅仅是获取新的token,因此其作用和安全性要求都大为降低,所以其过期时间也可以设置得长一些。

目标效果 - 保证每一小时, 都是一个不同的token

第一次请求 9:00 用的是 token1
第二次请求 12:00 用的是 token2

image.png

  • token:

    • 作用:在访问一些接口时,需要传入token,就是它。
    • 有效期:2小时。
  • refresh_token

    • 作用: 当token的有效期过了之后,可以使用它去请求一个特殊接口(这个接口也是后端指定的,明确需要传入refresh_token),并返回一个新的token回来(有效期还是2小时),以替换过期的那个token。
    • 有效期:14天。(最理想的情况下,一次登陆可以持续14天。)

image.png 对于 某次请求A 的响应,如果是401错误

  • 有refresh_token,用refresh_token去请求回新的token

    • 新token请求成功

      • 更新本地token
      • 再发一次请求A
    • 新token请求失败

      • 清空vuex中的token
      • 携带请求地址,跳转到登陆页
  • 没有refresh_token

    • 清空vuex中的token
    • 携带请求地址,跳转到登陆页

对于一个请求的响应 401, 要这么处理, 对于十个请求的响应 401, 也要这么处理,

我们可以统一将这个token过期处理放在响应拦截器中

请求拦截器: 所有的请求, 在真正被发送出去之前, 都会先经过请求拦截器 (可以携带token)

响应拦截器: 所有的响应, 在真正被(.then.catch await)处理之前, 都会先经过响应拦截器, 可以在这个响应拦截器中统一对响应做判断

响应拦截器处理token

1、没有 refresh_token 拦截到登录页, 清除无效的token

  • utils/request.js
// 添加响应拦截器
http.interceptors.response.use(function (response) {
  // 对响应数据做点什么 (成功响应) response 就是成功的响应 res
  return response
}, function (error) {
  // 对响应错误做点什么 (失败响应) 处理401错误
  // console.dir(error)
  if (error.response.status === 401) {
    console.log('token过期了, 一小时过去了, 需要通过refresh_token去刷新token')
    // 获取 refresh_token, 判断是否存在, 存在就去刷新token
    const refreshToken = store.state.tokenInfo.refresh_token
    if (refreshToken) {
      console.log('存在refreshToken, 需要进行刷新token操作')
    } else {
      // 没有refreshToken, 直接去登录, 将来还能跳回来
      // router.currentRoute 指向当前路由信息对象 === 等价于之前页面中用的 this.$route
      // 清除本地token, 跳转登录 (无意义的本地token内容, 要清除)
      store.commit('user/removeToken')
      router.push({
        path: '/login',
        query: {
          backto: router.currentRoute.fullPath
        }
      })
    }
  }
  return Promise.reject(error)
})

2、提供清除token的mutation

因为不仅要清空本地的token,还要清空vuex的token信息

  • store/user.js
  mutations: {
    setTokenFn (state, data) {
      state.tokenInfo = data
      console.log('存', data)
      setToken(data)
      const res = getToken()
      console.log('取', res)
    },
    // 移出tokenInfo的信息, 恢复成空对象
    removeToken (state) {
      state.tokenInfo = {}
      // 更新到本地, 本地可以清掉token信息
      removeToken()
    }
  },

3、如果有refresh_token,那就发送请求,刷新token

注意: 这边发请求, 不要用上面二次处理好的request实例, 用它会自动在请求前帮你携带token(会覆盖你的refresh_token),要用原生的axios
注意:刷新完token,拿到最新的token并存好之后,应该重新发送刚刚的请求,做到让用户无感知

  • utils/request.js
const refreshToken = store.state.user.tokenInfo.refresh_token
if (refreshToken) {
  console.log('存在refreshToken, 需要进行刷新token操作')
  // (1) 发送请求, 进行刷新token操作, 获取新的token
  // 注意: 这边发请求, 不用request实例, 用它会自动在请求前帮你携带token(会覆盖你的refresh_token)
  // 这边, 直接用 axios 发送请求
  const res = await axios({
    method: 'put',
    url: 'http://toutiao.itheima.net/v1_0/authorizations',
    // 请求头中携带refresh_token信息
    headers: {
      Authorization: `Bearer ${refreshToken}`
    }
  })
  const newToken = res.data.data.token
  // (2) 将新token更新到vuex中
  store.commit('user/setTokenInfo', {
    refresh_token: refreshToken,
    token: newToken
  })
} 

4、refresh_token也过期了

那就是真正的用户过期了,直接重复路由守卫里面的操作,清除掉本地token信息,带着目标路径前往login页面,登录完之后返回刚刚要前往的目标页面。
额,发现文章顺序写错了,这里提一下路由守卫里做了什么事,在07里面有。就是用户在进入白名单页面时,比如说首页或者其他不需要个人信息页面时,是不需要登录的,如果在没有登录的情况下,想要进入类似于个人中心,会员中心这种页面时,那么就需要让用户去登录页面登录一下,登录完成之后再返回刚刚要去的个人中心页面,这时候就是在路由跳转的时候,带上刚刚页面的路由路径,可以在登录完成之后原路返回。

// 添加请求拦截器
request.interceptors.request.use(function (config) {
  // 在发送请求之前做些什么
  const token = store.state.user.tokenInfo.token
  console.log(token)
  if (token) {
    config.headers.Authorization = `Bearer ${token}`
  }
  return config
}, function (error) {
  // 对请求错误做些什么
  console.log(error)
  return Promise.reject(error)
})

// 添加响应拦截器
request.interceptors.response.use(function (response) {
  // 对响应数据做点什么
  return response
}, async function (error) {
  // 对响应错误做点什么
  // 对响应错误做点什么 (失败响应) 处理401错误
  // console.dir(error)
  if (error.response.status === 401) {
    console.log('token过期了, 一小时过去了, 需要通过refresh_token去刷新token')
    // 获取 refresh_token, 判断是否存在, 存在就去刷新token
    const refreshToken = store.state.user.tokenInfo.refresh_token
    console.log(refreshToken)
    if (refreshToken) {
      try {
        console.log('存在refreshToken, 需要进行刷新token操作')
        // (1) 发送请求, 进行刷新token操作, 获取新的token
        // 注意: 这边发请求, 不用request实例, 用它会自动在请求前帮你携带token(会覆盖你的refresh_token)
        // 这边, 直接用 axios 发送请求
        const res = await axios({
          method: 'put',
          url: 'http://toutiao.itheima.net/v1_0/authorizations',
          // 请求头中携带refresh_token信息
          headers: {
            Authorization: `Bearer ${refreshToken}`
          }
        })
        const newToken = res.data.data.token
        // (2) 将新token更新到vuex中
        store.commit('user/setTokenFn', {
          refresh_token: refreshToken,
          token: newToken
        })
        console.log('>>>>>>>>>>>>>>>>>>>.', error.config)
        // 刷新token后,应该重新发送刚才的请求,让用户刷新token无感知
        return request(error.config)
      } catch (error) {
        console.log('使用refresh_token获取新token失败')
        // 清除本地token跳转路由
        store.commit('user/removeTokenInfo')
        // 路由跳转, 进入登录页
        router.push({
          path: '/login',
          query: {
            backto: router.currentRoute.fullPath
          }
        })
      }
    } else {
      // 没有refreshToken, 直接去登录, 将来还能跳回来
      // router.currentRoute 指向当前路由信息对象 === 等价于之前页面中用的 this.$route
      // 清除本地token, 跳转登录 (无意义的本地token内容, 要清除)
      store.commit('user/removeToken')
      router.push({
        path: '/login',
        query: {
          backto: router.currentRoute.fullPath
        }
      })
    }
  }
  return Promise.reject(error)
})

注意点

  • 响应拦截器要加在axios实例 reuqest 上。
  • 用refresh_token请求新token时,要用axios,不要用实例 request (需要: 手动用 refresh_token 请求)
  • 得到新token之后,再发请求时,要用 request 实例 (用token请求)
  • 过期的 token 可以用 refresh_token 再次更新获取新token, 但是过期的 refresh_token 就应该从清除了
  • 在使用的时候注意参数别写错,尤其是token跟refreshtoken的提取,名字别错