Vue权限控制管理

  • A+
所属分类:脚本语言
1.1 权限的分类
  • 后端权限

    ​ 从根不上讲前端仅仅只是视图层的展示, 权限的核心是在于服务器中的数据变化,所以后端才是权限的关键,后端权限可以控制某个用户是否能够查询数据, 是否能够修改数据等操作。

    1.后端如何知道该请求是哪个用户发过来的

    ​ cookie

    ​ session

    ​ token

    2.后端的权限设计RBAC(后面再了解)

    ​ 用户

    ​ 角色

    ​ 权限

    • 前端权限

      前端权限的控制本质上来说,就是控制端的视图层的展示和前端所发送的请求。但是只有前端权限控制没有后端权限控制是万万不可的。前端权限控制只可以说是达到锦上添花的效果。

    1.2 前端权限的意义

    ​ 如果仅从能够修改服务器中数据库中的数据层面上讲, 确实只在后端做控制就足够了, 那为什么越来越多的项目也进行了前端权限的控制, 主要有这几方面的好处。

    • 降低非法操作的可能性
      不怕赃偷就怕贼惦记, 在页面中展示出一个就算点击了也最终会失败的按钮,势必会增加有心者非法操作的可能性。
    • 尽可能排除不必要清求, 减轻服务器压力
      没必要的请求, 操作失败的清求, 不具备权限的清求, 应该压根就不需要发送, 请求少了, 自然也会减轻服务器的
      压力。
    • 提高用户体验
      根据用户具备的权限为该用户展现自己权限范围内的内容, 避免在界面上给用户带来困扰, 让用户专注于分内之事。

    二、前端权限的控制思路

    2.1菜单(侧边栏)的控制

    在登录请求中,会得到权限数据,当然,这个需要后端返回数据的支持(我们也可以自己模拟数据),前端根据权限数据,展示对应的菜单,点击菜单,才能查看到相关的界面。

    2.2界面的控制

    如果用户没有登录,手动在地址栏敲入管理界面的地址,则需要跳转到登录界面。

    如果用户已经登录,如果手动敲入非权限内的地址,则跳转不过去或者需要跳转到404界面。

    2.3按钮的控制

    在某个菜单的界面中,还得根据权限数据,展示出可进行操作的按钮,比如删除,修改,增加。

    2.4请求和响应的控制

    如果用户通过非常规操作,比如通过浏览器调试工具将某些禁用的按钮变成启用状态,此时发的请求,也应该被前端所拦截。

    三、实现步骤

    3.1权限菜单栏控制

    在src文件夹下新建一个mock文件夹,在mock文件夹下面新建一个js文件,然后在控制台输入npm install mockjs安装mockjs模块,之后在main.js中导入mock文件。在mock文件下写一个模拟数据,分配两个账户,分别有各自的菜单列表和token。

    代码如下:

    //使用Mock
    const Mock = require('mockjs')
    Mock.setup({
        timeout: '500-1000'
    })
    
    // 用户信息
    
    const users = [
        {
            id: 1,
            username: 'user',
            password: '123456',
            photo: 'https://ss1.bdstatic.com/70cFvXSh_Q1YnxGkpoWK1HF6hhy/it/u=2667925509,1048662418&fm=11&gp=0.jpg',
            token: 'user-token',
            role: 'user',
            rights: []
        },
        {
            id: 2,
            username: 'admin',
            password: '123456',
            photo: 'https://ss1.bdstatic.com/70cFvXSh_Q1YnxGkpoWK1HF6hhy/it/u=2364244149,3298797080&fm=26&gp=0.jpg',
            token: 'admin-token',
            role: 'admin',
            rights: []
        }
    ]
    
    // 权限信息 将权限信息从用户信息中抽离出来 不同身份对应不同的路由信息
    // 这样方便了后期的维护 否则以后每加一个页面就需要在每个用户信息中做更改 十分的不方便 如果用户多了更是增添了不必要的麻烦
    // 将用户直接分为不同身份 然后对不同身份做处理 这样比较合理
    const roles = {
        user: [
            {
                id: 1,
                authName: '评估管理',
                icon: 'el-icon-connection',
                children: [
                    {
                        id: 11,
                        authName: '估价师',
                        icon: 'el-icon-s-grid',
                        path: 'appraiser',
                        rights: ['view']
                    },
                    {
                        id: 12,
                        authName: '估价师库',
                        icon: 'el-icon-s-marketing',
                        path: 'appraiserLibrary',
                        rights: ['view']
                    }
                ]
            }
        ],
        admin: [
            {
                id: 1,
                authName: '评估管理',
                icon: 'el-icon-connection',
                children: [
                    {
                        id: 11,
                        authName: '估价师',
                        icon: 'el-icon-s-grid',
                        path: 'appraiser',
                        rights: ['view', 'edit', 'add', 'delete']
                    },
                    {
                        id: 12,
                        authName: '估价师库',
                        icon: 'el-icon-s-marketing',
                        path: 'appraiserLibrary',
                        rights: ['view', 'edit', 'add', 'delete']
                    }
                ]
            },
            {
                id: 2,
                authName: '项目管理',
                icon: 'el-icon-set-up',
                children: [
                    {
                        id: 21,
                        authName: '项目登记',
                        icon: 'el-icon-s-custom',
                        path: 'ProjectRegistration',
                        rights: ['view', 'edit', 'add', 'delete']
                    },
                    {
                        id: 22,
                        authName: '项目查询',
                        icon: 'el-icon-s-custom',
                        path: 'ProjectQuery',
                        rights: ['view', 'edit', 'add', 'delete']
                    }
                ]
            }
        ]
    }
    
    
    // 用户登录
    Mock.mock('/login', 'post', option => {
        const { username, password } = JSON.parse(option.body)
        const user = users.find(item => {
            return item.username === username && item.password === password
        })
        return user
    })
    
    // 用户权限信息
    Mock.mock('/roles', 'post', option => {
        return roles[option.body]
    })
    js

