Vue项目实战 [ 身份认证、Vuex、Token](4)

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

身份认证

  • 虽然完成了登录功能,但实际上现在用户没登录也能访问(对应的url),这样的话显得登录功能毫无意义。
  • 为了让登录变得有意义:
    1. 应当在⽤户登录成功后给⽤户⽣成⼀个标记(令牌),并将这个令牌保存起来。
    2. 在⽤户访问任意需要登录的⻚⾯(组件)时都去验证令牌;
    3. 从⽽识别⽤户是否登录或是否有权访问对应功能。
    a. 成功时,访问组件。 b. 失败时,进⾏提示。
  • 如何能够让 login 组件中的数据可以被任意其他组件访问呢?这时可以使⽤ Vue 官⽅的状态管理⼯具Vuex。

Vuex

简介

  • Vuex是专门为Vue.js设计的状态管理库
  • 采用集中式的方式存储需要共享的数据
  • 本质就是一个JavaScript库
  • 用来解决复杂组件通信数据共享的问题

[官方文档]Vuex 可以帮助我们管理共享状态,并附带了更多的概念和框架。这需要对短期和⻓期效益进⾏权衡。如果您不打算开发⼤型单⻚应⽤,使⽤ Vuex 可能是繁琐冗余的。确实是如此——如果您的应⽤够简单,您最好不要使⽤ Vuex。⼀个简单的 store 模式 (opens new window)就⾜够您所需了。但是,如果您需要构建⼀个中⼤型单⻚应⽤,您很可能会考虑如何更好地在组件外部管理状态,Vuex 将会成为⾃然⽽然的选择。

如何判断是否需要Vuex?

  • 多个视图依赖于同一状态
  • 来自不同视图的行为需要变更为同一状态

Vuex使用

安装Vuex

  • npm install vuex -S
  • 也可以使用Vue CLI创建项目时可以在项目选项中选择Vuex,进行安装

使用

  • 因为Vuex和VueRouter一样,是Vue的插件,因此需要使用vue.use(Vuex)

State

  • 存储在组件中需要共享的数据

Mutation

  • 更改Vuex的store中的状态的唯一方法是提交mutation
    • 原因:方便维护
  • 提交mutation使用store.commit('方法名')
    • 含义:触发某一个mutation
  • 有历史记录
    • 通过详细的列表记录了每一次操作
  • 必须为同步函数

提交载荷(Payload)

  • 向store.commit传入额外的参数,即mutation的载荷
  • 大多数情况下,载荷应该是一个对象,这样可以包含多个字段并且记录mutation会更易读

Action

  • Action类似于mutation,不同在于:
    • Action提交的是mutation,而不是直接变更状态
    • Action可以包含任意异步操作,而mutation中不能存在异步操作。
    • 举例:mutation中只能存在同步操作,若在mutation中写异步操作在devtools中不能达到记录真实操作顺序的效果。

总结

  • 异步操作写在actions里,同步操作写在mutations里面,最终调用的是具有异步功能的actions

Module

  • 由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得比较复杂时,store对象就有可能变得相当臃肿。
  • 为了解决以上问题,Vuex允许我们将store分割成为模块(module)。每个模块拥有自己的state、mutation、action、getter、甚至是嵌套子模块。
  • 代码演示
const moduleA = {
  state: () => ({...}),
  mutations: {...},
  actions: {...},
  getters: {...}
}
复制代码

getters

  • Vuex允许在store中定义“getter”(可以认为是store的计算属性)。就像计算属性一样,getter的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。
  • 代码演示
const store = new Vuex.store({
  state:{
    todos: [
      {id: 1,text: '...',done: true},
      {id: 2,text: '...',done: false}
    ]
  },
  getters: {
    doneTodos: state => {
      return state.todos.filter(todo => todo.done)
    }
  }
})
复制代码

身份认证:登录状态存储

  • 为了能在任意组件中访问用户的登录信息,因此将状态存储在Vuex中。

校验页面访问权限

  • 路由跳转时,需要校验登陆状态,并根据结果进行后续处理。
  • 使用Vue Router的导航守卫beforeEach,在任务导航被触发时进行登陆状态检测。

