前端面试常考之——Vue前端路由权限控制(vuex版本)

文章目录

前端权限控制思路

1. 菜单的权限控制
  • 菜单的控制
    在登录请求中,会得到权限数据,当然,这个需要后端返回数据的支持。前端根据权限数据展示对应的菜单,点击菜单才能查看相关的界面
  • 界面的控制
    如果用户没有登录,手动在地址栏敲入管理界面的地址,则需要跳转到登录页。如用户已经登录,可是手动敲入非权限内的地址,则需要跳转404界面
  • 按钮的控制
    在某个菜单的界面,还得根据权限数据,展示出可进行操作的按钮,比如删除,修改增加
  • 请求和响应的控制
    如果用户通过非常规的操作,比如通过浏览器调试工具将某些禁用的按钮变成启用状态,此时发的请求,也应当被前端所拦截

Vue的权限控制实现

1. 菜单的控制
  • 查看登录之后获取到的数据

    json 复制代码
    {
        "data": {
            "id": 500,
            "rid": 0,
            "username": "admin",
            "mobile": "13999999999",
            "email": "123999@qq.com",
            "token": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOjUwMCwicmlkIjowLCJpYXQiOjE1MTI1NDQyOTksImV4cCI6MTUxMjYzMDY5OX0.eGrsrvwHm-tPsO9r_pxHIQ5i5L1kX9RX444uwnRGaIM"
        },
        "rights": [
            {
                "id": 125,
                "authName": "用户管理",
                "icon": "icon-user",
                "children": [
                    {
                        "id": 110,
                        "authName": "用户列表",
                        "path": "users",
                        "rights": [
                            "view",
                            "edit",
                            "add",
                            "delete"
                        ]
                    }
                ]
            },
            {
                "id": 103,
                "authName": "角色管理",
                "icon": "icon-tijikongjian",
                "children": [
                    {
                        "id": 111,
                        "authName": "角色列表",
                        "path": "roles",
                        "rights": [
                            "view",
                            "edit",
                            "add",
                            "delete"
                        ]
                    }
                ]
            },
            {
                "id": 101,
                "authName": "商品管理",
                "icon": "icon-shangpin",
                "children": [
                    {
                        "id": 104,
                        "authName": "商品列表",
                        "path": "goods",
                        "rights": [
                            "view",
                            "edit",
                            "add",
                            "delete"
                        ]
                    },
                    {
                        "id": 121,
                        "authName": "商品分类",
                        "path": "categories",
                        "rights": [
                            "view",
                            "edit",
                            "add",
                            "delete"
                        ]
                    }
                ]
            }
        ],
        "meta": {
            "msg": "登录成功",
            "status": 200
        }
    }

    在这部分数据中,除了该用户的基本信息之外,还有两个字段很关键

    • token,由于前端用户的状态保持
    • rights:该用户具备的权限数据,一级权限就对应一级菜单,二级权限就对应二级菜单
  • 根据rights中的数据,动态渲染左侧菜单栏,数据在Login.vue得到,但是在Home.vue才使用,所以可以把数据用vuex进行维护

    • vuex------index.js

      js 复制代码
      import Vue from 'vue'
      import Vuex from 'vuex'
      
      Vue.use(Vuex)
      
      export default new Vuex.Store({
        state: {
          rightList: JSON.parse(sessionStorage.getItem('rightList') || '[]'),
          username: sessionStorage.getItem('username')
        },
        mutations: {
          setRightList(state, data) {
            state.rightList = data
            sessionStorage.setItem('rightList', JSON.stringify(data))
          },
          setUsername(state, data) {
            state.username = data
            sessionStorage.setItem('username', data)
          }
        },
        actions: {
      
        },
        getters: {
      
        }
      })
    • Login.vue

      js 复制代码
      login() {
            this.$refs.loginFormRef.validate(async (valid) => {
              if (!valid) return
              const { data: res } = await this.$http.post('login', this.loginForm)
              if (res.meta.status !== 200) return this.$message.error('登录失败!')
              console.log(res)
              this.$store.commit('setRightList', res.rights)
              this.$store.commit('setUsername', res.data.username)
              this.$message.success('登录成功')
              this.$router.push('/home')
            })
          },
    • Home.vue

      js 复制代码
      import { mapState } from 'vuex'
      computed: {
          ...mapState(['rightList', 'username'])
        },
      created() {
          this.activePath = window.sessionStorage.getItem('activePath')
          // 初始化menulist菜单栏的数据
          this.menulist = this.rightList
        },
  • 刷新界面菜单消失

    • 原因分析

      因为菜单数据是登录之后才获取的,存放在vuex中
      一旦刷新界面,vuex中的数据会初始化为空
      因此,需要将权限数据存储在sessionStorage中,并让其和vuex中的数据保持同步
      
    • 代码解决

      js 复制代码
      export default new Vuex.Store({
        state: {
          rightList: JSON.parse(sessionStorage.getItem('rightList') || '[]'),
          username: sessionStorage.getItem('username')
        },
        mutations: {
          setRightList(state, data) {
            state.rightList = data
            sessionStorage.setItem('rightList', JSON.stringify(data))
          },
          setUsername(state, data) {
            state.username = data
            sessionStorage.setItem('username', data)
          }
        },
        actions: {
      
        },
        getters: {
      
        }
      })
    • 退出按钮的逻辑

      js 复制代码
      logout() {
            // 删除sessionStorage中的数据
            sessionStorage.clear()
            this.$router.push('/login')
            // 删除vuex中的数据,让当前的界面刷新即可
            window.location.reload()
          },
