Vuex身份认证

作者: amanohina | 来源:发表于2021-03-03 16:13 被阅读0次

    虽说上一节我们实现了登录功能,但是实际上还是可以通过浏览器的地址来跳过登录访问到后台,这种可有可无的登录功能使得系统没有安全性,而且没有意义
    为了让登录这个功能有意义,我们应该:

    • 应当在用户登录成功之后给用户生成一个标记(令牌),将这个令牌保存起来
    • 在用户访问任意需要登录的页面(组件)的时候都要去验证令牌
    • 从而识别到用户是否登录或者是否有权限去访问对应的功能
      1.成功时,访问组件
      2.失败时,进行提示
      如何让login组件中的数据被任意其他组件访问呢?这个时候可以使用vue官方的状态管理工具Vuex

    Vuex

    Vuex是一个专门为Vue.js应用程序开发的状态管理模式
    文档:

    https://vuex.vuejs.org/zh

    • Vuex是专门为Vue.js设计的状态管理库
    • 采用集中式的方式存储需要共享的数据
    • 本质上就是一个JavaScript库
    • 用来解决复杂组件通信,数据共享的问题
      简单来说就是Vuex用来统一存储需要在多个组件间共享的状态(数据),状态可以被任意组件操作,使得组件通信变得易如反掌

    那么归根到底,我们是否需要Vuex,要根据什么来判断呢?

    • 多个视频依赖于同一个状态
    • 来自不同视图的行为需要变更同一状态
      安装Vuex
      通过npm安装:
    npm install vuex -S
    

    使用Vue CLI创建项目的时候,可以在项目选项中选择Vuex,这个时候就不需要再单独安装了,这边我们已经在创建项目的时候安装过了Vuex,所以不再多做操作

    使用

    创建Vuex实例store,store通常称之为"容器"
    文件,store/index.js



    main.js导入,根Vue实例中引入Vuex作为插件

    通过Vue.use()引入Vuex中,Vuex的功能被注入到根实例下的所有子组件中,可以通过$store访问到内部功能
    我们的项目通过VueCLI创建时已经选择了Vuex,所以创建和引入都已经被Vue CLI自动完成了

    State

    容器中的state用于存储需要在组件之间共享的数据

    • 容器中的数据可以被任意组件访问
    • 容器中的数据为响应式数据
      state存储count
      Vue官方调试工具也可以看到数据
      在组件中,通过vm.$store.state.状态名访问
    // login/index.vue
    methods: {
      async onSubmit () {
        console.log(this.$store.state.user)
        ...
      }
    }
    

    Mutation

    来源:官方文档

    简单来说,要修改Vuex的state,必须提前定义Mutation函数,需要的时候再进行提交,Mutation接收state对象作为第一个参数,用于操作state内部的数据

    // store/index.js
    export default new Vuex.Store({
      state: {
        user: 100
      },
      mutations: {
        setUser (state) {
          state.user++
        }
      },
      actions: {
      },
      modules: {
      }
    })
    

    在组件中通过vm.$store.commit('Mutation名称')提交Mutation,执行操作

    // login/index.vue
    methods: {
      async onSubmit () {
        console.log(this.$store.state.user)
        this.$store.commit('setUser')
        console.log(this.$store.state.user)
        ...
      }
    }
    

    Mutation还接受提交载荷(payload)作为第二个参数,指的是commit()传入的额外数据,常常需要根据上下文数据修改state使用

    // store/index.js
    mutations: {
      setUser (state, payload) {
        state.user = payload
      }
    },
    
    // login/index.vue
    methods: {
      async onSubmit () {
        this.$store.commit('setUser', '示例内容')
        ...
      }
    }
    

    但是实际上在大多数情况下,载荷应该是一个对象才对,这样就可以包含多个字段并且记录的mutation会更易读:

    mutations: {
      increment (state, payload) {
        state.count += payload.amount
      }
    }
    
    store.commit('increment', {
      amount: 10
    })
    
    文档中的另一种对象风格的提交方式,可以记录一下

    Mutation的设置方式使得Vuex的状态修改有迹可循,易于维护,如果state可以通过赋值修改,一旦出错了就找不到错误点了
    除此之外,Vue Devtools还提供了Vuex更高级的调试方式Time Travel

    可以回溯到特定的操作点上进行调试
    Mutation必须为同步函数,由于DevTools提供了Mutation日志功能,为了确保功能正常,内部不能存在异步任务,否则DevTools将无法得知Mutation的准确调用顺序,如果需要进行异步操作,那么则需要Vuex的Action

    Action

    Action类似于Mutation,不同的地方在于:

    • Action提交的是Mutation,而不是直接变更状态
    • Action可以包含任意的异步操作
      Action 函数接受一个与store实例具有相同方法和属性的context对象,因为可以调用context.commit提交一个Mutation
    // store/index.js
    actions: {
      addAction (context) {
        setTimeout(function () {
          context.commit('setUser')
        }, 1000)
      }
    },
    

    在实机操作中,我们经常用到ES2015的参数结构来简化代码(就比如我们在解构后端传来的数据一样,直接{ data }),这种情况尤其是出现在我们需要多次调用commit的时候

      actions: {
        // jia (context) {
        //   context.commit('jia')
        // }
        jia ({ commit }) {
          commit('jia')
        }
      },
    

    Action 通过 vm.$store.dispatch方法触发,参数1为action名称,参数2为payload

    // login/index.vue
    methods: {
      async onSubmit () {
        this.$store.dispatch('addAction')
        ...
      }
    }
    

    Vuex 核心概念还有Getter和Module,可以通过文档来进行学习

    身份认证

    登录状态存储

    获取能够在任意组件中访问用户的登录信息,我们将状态存储在Vuex中

    // store/index.js
    import Vue from 'vue'
    import Vuex from 'vuex'
    
    Vue.use(Vuex)
    
    export default new Vuex.Store({
      state: {
        user: null
      },
      mutations: {
        setUser (state, payload) {
        state.user = payload
        }
      },
      actions: {
      },
      modules: {
      }
    })
    

    声明Mutation拿来用于修改user数据,具体内容采用payload载荷方式传入,我们可以通过devtools调试工具查看
    观察到通过接口传来的数据,我们应该拿来存入的是data.content,内部为用户的相关信息。

    // login/index.js
    methods: {
      async onSubmit () {
        try {
          ...
          // 当登录成功时,记录登录状态,存储到 Vuex 中
          this.$store.commit('setUser', data.content)
          this.$router.push({
            name: 'home'
          })
          this.$message.success('登录成功')
        } catch (err) {
          console.log('验证失败', err)
        }
      }
    }
    

    那么又因为传来的数据是JSON格式,为了以后方便我们的使用,我们应该在Mutation的setUser中转换为对象保存,可以通过DevTools调试工具查看到,然后就是通过本地存储的方式对user进行数据持久化,避免页面刷新后丢失,存储成功之后呢,就可以将user的初始值更改为本地存储获取user的数据

      state: {
        // 用于登录成功后保存用户信息的(初始值尝试读取本地存储
        user: JSON.parse(window.localStorage.getItem('user') || null)
      },
      mutations: {
        // 存储用户数据
        setUser (state, payload) {
          // 将payload转换为对象后进行存储
          state.user = JSON.parse(payload)
          // 将payload的数据添加到本地存储中
          // 本地存储只能存储字符串
          window.localStorage.setItem('user', payload)
        }
      },
    

    校验页面访问权限

    路由跳转时,需要校验登录状态,根据结果进行后续处理
    这里使用Vue Router的导航守卫beforeEach,在任务导航被触发的时候进行登录状态监测,当前后台页面都需要登录状态,但是有些页面不需要登录状态的话,这个该如何处理呢?
    使用:Vue Router的路由元信息功能来设置
    下面给需要设置登录状态的路由添加路由元信息(比如我们把home页面的子路由的部分设置为需要登录)

    • meta用于保存与路由相关的自定义数据
    • requiresAuth表示是否需要认证,true为需要



      用户登录状态保存在store(Vuex)中,需要引入文件来读取数据检测,在导航守卫中检测to的路由是否需要登录

    // 全局前置守卫
    router.beforeEach((to, from, next) => {
      // 验证 to 路由记录是否需要进行身份认证
      if (to.matched.some(record => record.meta.requiresAuth)) {
        // 验证Vuex 的 store 中的用户信息是否存储
        if (!store.state.user) {
          // 未登录,跳转到登录页面
          return next({ name: 'login' })
        }
        // 已经登录,允许通过
        next()
      } else {
        // 无需登录,允许通过
        next() // 确保一定要调用 next()
      }
    })
    

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

    上一次我如果访问了用户管理/user,过了一段时间状态过期,直接访问路由/user,跳转到login,登陆之后又跳转了首页。如果我希望登录后直接跳转到user而不是首页,我们就应该在每次跳转到/login时记录当前to目标的路由信息,这个时候就可以通过跳转路由的query属性进行设置

    //router/index.js
     if (!store.state.user) {
          // 未登录,跳转到登录页面
          next({
            name: 'login',
            query: {
              // 将本次路由的fullpath传递给login页面
              // path仅包含路径,fullpath为完整url(包含了查询字符串参数等信息)
              redirect: to.fullPath
            }
          })
        }
    

    那么在登录页中,登陆成功的跳转时,应该检测是否存在登录前的页面信息,有就跳转,没有就默认跳转首页


    变更了一下push顺序,防止出现路由重定向提前

    除了登录过期以外,例如将页面存储书签,或者点击其他人发送的链接访问,都可以在登陆之后自动跳转到对应的路由,提高了我们的体验


    删除掉本地用户信息后使用对应路由打开网页就可以在登录后定向到指定的路由了

    用户信息与接口鉴权

    用户基本信息接口:接口
    首先使用postman进行接口测试
    在集合中添加一个新的请求,设置基本信息


    发送请求,发现传回来的是错误信息
    HTTP状态码是401,状态文本为'Unauthorized'未授权
    这个时候就说明接口需要授权才能访问,查询了接口的说明文档可以得知,需要一个名为“Authotization”的请求参数(位于请求头),用于验证授权信息,这种验证接口的授权的处理方式我们称之为接口鉴权

    得出两条结论:
    • 后端提供的接口是没法随便访问的
    • 使用接口前需要进行接口的鉴权处理
      那么我们该怎么获得权限去获取后端返回的数据呢?

    Token

    一种常用的接口鉴权方式
    Token是在用户登陆成功之后,由服务端生成的一段保存了用户身份信息,加密的字符串
    生成之后,通过响应方式将token信息响应到服务端,通过之前的登录接口响应成功时可以看到

    将其复制下来,保存到用户基本信息接口的请求头上
    请求头Authorization
    就能得到信息啦,接口鉴权成功

    Postman统一设置token

    后续我们要用到的这个集合的接口会更多,每次都写Token的话就要被烦到,所以我们可以使用Postman提供的统一设置方式





    保存了之后,回到用户信息接口会发现我们单独设置的authorization提示了已经被统一设置的信息,我们就可以将自己设置的给删除掉,再次发送请求也是没有问题的

    实现用户信息展示

    测试处理完毕之后,我们需要在代码中进行Token处理和功能实现
    首先要封装用于用户信息请求的方法,在header最后那个设置Token,引入store用于读取token



    接着在app-header组件中引入功能,并且在created钩子函数中请求数据,并将其数据绑定到视图中

    <script>
    // 引入用户信息接口功能
    import { getUserInfo } from '@/services/user'
    
    export default {
      name: 'AppHeader',
      created () {
        // 加载用户信息
        // 钩子函数不建议书写代码逻辑,最好直接使用封装好的函数
        this.loadUserInfo()
      },
      data () {
        return {
          // 用户信息
          userInfo: {}
        }
      },
      methods: {
        async loadUserInfo () {
          const { data } = await getUserInfo()
          // console.log(data)
          this.userInfo = data.content
        }
      }
    }
    </script>
    
    绑定数据

    通过请求拦截器设置Token

    通过Axios的请求拦截器进行统一设置Token
    很多请求都是需要在header设置Token信息的,可以通过Axios拦截器进行统一处理
    Axios拦截器与导航守卫相似,可以在任意请求和响应前进行拦截处理,功能分为:

    • 请求拦截器
    • 响应拦截器
      通过请求拦截器参数config.headers可以访问请求头,将store中的Token统一设置就可以了
    // 设置请求拦截器
    request.interceptors.request.use(function (config) {
    // 判断config.url的前缀是什么,然后进行请求baseURL的设置
      config.baseURL = getBaseURL(config.url)
    
      // 统一的token信息设置
      // 为了严谨,可以读取store中的user后进行Token检测
      const { user } = store.state
      if (user && user.access_token) {
        // 设置token
        config.headers.Authorization = user.access_token
      }
    
      return config
    })
    
    

    这个拦截器设置之后呢,services/user.js中的getUserInfo内部的Token设置就可以删除了,同时也可以去掉store的引入

    // services/user.js
    ...
    // import store from '@/store'
    ...
    // 用户基本信息请求
    export const getUserInfo = () => {
      return request({
        method: 'GET',
        url: '/front/user/getInfo'
        // 在 header 中设置 Token 信息(统一设置后去除,记得去除上一行的分号)
        /* headers: {
          Authorization: store.state.user.access_token
        } */
      })
    }
    

    用户退出

    首先我们要给退出按钮设置点击事件,发现其实是触发不了的,那是因为我们使用的是Element组件,这里的退出按钮是组件,而组件设置的都是自定义事件

    // app-header.vue
    <el-dropdown-item
      divided
      @click="handleLogout"
    >退出</el-dropdown-item>
    
    ...
    
    methods: {
      ...
        // 退出按钮功能
      handleLogout () {
        console.log('点击退出')
      }
    }
    

    我们可以使用事件修饰符.native来监听组件根元素的原生事件

    // app-header.vue
    <el-dropdown-item
      divided
      @click.native="handleLogout"
    >退出</el-dropdown-item>
    

    点击退出后,清除store内部的用户信息,同时跳转回到登录页面

    • 通过mutation中的setUser清空user,由于setUser也设置了本地存储,这个时候也会自动清空
    // 退出功能
        handleLogout () {
          // 清除store的用户信息
          this.$store.commit('setUser', null)
          // 跳转登录页
          this.$router.push('/login')
        }
    

    可以通过Element的MessageBox弹框组件进行退出确认提示,增强体验感

    • 确认提示使用&confirm(),确定触发then(),取消触发catch()
    • 按钮内容默认为确定和取消,如不更改可以删除
      在点击退出按钮时,通过this,$confirm()进行确认消息设置
    • 将前面设置的退出功能移步到位确认退出的代码区域
    // 退出功能
        handleLogout () {
          this.$confirm('确认退出吗?', '退出提示', {
            confirmButtonText: '确定',
            cancelButtonText: '取消',
            type: 'warning'
          }).then(() => {
            // 清除store的用户信息
            this.$store.commit('setUser', null)
            // 跳转登录页
            this.$router.push('/login')
            this.$message({
              type: 'success',
              message: '退出成功!'
            })
          }).catch(() => {
            this.$message({
              type: 'info',
              message: '取消退出'
            })
          })
        }
    

    以上,登录功能到认证到退出一个闭环功能完成了,完善程度很高

    相关文章

      网友评论

        本文标题:Vuex身份认证

        本文链接:https://www.haomeiwen.com/subject/bqxxqltx.html