实现方法

  • 直接给某个路由设置,这时内部的子路由都需要认证(包括自己)
  • 验证Vuex的store中的登录用户信息是否存储
  • 当前后台⻚⾯均需要登录状态,但如果需求中只有部分⻚⾯需要登录状态的话,该如何判断处理呢?可以通过 Vue Router 中的路由元信息功能来设置。

路由元信息

  • 设置部分页面需要验证登录状态
  • 此项目中将"/"的子路由设置为需要身份认证
    • meta用于保存与路由相关的自定义数据
    • requiresAuth表示是否需要认证,true为需要认证

身份认证-路由元信息书写方式.png

登录后跳转到上次访问的页面

  • 例如我当前访问⼴告管理/advert,这时我想访问⽤户管理/user,但因为⻓时间没操作,登录状态过期了(⼿动请求 store 模拟),就会⾃动跳回/login,当我再次登录后,默认会跳转到/⾸⻚。
  • 如果希望登录后能跳转到上次正在访问的⻚⾯,该如何操作呢?
  • 这时应当在每次跳转到/login 时记录当前 to ⽬标路由信息,通过跳转路由的 query 属性进⾏设置

身份认证-跳转到上一次访问的页面.png

  • login/index.vue中这样书写:this.$router.push(this.$route.query.redirect || '/')

注意事项

  • fullpath与path的区别

身份认证-注意事项path.png

  • r o u t e r router与 route的区别
    • 前者:跳转操作、调用方法
    • 后者:当前这条路由的路由信息
  • 校验页面访问权限是否成功时,需要将
    • Vuex中的user对象设为:null
    • 控制台Application中的Local Storage中的user对象删除

用户信息与接口鉴权

  • 先使用postman进行接口测试
  • 测试过程中可以将token字符串直接写在全局。这样就不用每次测试不同接口都填写一遍了。

身份认证-全局变量存储header.png

注意

  • 401:未授权(4开头的客户端错误)

实现用户信息展示

  • 封装请求
// 用户基本信息 因为不需要传递参数,因此传递为空
export const getUserInfo = () => {
  return request({
    method: 'GET',
    url: '/front/user/getInfo'
    headers: {
      // 需要从Vuex的store中取出token信息
      Authorization: store.state.user.access_token
    }
  })
}
复制代码
  • 数据绑定给视图
methods: {
    async loadUserInfo () {
      // 将返回的数据保存到data中
      const { data } = await getUserInfo()
      this.userInfo = data.content
    }
  }
复制代码

通过请求拦截器设置Token

  • 注意:需要先判断是否有user对象才能user.access_token,要严谨
// 创建axios请求拦截器,回调函数
request.interceptors.request.use(function (config) {
  const { user } = store.state
  // 设置请求头中的token字符串
  if (user && user.access_token) {
    config.headers.Authorization = user.access_token
  }
  return config
})
复制代码

用户退出

  • “退出”设置点击事件无效
    • 分析:此处的“退出”是组件而不是按钮,给组件绑定的事件都是自定义事件,并不是原生的dom事件。
    • 但是现在想要用原生的dom事件,就需要给事件设置“事件修饰符”

身份认证-用户退出.png

提升用户体验:弹出框

  • 提醒用户是否需要退出MessageBox弹窗
handleLogout () {
// 消息弹框的内容  消息弹框的标题
  this.$confirm('确定退出吗?', '退出提示', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    type: 'warning'
  }).then(() => {
    // messageBox与message消息提示相结合,提示信息
    this.$message({
      type: 'success',
      message: '退出成功!'
    })
    // 1.消除Vuex中存储的user对象为null
    this.$store.commit('setUser', null)
    // 2.路由跳转 至 /login
    this.$router.push('/login')
  }).catch(() => {
    this.$message({
      type: 'info',
      message: '已取消退出'
    })
  })
}
复制代码

Token过期处理

  • 之所以出现“用户信息消失”,但是页面仍停留在“首页”,因为后台页面不需要去服务端鉴权,只是在客户端本地验证Vuex中的数据,但是在本地Vuex是无法验证token是否过期
  • 后端的接口鉴权并不是一直可以使用的,会过期,过期后是可以进行处理的