2. 界面的控制
  1. 正常的逻辑是通过登录界面,登录成功之后跳转到管理平台界面,但是如果用户直接敲管理平台的地址,也是可以跳过登录的步骤,所以应该在某个时机判断用户是否登录
  • 如何判断是否登录

    js 复制代码
    login() {
        	// 登录时存储token
            sessionStorage.setItem('token', res.data.token)
        },
  • 什么时机

    • 路由导航守卫

      js 复制代码
      router.beforeEach((to, from, next) => {
        if (to.path === '/login') {
          next()
        } else {
          const token = sessionStorage.getItem('token')
          if (!token) {
            next('/login')
          } else {
            next()
          }
        }
      })
  1. 虽然菜单项已经被控制住了,但是路由信息还是完整的存在于浏览器,正比如zhangshan这个用户并不具备角色这个菜单,但是在地址栏中敲入/roles的地址,依然可以访问角色界面。
  • 路由导航守卫

    路由导航守卫固然可以在每次路由地址发生变化的时候,从vuex中取出rightList判断用户将要访问的界面有没有权限。不过从另一个角度来说,这个用户不具备权限的路由,是否也应该压根就不存在呢?

  • 动态路由

    • 登录成功之后动态添加
    js 复制代码
    // router.js
    import Vue from 'vue'
    import Router from 'vue-router'
    import Login from '@/components/Login.vue'
    import Home from '@/components/Home.vue'
    import Welcome from '@/components/Welcome.vue'
    import Users from '@/components/user/Users.vue'
    import Roles from '@/components/role/Roles.vue'
    import GoodsCate from '@/components/goods/GoodsCate.vue'
    import GoodsList from '@/components/goods/GoodsList.vue'
    import NotFound from '@/components/NotFound.vue'
    import store from '@/store'
    
    Vue.use(Router)
    
    // 动态路由规则映射
    const userRule = { path: '/users', component: Users }
    const roleRule = { path: '/roles', component: Roles }
    const goodRule = { path: '/goods', component: GoodsList }
    const categoryRule = { path: '/categories', component: GoodsCate }
    
    const ruleMapping = {
      'users': userRule,
      'roles': roleRule,
      'goods': goodRule,
      'categories': categoryRule
    }
    
    const router = new Router({
      routes: [
        {
          path: '/',
          redirect: '/welcome'
        },
        {
          path: '/login',
          component: Login
        },
        {
          path: '/home',
          component: Home,
          redirect: '/welcome',
          children: [
            { path: '/welcome', component: Welcome },
            // { path: '/users', component: Users },
            // { path: '/roles', component: Roles },
            // { path: '/goods', component: GoodsList },
            // { path: '/categories', component: GoodsCate }
          ]
        },
        {
          path: '*',
          component: NotFound
        }
      ]
    })
    // 动态路由
    export function initDynamicRoutes() {
      // 根据二级权限,对路由规则进行动态的添加
      console.log(router)
      const currentRoutes = router.options.routes
      const rightList = store.state.rightList
      rightList.forEach(item => {
        item.children.forEach(item => {
          // item 二级权限
          const temp = ruleMapping[item.path]
          currentRoutes[2].children.push(temp)
        })
      })
      currentRoutes.forEach(item => {
        router.addRoute(item)
      })
    }
    // 路由导航守卫 拦截没登录时的权限路由
    router.beforeEach((to, from, next) => {
      if (to.path === '/login') {
        next()
      } else {
        const token = sessionStorage.getItem('token')
        if (!token) {
          next('/login')
        } else {
          next()
        }
      }
    })
    
    export default router
    js 复制代码
    // Login.vue
    import { initDynamicRoutes } from "@/router.js"
    login() {
          this.$refs.loginFormRef.validate(async (valid) => {
            if (!valid) return
            const { data: res } = await this.$http.post('login', this.loginForm)
            if (res.meta.status !== 200) return this.$message.error('登录失败!')
            console.log(res)
            this.$store.commit('setRightList', res.rights)
            this.$store.commit('setUsername', res.data.username)
            sessionStorage.setItem('token', res.data.token)
            this.$message.success('登录成功')
    
            // 根据用户所具备的权限,动态添加路由规则
            initDynamicRoutes()
            
            this.$router.push('/home')
          })
        },
    • App.vue中添加,防止登录后再次刷新后重新路由规则重新加载,菜单被初始化
    js 复制代码
    export default {
      name: 'app',
      created() {
        initDynamicRoutes() //动态添加路由规则
      }
    }
