使用Store优化封装本地存储TOKEN & token过期处理

1,440 阅读4分钟

优化封装本地存储TOKEN操作模块

基本步骤

  1. 创建 src/utils/storage.js 模块
// 获取数据
export const getItem = key => {
  const data = window.localStorage.getItem(key)
  
  // 用try catch来进行 json的转换
  try {
    return JSON.parse(data)
  } catch (err) {
    return data
  }
}

// 存储数据
export const setItem = (key, value) => {
  // 将对象类型的数据 转换为json字符串格式
  if (typeof value === 'object') {
    value = JSON.stringify(value)
  }
  window.localStorage.setItem(key, value)
}

// 清除数据
export const removeItem = key => {
  window.localStorage.removeItem(key)
}
  1. store/index.js引入方法,并使用
import { getItem, setItem } from '@/utils/storage.js'
// 定义key的名字
const TOKEN = 'TOUTIAO_USER'

...

export default new Vuex.Store({
  state: {
    // 获取本地数据
    user: getItem(TOKEN)
  },
  mutations: {
    setUser (state, data) {
      state.user = data
      // 保存本地数据
      setItem(TOKEN)
    }
  }
})
  1. src/views/login/index.vue中,登录成功后存入token
  methods: {
    // 提交 登录表单
    async onSubmit() {
      ...
      // 3.发送请求
      // 在aixos的 try 代码快中,只要响应状态码不是 4/5 开头,都会正常执行,
      // 否则会去catch代码块中
      try {
        const {
          data: { data }
        } = await login(user)
        // data--{ token: 'xxx', refresh_token: 'xxx' }
        
        // 更新store中的数据
        this.$store.commit('setUser', data)
        ...
      } catch (err) {
        ...
      }
    },
    ...
    }
  1. utils/request.js中,通过请求拦截器--设置统一的headers(token)
import axios from 'axios'
import store from '@/store'

// axios.defaults.baseURL = 'http://127.0.0.1:8888/api/private/v1/'

// 创建axios实例,并设置配置项目(不同的基地址/参数)
const request1 = axios.create({
  baseURL: 'http://toutiao-app.itheima.net'
  // baseURL: 'http://ttapi.research.itcast.cn/' // 基础路径
})

// 请求拦截器--设置统一的headers
// Add a request interceptor
request1.interceptors.request.use(
  function(config) {
    // Do something before request is sent
    // 发起请求会经过这里
    // config :本次请求的配置对象
    // config 里面有一个属性:headers
    const { user } = store.state
    // 先判断 有没有token(如登录时,就没有token,就不需要添加token)
    if (user && user.token) {
      config.headers.Authorization = `Bearer ${user.token}`
    }
    return config
  },
  function(error) {
    // Do something with request error
    // 请求出错了,还没有发出去
    return Promise.reject(error)
  }
)

export default request1

关于TOKEN过期问题

问题阐述

登录成功之后后端会返回两个 Token:

  • token:访问令牌,有效期2小时
  • refresh_token:刷新令牌,有效期14天,用于访问令牌过期之后重新获取新的访问令牌

token超过有效期服务端会返回 401 表示 Token 无效或过期了。

为什么过期时间这么短?

  • 为了安全,例如 Token 被别人盗用

过期了怎么办?

  • 让用户重新登录,用户体验太差了
  • 使用 refresh_token 解决 token 过期

解决办法

当用户的 token 过期时,拿着refresh_token去换取新的token,并及时更新store和本地的token信息,来保持用户的登陆状态 v2-8f29f24dd291ddf46abda5d5ab7bec6c_720w.jpg

处理流程:

  1. 在axios的响应拦截器axios.interceptors.request.use()中加入token刷新逻辑

    • 判断错误状态码err.response.status是否为401,如果是说明token过期了,进行处理
  2. 判断此时store(本地)中是否有refresh_token

    • 如果没有,跳转到login页重新登录router.push('/login')

    • 如果有,则从store.state中获取到refresh_token

  3. refresh_token放到请求头的Authorization中,向服务器获取新的 token

    • 注意:此时直接用aixos请求,避免被request1请求拦截器中headers的Authorization覆盖
  4. 获取新的token成功后,将store和本地存储中将旧的token替换为新的token

    • 如果更新token成功,把之前失败的用户请求继续发出去

    • 如果失败,直接跳转 登录页

src/utils/request.js

/**
 * 封装 axios 请求模块
 */
import axios from 'axios'
import store from '@/store'
// 处理大数据 的插件
import jsonBig from 'json-bigint'
import router from '@/router'