然后我们把这个数据存放在vuex中,然后主页根据vuex中的数据进行菜单列表的渲染。

问题:刷新界面vuex数据消失,菜单栏消失。

解决:将数据存储在sessionStorage中,并让其和vuex中的数据保持同步。

vuex的代码如下:

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


Vue.use(Vuex)

export default new Vuex.Store({
    state: {
        role: sessionStorage.getItem('role'),
        rightList: JSON.parse(sessionStorage.getItem('rightList') || '[]'),//防止刷新侧边栏数据消失
        username: sessionStorage.getItem('username'),
        photo: sessionStorage.getItem('photo')
    },
    mutations: {
        setRole (state, data) {
            state.role = data
            sessionStorage.setItem('role', data)
        },
        setRightList (state, data) {
            state.rightList = data
            sessionStorage.setItem('rightList', JSON.stringify(data))
        },
        setUsername (state, data) {
            state.username = data
            sessionStorage.setItem('username', data)
        },
        setPhoto (state, data) {
            state.photo = data
            sessionStorage.setItem('photo', data)
        }
    },
    actions: {},
    getters: {}
})
js

将mock里面的数据存入vuex后,我们需要在Login.vue的登录按钮事件中设置点击登录后将数据传入我们登录后的页面中。

此时我们在登录按钮的事件中这样写:

submitForm(form) {
    this.$refs[form].validate((valid) => {
        if (valid) {
            this.loading = true
            login(this.form).then(res => {
                // console.log(res, 'login=>res')
                // 将用户身份存入vuex 普通用户身份: user 管理员用户身份: admin
                this.$store.commit('setRole', res.data.role)

                this.$store.commit('setUsername', res.data.username)
                this.$store.commit('setPhoto', res.data.photo)
                sessionStorage.setItem('token', res.data.token)

                getRoles(res.data.role).then(ret => {
                    // console.log(ret.data, 'getRoles=>ret.data')
                    // 将对应身份下的路由存储到vuex
                    this.$store.commit('setRightList', ret.data)
                    this.loading = false
                    this.$message.success('登陆成功')

                    // 根据用户所具备的权限 动态添加路由规则
                    initDynamicRoutes()
                    this.$router.push('/frame')
                })
            })
        } else {
            this.$message.error('用户名或密码错误');
            return false;
        }
    });
},
js

传入框架页面之后,我们需要在frame(框架页面遍历侧边栏的菜单数据数组,使其显示对应用户的菜单列表),先导入相应的数据,