3. 按钮的控制

​ 虽然用户可以看到某些界面了,但是这个界面的一些按钮,该用户可能是没有权限的,因此,我们需要对组件中的一些按钮进行控制。用户不具权限的按钮就隐藏或者禁用,而在这块中,可以把该逻辑放到自定义指令中

  • permission.js 注册自定义指令

    js 复制代码
    import Vue from 'vue'
    import router from '@/router.js'
    Vue.directive('permission', {
      inserted(el, binding) {
        console.log(binding)
        const action = binding.value.action
        const effect = binding.value.effect
        // 判断 当前路由所对应的组件中,如何判断用户是否具备action的权限
        console.log(router.currentRoute.meta)
        if (router.currentRoute.meta.indexOf(action) == -1) {
          if (effect === 'disabled') {
            el.disabled = true
            el.classList.add('is-disabled')
          } else {
            el.parentNode.removeChild(el)
          }
        }
      }
    })
  • main.js

    js 复制代码
    import './utils/permission.js' //引入到入口文件permission才会被加载
  • router.js 把路由元信息添加进来

    js 复制代码
    export function initDynamicRoutes() {
      // 根据二级权限,对路由规则进行动态的添加
      console.log(router)
      const currentRoutes = router.options.routes
      const rightList = store.state.rightList
      rightList.forEach(item => {
        item.children.forEach(item => {
          // item 二级权限
          const temp = ruleMapping[item.path]
          // 把路由元信息添加进来
          temp.meta = item.rights
          currentRoutes[2].children.push(temp)
        })
      })
      currentRoutes.forEach(item => {
        router.addRoute(item)
      })
    }
  • 使用自定义指令

    js 复制代码
    v-permission="{action:'add'}"
    v-permission="{action:'edit', effect:'disabled'}"
