项目中如何用Vuex处理用户Token与优化封装本地存储操作模块 & 优化设置 Token & Token 过期问题

1,387 阅读5分钟

1. 处理用户 Token

image.png

Token 是用户登录成功之后服务端返回的一个身份令牌,在项目中的多个业务中需要使用到:

  • 访问需要授权的 API 接口
  • 校验页面的访问权限
  • ...

问题:Token往哪儿存?

我们只有在第一次用户登录成功之后才能拿到 Token。所以为了能在其它模块中获取到 Token 数据,我们需要把它存储到一个公共的位置,方便随时取用。

  • 本地存储
    • 获取麻烦
    • 数据不是响应式
  • Vuex 容器(推荐)
    • 获取方便
    • 响应式的

使用容器存储 Token 的思路:

image.png

  • 登录成功,将 Token 存储到 Vuex 容器中
    • 获取方便
    • 响应式
  • 为了持久化,还需要把 Token 放到本地存储
    • 持久化

总结: Vuex状态管理工具可有可无 (*・ω-q)

  1. src/store/index.js

    import Vue from 'vue'
    import Vuex from 'vuex'
    
    Vue.use(Vuex)
    
    export default new Vuex.Store({
      // 1. 存储数据的地方 - 类比于vue文件的data()
      state: {
        // 一个对象,储存当前登录用户的token数据
        user: {}
      },
      // 2. 外界修改store中state的属性值,必须通过mutations中设置的修改方法 - 类比methods
      // 注意:这里方法里面的代码和.vue文件中的书写方式有差异,注意区分
      mutations: {
        setUser (state, data) {
          state.user = data
        }
      },
      // 3. 涉及到异步操作后修改state数据时,必须先过actions中的自定义方法,通过actions去调用mutations中的方法
      actions: {
      },
      // 4. 是state中数据的计算属性 - 类比computed
      getters: {
      },
      // 5. 模块化vuex,可以让每一个模块拥有自己的 state、mutation、action、 getters,使得结构非常清晰,方便管理。
      modules: {
      }
    })
    
    
  2. 登录成功以后将后端返回的 token 调用commit方法存到store中

    async onSubmit () {
    	...
      try {
        const res = await loginAPI(user)
        console.log('登录成功', res)
        // 调用store中的方法,将接口返回的token存到状态管理器中
        this.$store.commit('setUser', res.data.data)
    
        // 提示 success 或者 fail 的时候,会先把其它的 toast 先清除
        this.$toast.success('登录成功')
      } catch (err) {
        ...
    },
    

image.png 3. 将 store中的 token 相关数据存储到容器中

const TOKEN_KEY = 'TOUTIAO_USER'

export default new Vuex.Store({
  state: {
    user: JSON.parse(window.localStorage.getItem(TOKEN_KEY))
  },
  mutations: {
    setUser (state, data) {
      state.user = data
      // 为了防止刷新丢失,需要把数据备份到本地存储
      window.localStorage.setItem(TOKEN_KEY, JSON.stringify(state.user))
    }
  },
	...
})

2. 优化封装本地存储操作模块 - 封装localStrage功能

  1. 创建 src/utils/storage.js 模块

    • 存储
    • 获取
    • 删除
    // 封装本地存储操作模块
    
    /* 一个本地存储的数据应该拥有那些特性: 增删改查 */
    
    /*
      储存数据 (新增, 修改)
    */
    export const setItem = (key, value) => {
      // 将数组,对象等引用数据类型转化为JSON字符串进行存储
      // 将简单数据类型直接存储
      // 需要外界使用该方法时传入对一个的 键名
      if (typeof value === 'object') {
        // 将数组,对象等引用数据类型转化为JSON字符串进行存储
        value = JSON.stringify(value)
      }
    
      window.localStorage.setItem(key, value)
    }
    
    /*
      获取数据
    */
    export const getItem = key => {
      // 如果该键存储的是引用数据类型的JSON字符串,那么需要进行JSON.parse的转化
      const data = window.localStorage.getItem(key)
      // 使用JSON.parse()做JSON数据转化时可能会出现报错
      // 1. 做条件判断(要去找到所有满足、不满足的条件) 2. 做错误判断
      try {
        // 先尝试做JSON.parse()的转化,如果报错了,在把他当成原始数据进行返回
        return JSON.parse(data)
      } catch (error) {
        return data
      }
    }
    
    /*
      删除缓存数据
    */
    export const removeItem = key => {
      window.localStorage.removeItem(key)
    }
    
    
    
  2. 在store/index.js引入方法

    import { getItem, setItem } from '../utils/storage.js'
    
  3. 使用方法

    import { getItem, setItem } from '../utils/storage.js'
    const TOKEN_KEY = 'TOUTIAO_USER'
    
    export default new Vuex.Store({
      state: {
        user:getItem(TOKEN_KEY)
      },
      mutations: {
        setUser (state, data) {
          state.user = data
          // 为了防止刷新丢失,需要把数据备份到本地存储
          setItem(TOKEN_KEY, state.user)
        }
      },
    	...
    })
    

3. Vuex各属性的使用

  1. 创建测试用store.js

    import Vue from 'vue'
    import Vuex from 'vuex'
    
    Vue.use(Vuex)
    
    export default new Vuex.Store({
      // state存放状态,
      state: {
        name: 'tom', // 需要共用的数据
        age: '22'
      },
      // getter为state的计算属性
      getters: {
        getName: (state) => state.name, // 获取name
        getAge: (state) => state.age
      },
      // mutations可更改状态的逻辑,同步操作
      mutations: {
        setName: (state, data) => { state.name = data },
        setAge: (state, data) => { state.age = data }
      },
      // 提交mutation,异步操作
      actions: {
        acSetName (context, name) {
          setTimeout(() => {
            // 延时1秒提交至mutations中的方法
            context.commit('setName', name)
          }, 1000)
        },
    
        acSetAge (context, age) {
          setTimeout(() => {
            context.commit('setAge', age)
          }, 1000)
        }
      },
      // 将store模块化
      modules: {
      }
    })
    
    
  2. 创建页面comOne.vue测试计算属性

    <template>
      <div class="wrapper">
        asd
        <!-- 读取mapGetters中的getName与getAge -->
        <div>
          name:<span>{{ getName }}</span>
        </div>
        <div>
          age:<span>{{ getAge }}</span>
        </div>
      </div>
    </template>
    
    <script>
    import { mapState, mapGetters } from 'vuex' // 导入vuex的辅助函数
    export default {
      components: {},
      // 计算属性computed无法传递参数
      computed: {
        // 映射 state 中的数据为计算属性
        ...mapState(['name', 'age']),
        // 映射 getters 中的数据为计算属性
        ...mapGetters(['getName', 'getAge'])
      }
    }
    </script>
    <style scoped>
    </style>
    
    
  3. 创建comTwo.vue测试同步异步方法

    <template>
      <div class="wrapper">
        <div>
          <span>同步修改:</span>
          <!--直接回车调用mapMutations中的setName方法与setAge方法-->
          <input
            v-model="nameInp"
            @keydown.enter="setName(nameInp)"
            placeholder="同步修改name"
          />
          <input
            v-model="ageInp"
            @keydown.enter="setAge(ageInp)"
            placeholder="同步修改age"
          />
        </div>
    
        <div>
          <span>异步修改:</span>
          <!--直接回车调用mapAtions中的acSetName方法与acSetAge方法-->
          <input
            v-model="acNameInp"
            @keydown.enter="acSetName(acNameInp)"
            placeholder="异步修改name"
          />
          <input
            v-model="AcAgeInp"
            @keydown.enter="acSetAge(AcAgeInp)"
            placeholder="异步修改age"
          />
        </div>
      </div>
    </template>
    
    <script>
    import { mapMutations, mapActions } from 'vuex' // 导入vuex的辅助函数
    export default {
      components: {},
      data () {
        return {
          nameInp: '', // 绑定输入框的值
          ageInp: '',
          acNameInp: '',
          AcAgeInp: ''
        }
      },
      methods: {
        // 用于生成与 mutations 对话的方法,即:包含 $store.commit(xx) 的函数
        ...mapMutations(['setName', 'setAge']),
        // 用于生成与 actions 对话的方法,即:包含 $store.dispatch(xx) 的函数
        ...mapActions(['acSetName', 'acSetAge'])
      }
    }
    </script>
    <style scoped>
    </style>
    
    

4. 关于 Token 过期问题

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

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

我们的项目接口中设定的 Token 有效期是 2 小时,超过有效期服务端会返回 401 表示 Token 无效或过期了。

为什么过期时间这么短?

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

过期了怎么办?

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

如何使用 refresh_token 解决 token 过期?

到课程的后面我们开发的业务功能丰富起来之后,再给大家讲解 Token 过期处理。

大家需要注意的是在学习测试的时候如果收到 401 响应码,请重新登录

image.png

5.优化设置 Token

项目中的接口除了登录之外大多数都需要提供 token 才有访问权限。

通过接口文档可以看到,后端接口要求我们将 token 放到请求头 Header 中并以下面的格式发送。

image.png

字段名称:Authorization

字段值:Bearer token,注意 Bearertoken 之间有一个空格

方式一:在每次请求的时候手动添加(麻烦)。

axios({
  method: "",
  url: "",
  headers: {
    Authorization: "Bearer token"
  }
})

方式二:使用请求拦截器统一添加(推荐,更方便)。

image.png

src/utils/request.js 中添加拦截器统一设置 token:

import axios from 'axios'
import store from '../store/index.js'const request = axios.create({
  baseURL: 'http://toutiao.itheima.net/' // 接口的基准路径
})
​
// 请求拦截器
// Add a request interceptor
request.interceptors.request.use(function (config) {
  // Do something before request is sent
  // config :本次请求的配置对象
  // config 里面有一个属性:headers
  const { user } = store.state
  if (user && user.token) {
    config.headers.Authorization = `Bearer ${user.token}`
  }
  return config
}, function (error) {
  // 如果请求出错 - 抛出异常
  // Do something with request error
  return Promise.reject(error)
})

api.user.js注释掉store和获取用户信息携带的请求头

import request from '@/utils/request'
// import store from '@/store'/**
 * 获取用户自己的信息
 */
export const getUserInfo = () => {
  return request({
    method: 'GET',
    url: '/v1_0/user'
    // 发送请求头数据
    // headers: {
    //   // 注意:该接口需要授权才能访问
    //   //       token的数据格式:Bearer token数据,注意 Bearer 后面有个空格
    //   Authorization: `Bearer ${store.state.user.token}`
    // }
  })
}