<script>
    import { mapState } from 'vuex'
    import {initDynamicRoutes} from "@/router";
    export default {
js
computed: {
    ...mapState(['rightList']),//侧边栏数据
    ...mapState(['username']), // 用户名
    ...mapState(['photo']) // 用户头像
    //三个可以写在一起!
},
created () {
    // 初始化menulist菜单栏的数据
    // this.menulist = this.rightList
            initDynamicRoutes()
            this.menuList = this.rightList

},
js

这样的话rightList数组数据就传给menuList,此时我们只需要将menuList遍历出来到侧边栏中就可以了。

<!-- 动态路由 -->
<!-- 一级菜单 -->
<el-submenu
        :index="item.id+''"
        v-for="item in menuList"
        :key="item.index"
>
    <template slot="title">
        <span>{{item.authName}}</span>
    </template>
    <!--二级菜单-->
        <el-menu-item
                :index="'/'+subItem.path"
                v-for="subItem in item.children"
                :key="subItem.index"
        >
            <template >
                <i :class="subItem.icon"></i>
                <span>{{subItem.authName}}</span>
            </template>
        </el-menu-item>
</el-submenu>
html
3.2 界面的控制

登录成功后,将token数据存储在sessionStorage中,判断是否登录。

1.路由导航守卫
// 路由导航守卫
router.beforeEach((to, from, next) => {
    if (to.path === '/Login') {
        next()
    } else {
        const token = sessionStorage.getItem('token')
        if (!token) {
            next('/Login')
        } else {
            next()
        }
    }
})
js

问题:这样用户在登录之后就可以访问其他界面了,但如果用户A登录之后他只能访问a页面,他不能访问b页面,但是这时候他还是可以通过地址栏输入进入到b页面。

解决:当然我们也可以设置路由导航守卫,但是如果有多个页面,设置会非常不方便,并且对于用户A来说,她是不用访问b页面的,这时候我们干嘛不设置登录A用户时只显示A用户所要显示的路由呢,这时候我们用到动态路由。

2.动态路由

根据当前用户所拥有的权限数据动态添加所需要的路由。

1.先定义好所有的路由规则

//动态路由
    const ProjectQueryRule={
        path: '/ProjectQuery',
        name:'ProjectQuery',
        component:()=>import('@/views/projectManagement/ProjectQuery'),
    }
    const ProjectRegistrationRule={
            path: '/ProjectRegistration',
            name:'ProjectRegistration',
            component:()=>import('@/views/projectManagement/ProjectRegistration'),

        }
    const appraiserRule={
                path: '/appraiser',
                name:'appraiser',
                component:()=>import('@/views/evaluateManagement/appraiser'),

            }
    const appraiserLibraryRule={
            path: '/appraiserLibrary',
            name:'appraiserLibrary',
            component:()=>import('@/views/evaluateManagement/appraiserLibrary'),

        }
/**
 * eslint报错
 * error Unnecessarily quoted property 'xxx' found quote-props
 * 表示 key没必要加引号 'table': tableRule => table: tableRule
 */

// 路由规则和字符串的映射关系
const ruleMapping = {
    ProjectQuery: ProjectQueryRule,
    ProjectRegistration: ProjectRegistrationRule,
    appraiser: appraiserRule,
    appraiserLibrary:appraiserLibraryRule
}
js

2.登录之后动态添加路由,注意这个initDynamicsRoutes的方法需要暴露出去在登录页面调用。

export function initDynamicRoutes () {
    // console.log(router)
    // 根据二级权限 对路由规则进行动态的添加

    const currentRoutes = router.options.routes
    //console.log(currentRoutes)
    // currentRoutes[2].children.push()
    const rightList = store.state.rightList
    // console.log(rightList)
    rightList.forEach(item => { // 如果是没有子路由的话 就直接添加进去 如果有子路由的话就进入二级权限遍历
        // console.log(item, 'item-1')
        if (item.path) {
            const temp = ruleMapping[item.path]
            // 路由规则中添加元数据meta
            temp.meta = item.rights
            currentRoutes[1].children.push(temp)
        }
        item.children.forEach(item => {
            // item 二级权限
            // console.log(item, 'item-2')
            const temp = ruleMapping[item.path]
            // 路由规则中添加元数据meta
            temp.meta = item.rights
            //console.log(temp.meta)
            currentRoutes[2].children.push(temp)
        })
    })
     console.log(currentRoutes)
    router.addRoutes(currentRoutes)
}
js

这样当用户A在地址栏输入自己不能访问的路由时,则不会跳转到该页面,跳转到空白页面或者404页面。

**问题:**如果我们重新刷新的话动态路由就会消失,动态路由是在登录成功后才会调用的,刷新的时候并没有调用,所以动态路由没有添加上。

解决:可以在app.vue(框架页面)中created中调用添加动态路由的方法

created(){
          initDynamicRoutes()
  }
js
3.3按钮的控制

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

这时我们需要根据我们mock设置的right属性来判断用户有什么权限。

Vue权限控制管理

添加自定义指令控制按钮

<!--如果添加估价师这个按钮有添加的权限,那就显示这个按钮,如果没有添加的权限,那就隐藏这个按钮。-->
<el-button type="primary" @click="addAppraiser()" v-permission="{action:'add'}">添加估价师</el-button>

 <!--如果编辑这个按钮有添加的权限,那就显示这个按钮并且能点击,如果没有添加的权限,那就禁用这个按钮。-->
<el-button type="primary" size="mini" @click="handleEditBtn(row)" v-permission="{action:'add',effect:'disabled'}">编辑</el-button>

html

这样设置之后还不能实现效果,我们需要给这个设置对应的指令。

在util文件夹下新建permission.js文件,在里面设置我们这个自定义指令

// 自定义指令的注册
import Vue from 'vue'
import router from '@/router'
Vue.directive('permission', {
  inserted (el, binding) {
    // console.log(el)
    // 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) { // 等于-1说明没找到 不具备权限
      if (effect === 'disabled') {
        el.disabled = true
        el.classList.add('is-disabled')
      } else {
        el.parentNode.removeChild(el)
      }
    }
  }
})
js