4. 请求和响应的控制
请求控制
  • 除了登录请求都要带上token,这样服务器才可以鉴别你的身份

    js 复制代码
    // http.js
    import axios from 'axios'
    import Vue from 'vue'
    // 配置请求的跟路径, 目前用mock模拟数据, 所以暂时把这一项注释起来
    // axios.defaults.baseURL = 'http://127.0.0.1:8888/api/private/v1/'
    
    axios.interceptors.request.use(req => {
      // console.log(req.url, req.method)
      if (req.url !== 'login') {
        // 不是登录的请求,我们应该在请求头中加入token数据
        req.headers.Authorization = sessionStorage.getItem('token')
      }
      return req
    })
    Vue.prototype.$http = axios
  • 如果发出了非权限内的请求,应该直接在前端范围内组织,虽然这个请求发送到服务器也会被拒绝

    js 复制代码
    // http.js
    import axios from 'axios'
    import Vue from 'vue'
    import router from '@/router.js'
    // 配置请求的跟路径, 目前用mock模拟数据, 所以暂时把这一项注释起来
    // axios.defaults.baseURL = 'http://127.0.0.1:8888/api/private/v1/'
    
    // 请求方式和权限的映射
    const actionMapping = {
      'get': 'view',
      'post': 'add',
      'put': 'edit',
      'delete': 'delete'
    }
    axios.interceptors.request.use(req => {
      // console.log(req.url, req.method)
      if (req.url !== 'login') {
        // 不是登录的请求,我们应该在请求头中加入token数据
        req.headers.Authorization = sessionStorage.getItem('token')
    
        // 判断非权限范围内的请求
        // router.currentRoute.meta
        // resful风格请求
        /*
          get请求     view
          post请求    add
          put请求     edit
          delete请求  delete
          [add view edit delete]
        */
        const action = actionMapping[req.method]
        // 判断 action 是否存在当前路由的权限中
        const rights = router.currentRoute.meta
        if (rights && rights.indexOf(action) === -1) {
          // 没有权限
          alert('没有权限')
          return Promise.reject(new Error('没有权限'))
        }
      }
      return req
    })
    Vue.prototype.$http = axios
响应控制
  • 得到了服务器返回的状态码401,代表token超时或者被篡改了,此时应该强制跳转登录页

    js 复制代码
    axios.interceptors.response.use((res) => {
      if (res.data.meta.status === 401) {
        router.push('/login')
        sessionStorage.clear()
        // 通过Vuex的actions或mutations来清空或重置存储在store中的登录相关状态。
        this.$store.dispatch('logout')
      }
      return res
    })

小结

前端权限的实现必须要后端提供数据支持,否则无法实现.

返回的权限数据的结构,前后端需要沟通协商,怎样的数据使用起来才最方便.

4.1.菜单控制

  • 权限的数据需要在多组件之间共享,因此采用vuex.

  • 防止刷新界面,权限数据丢失,所以需要存储在sessionStorage,并且要保证两者的同步

4.2.界面控制

  • 路由的导航守卫可以防止跳过登录界面

  • 动态路由可以让不具备权限的界面的路由规则压根就不存在

4.3.按钮控制

  • 路由规则中可以增加路由元数据meta

  • 通过路由对象可以得到当前的路由规则,以及存储在此规则中的meta数据.

  • 自定义指令可以很方便的实现按钮控制

4.4.请求和响应控制

  • 请求拦截器和响应拦截器的使用.

  • 请求方式的约定restful

相关推荐
.生产的驴9 分钟前
SpringBoot 消息队列RabbitMQ 消息确认机制确保消息发送成功和失败 生产者确认
java·javascript·spring boot·后端·rabbitmq·负载均衡·java-rabbitmq
布瑞泽的童话23 分钟前
无需切换平台?TuneFree如何搜罗所有你爱的音乐
前端·vue.js·后端·开源
白鹭凡35 分钟前
react 甘特图之旅
前端·react.js·甘特图
2401_8628867839 分钟前
蓝禾,汤臣倍健,三七互娱,得物,顺丰,快手,游卡,oppo,康冠科技,途游游戏,埃科光电25秋招内推
前端·c++·python·算法·游戏
书中自有妍如玉1 小时前
layui时间选择器选择周 日月季度年
前端·javascript·layui
Riesenzahn1 小时前
canvas生成图片有没有跨域问题?如果有如何解决?
前端·javascript
f8979070701 小时前
layui 可以使点击图片放大
前端·javascript·layui
小贵子的博客1 小时前
ElementUI 用span-method实现循环el-table组件的合并行功能
javascript·vue.js·elementui
明似水1 小时前
掌握 Flutter 中的 `Overlay` 和 `OverlayEntry`:弹窗管理的艺术
javascript·flutter