Token过期分析

  • Token用于进行接口鉴权,但Token具有后端设置的过期时间,当Token过期后,就无法再请求接口数据了。
  • 项目中后端设置的过期时间为24h,测试时可以手动修改token值让Token失效
  • 处理方式
    • 方式1:用户重新登陆,获取新的Token。但当过期时间较短时,用户每次都要重新登录,体验不好。
      • 为了提高用户信息安全性,Token的过期时间都比较短。(就算万一泄露了,过一会儿就过期无效了)
    • 方式2:根据用户信息,自动给用户生成新的Token,减少重新登录次数。
  • 观察前面的功能,接口的响应信息中具有三个与Token相关的信息。
    • access_token:当前使用的Token,用于访问需要授权的接口
    • expires_in:access_token的过期时间
    • refresh_token:刷新获取新的access_token

刷新Token(重点)

  • 方法1:在每个请求发起前进行拦截(请求拦截器),根据expires_in判断token是否过期,如果过期则刷新后再继续请求接口。
    • 优点:请求前拦截处理,能节省请求次数。
    • 缺点:后端需要提供Token过期时间字段(例如expires_in),并且需要结合计算机本地时间判断,如果计算机时间被篡改(特别是比服务器时间慢)时,拦截会失败
  • 方法2:在每个请求响应后进行拦截(响应拦截器),如果发现请求失败(Token过期导致的)时,刷新你Token再重新请求接口。
    • 优点:无需Token过期时间字段,无需判断时间
    • 缺点:多消耗一次请求
  • 推荐使用方法2,相比方法1,方法2更加稳定,不会出现意外的问题。

使用Axios响应拦截器

  • 拦截器

Token过期-拦截器.png

  • 响应拦截器会在响应接收完毕,在对应请求处理前被拦截器拦截。
  • 响应拦截器参数response中保存了响应的信息。

刷新Token操作

  • HTTP状态码401表示未授权,可以导致401的情况有很多:
    • 没有Token
    • Token无效
    • Token过期
  • 判断方式如下:
    • 检测是否存在refresh_token(后端通常会限制每个refresh_token只能获一次新Token)
      • 获取成功,重启发送请求,请求接口数据即可
      • 获取失败,跳转登录页
    • 如果没有,跳转登录页

Axios错误处理

  • 查看官网,书写方式

处理Token过期

  • 数组存储因为Token更新,而挂起的请求

多个请求重复刷新Token处理

  • 如果页面中存在多个请求(大多数页面中都不会只有一个请求),如果Token过期,每个请求都会刷新Token,这时重复刷新多次没有意义,又增加了请求个数,还会出现额外的问题
    • 每个refresh_token只能使用一次,多次请求会出现错误

身份认证-多次请求.png

  • 为了避免多次请求重复刷新Token,可以通过一个(let)变量isRefreshing标记Token的刷新状态
    • 默认状态为false,并在发送刷新Token请求前检测,当状态为false才能传送
    • 发送刷新请求时,设置标记为true
    • 请求完毕,设置标记为false(应该在finally内部书写,不论请求失败还是成功)
  • **出现问题:**虽然刷新Token的问题解决了,但之前发送的两个请求只有一个成功执行,其余的请求被阻止了。
  • **解决方式:**声明一个数组存储所有被挂起的请求,当Token刷新完毕再将这些请求重新发送。
// 存储应等待挂起的请求
let requests = []
request.interceptors.response.use(function (response) {
.....
  if (status === 400) {
    errorMessage = '请求参数错误'
  } else if (status === 401) {
  ......
  if (isRefreshing) {
    // 将当前请求添加到数组中,并将数组返回
    return requests.push(() => {
      request(error.config)
    })
  }
  // 如果是没有在更新的,设置标记true
  isRefreshing = true
  // refresh_token进行token刷新
  request({
    method: 'POST',
    url: '',
    data: qs.stringify({
      refreshtoken: store.state.user.refresh_token
    })
  }).then({
    ......
    // 将其他的请求进行调用执行
    requests.forEach(callback => callback())
    // 清空所有的请求
    requests = []
    return request(error.config)
  }).catch({
    ......
  }).finally({
    isRefreshing = false
  })
  ......
  }
.....
}
复制代码

「欢迎在评论区讨论,掘金官方将在掘力星计划活动结束后,在评论区抽送100份掘金周边,抽奖详情见活动文章」。

猜你喜欢

转载自juejin.im/post/7017749153132478495