这个按钮权限是通过meta属性传入的。

item.children.forEach(item => {
    // item 二级权限
    // console.log(item, 'item-2')
    const temp = ruleMapping[item.path]
    // 路由规则中添加元数据meta
    temp.meta = item.rights
    //console.log(temp.meta)
    currentRoutes[2].children.push(temp)
})
js

设置完之后我们需要在main.js中导入import ‘./utils/permission.js’

3.4 请求和响应的控制

请求控制

  • 除了登录请求都要带上token,这样服务器才可以鉴别我们的身份。

此时我们需要用到axios的请求拦截设置

在util文件夹上新建request.js文件,加入以下代码

// 请求拦截器
request.interceptors.request.use(req => {
  // console.log(req.url)
  // console.log(req.method)
  if (req.url !== '/login' && req.url !== '/roles') {
    // 不是登录的请求 也不是获取权限的请求 则在请求头中加入token  不知道如何使用Mock来验证请求头中的token 故此处注释
    // req.headers.Authorization = sessionStorage.getItem('token')
    const action = actionMapping[req.method]
    // 判断非权限范围内的请求
    // console.log(router)
    const currentRight = router.currentRoute.meta
    // console.log(currentRight)
    if (currentRight && currentRight.indexOf(action) === -1) {
      // 没有权限
      alert('没有权限')
      return Promise.reject(new Error('没有权限'))
    }
  }
  return req
})
js

如果发出了非权限内的请求,应该直接在前端范围内阻止,虽然这个请求发到服务器也会被拒绝。

**非权限内的请求:**比如A用户是不能够操作该页面的按钮的,但是他通过f12调试把按钮改为可点击,如果我们不对这个请求进行处理,那么这个请求就会发送出去。

如果我们得到了服务器返回的状态码为401,代表token超时或者被篡改了,此时应该强制跳转到登录界面。request.js的完整代码如下。

import axios from 'axios'
import router from '@/router'

const request = axios.create()

// 映射
const actionMapping = {
  get: 'view',
  post: 'add',
  put: 'edit',
  delete: 'delete'
}

// request.defaults.baseURL = 'http://127.0.0.1:7001' // 注释掉之后调的接口将是Mock数据

// 请求拦截器
request.interceptors.request.use(req => {
  // console.log(req.url)
  // console.log(req.method)
  if (req.url !== '/login' && req.url !== '/roles') {
    // 不是登录的请求 也不是获取权限的请求 则在请求头中加入token  不知道如何使用Mock来验证请求头中的token 故此处注释
    // req.headers.Authorization = sessionStorage.getItem('token')
    const action = actionMapping[req.method]
    // 判断非权限范围内的请求
    // console.log(router)
    const currentRight = router.currentRoute.meta
    // console.log(currentRight)
    if (currentRight && currentRight.indexOf(action) === -1) {
      // 没有权限
      alert('没有权限')
      return Promise.reject(new Error('没有权限'))
    }
  }
  return req
})

// 响应拦截器
request.interceptors.response.use(res => {
  // console.log(res)
  if (res.data.status === 401) {
    router.push('/login')
    sessionStorage.clear()
    window.location.reload()
  }
  return res
})

export default request
js

同时也别忘了在main.js中导入。

四、小结

前端权限的实现之后需要后端提供数据支持,否则无法实现。

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

4.1 菜单的控制

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

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

4.2 界面的控制

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

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

4.3 按钮控制

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

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

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

4.4 请求和响应的控制

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

  • 我的微信
  • 这是我的微信扫一扫
  • weinxin
  • 我的微信公众号
  • 我的微信公众号扫一扫
  • weinxin