// axios.defaults.baseURL = 'http://127.0.0.1:8888/api/private/v1/'

// 创建axios实例,并设置配置项目(不同的基地址/参数)
const request1 = axios.create({
  baseURL: 'http://toutiao-app.itheima.net', // 基础路径
  /**
   * 配置处理后端返回数据中超出 js 安全整数范围问题
   */
  // transformResponse 允许自定义原始的响应数据(字符串)
  transformResponse: [
    function(data) {
      try {
        // 如果转换成功则返回转换的数据结果
        return jsonBig.parse(data)
      } catch (err) {
        // 如果转换失败,则包装为统一数据格式并返回
        return { data }
      }
    }
  ]
})

// 请求拦截器--设置统一的headers---------------------------------------------------------------
// Add a request interceptor
request1.interceptors.request.use(
  function(config) {
    // Do something before request is sent
    // 发起请求会经过这里
    // config :本次请求的配置对象
    // config 里面有一个属性:headers
    const { user } = store.state
    // a.先判断 有没有token(如登录时,就没有token,就不需要添加token)
    // b.且判断 请求头有没有Authorization这个属性,如果有了就不要再重新设置
    //   获取新的token时,需要携带refresh_token请求,而不是token
    if (user && user.token) {
      config.headers.Authorization = `Bearer ${user.token}`
    }
    return config
  },
  function(error) {
    // Do something with request error
    // 请求出错了,还没有发出去
    return Promise.reject(error)
  }
)

// 设置响应拦截器---------------------------------------------------------------
request1.interceptors.response.use(
  // 响应成功进入第1个函数
  // 该函数的参数是响应对象
  config => {
    // console.log('响应拦截器:', config)
    return config
  },
  // 响应失败进入第2个函数,该函数的参数是错误对象
  async err => {
    // 如果响应码是 401 ,则请求获取新的 token
    // 响应拦截器中的 error 就是那个响应的错误对象
    // console.dir(err)

    if (err.response && err.response.status === 401) {
      // 校验是否有 refresh_token
      const { user } = store.state
      // a. 如果没有就 跳转到登录页面,重新登录
      if (!user || !user.refresh_token) {
        return router.push('/login')
      }
      // b. 如果有refresh_token,则请求获取新的 token
      try {
        // b.1 如果更新token成功,把之前失败的用户请求继续发出去
        await getNewToken(user)
        // config 是一个对象,其中包含本次失败请求相关的那些配置信息,例如 url、method 都有
        // return 把 request 的请求结果继续返回给发请求的具体位置
        return request1(err.config)
      } catch (error) {
        // b.2 如果更新token失败,直接跳转 登录页
        console.log('请求刷新token出错了' + error)
        router.push('/login')
      }
      return Promise.reject(err)
    }
  }
)

//  请求新的token
async function getNewToken(user) {
  // 发送请求新的 token的请求
  const { data } = await axios({
    url: 'http://toutiao-app.itheima.net/v1_0/authorizations',
    method: 'PUT',
    headers: {
      Authorization: `Bearer ${user.refresh_token}`
    }
  })
  // 判断请求是否成功
  if (data.message === 'OK') {
    // 拿到新的token ,存到store和本地里
    const newToken = data.data.token
    // 调用store用的方法,更新token
    store.commit('updateToken', newToken)
  }
}

export default request1

src/store/index.js

import Vue from 'vue'
import Vuex from 'vuex'

// 引入本地存储方法
import { getItem, setItem } from '@/utils/storage.js'
const TOKEN = 'TOUTIAO_USER'

Vue.use(Vuex)
/*
## 本地存储:
1. 获取麻烦(不同组件访问数据时)
2. 数据不是响应式(比如刷新页面等业务时,需要重新获取token)

## 登录成功,将 Token 存储到 Vuex 容器中
1. 获取方便(不通组件可以共享访问vuex中的数据)
2. 响应式(一旦vuex中数据改变,会通知组件更新)
另外:为了持久化,还需要把 Token 放到本地存储
*/
export default new Vuex.Store({
  state: {
    // 用户的登录状态信息token
    // 获取本地数据
    user: getItem(TOKEN) || {}
  },
  mutations: {
    setUser(state, user) {
      // 更新 state中的user
      state.user = user
      // 保存本地数据
      setItem(TOKEN, user)
    },
    updateToken(state, newToken) {
      // 更新 state中的token
      state.user.token = newToken
      // 保存到本地数据
      setItem(TOKEN, state.user)
    }
  },
  actions: {},
  modules: {}
})