vue后台管理系统权限控制

vue权限控制
在Web系统中,权限很久以来一直都是后端程序所控制的,为什么呢?因为Web系统的本质围绕的是数据,而和数据库最紧密接触的是后端程序,所以在很长的一段时间内,权限一直都只是后端程序需要考虑的话题,但是随着前后端分离架构的流行,越来越多的项目也在前端进行权限控制

1.权限相关概念

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

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

cookie
session
token

2.后端权限设计RBAC

用户
角色
权限

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

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

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

2.前端权限控制思路

2.1菜单的控制
在登录请求中,会得到权限数据,当然,这个需要后端返回数据的支持,前端根据权限数据,展示对应的菜单,点击菜单才能查看相关的界面

2.2界面的控制
如果用户没有登录,手动在地址栏敲入管理界面的地址,则需要跳转到登录界面
如果用户已经登录,可是手动敲入非权限内的地址,则需要跳转404界面

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

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

3.Vue的权限控制实现

3.1菜单的控制

  • 查看登录之后获取到的数据
{
            data: {
              id: 500,
              rid: 0,
              username: "admin",
              mobile: "13999999999",
              email: "12399@qq.com",
              token: "123123123123",
            },
            right: [
              {
                id: 125,
                authName: "用户管理",
                icon: "icon-user",
                children: [
                  {
                    id: 110,
                    authName: "用户列表",
                    path: "users",
                    right: ["view", "edit", "add", "delete"],
                  },
                ],
              },
              {
                id: 103,
                authName: "角色管理",
                icon: "icon-tijikongjian",
                children: [
                  {
                    id: 111,
                    authName: "角色列表",
                    path: "roles",
                    right: ["view", "edit", "add", "delete"],
                  },
                ],
              },
              {
                id: 101,
                authName: "商品管理",
                icon: "icon-shangpin",
                children: [
                  {
                    id: 104,
                    authName: "商品列表",
                    path: "goods",
                    right: ["view", "edit", "add", "delete"],
                  },
                  {
                    id: 121,
                    authName: "商品分类",
                    path: "categories",
                    right: ["view", "edit", "add", "delete"],
                  },
                ],
              },
            ],
            meta: {
              msg: "登录成功",
              status: 200,
            },
          };

在这部分数据中,除了该用户的基本信息之外,还有两个字段很关键
1.token,用户前端用户的状态保持
2.rights: 该用户具备的权限数据一级权限就对应一级菜单,二级权限就对应二级菜单

  • 根据rights中的数据,动态渲染左侧菜单栏,数据在Login.vue得到,但是在Home.vue才使用,所以可以把数据用vuex进行维护

1.vuex中的代码

export default new Vuex.Store({
    state:{
        rightList:[]
    },
    mutations:{
        setRightList(state,data){
            state.rightList = data
        }
    },
    actions:{},
    getters:{}
})

2.Login.vue的代码

login(){
    //调接口成功后
    ......
    this.$store.commit('setRightList',res.rights)
    this.$message.success('登录成功')
    this.$router.push('/home')
}

3.Home.vue的代码

import { mapState } from "vue";
computed: {
    ...mapState(["rightList"]),
},
created() {
    this.activePath = window.sessionStorage.getItem("activePath");
    // 初始化菜单栏的数据
    this.menuList = this.rightList;
},
  • 刷新界面菜单消失

1.原因分析

因为菜单数据是登录之后才获取到的,获取菜单数据之后,就存放在vuex中
一旦刷新界面,Vuex中的数据会重新初始化,所以会变成空的数组
因此,需要将权限数据存储在sessionStorage中,并让其和Vuex中的数据保持同步代码片

2.代码解决

export default new Vuex.Store({
    state:{
        rightList: JSON.parse(sessionStorage.getItem('rightList') || '[]'),
    },
    mutations:{
        setRightList(state,data){
            state.rightList = data
            sessionStorage.setItem('rightList', JSON.stringify(data))
        }
    },
    actions:{},
    getters:{}
})
  • 识别用户名,方便查看当前用户

1.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:{}
})

2.Home.vue的代码

computed: {
    ...mapState(["rightList", "username"]),
},
<el-button type="info" @click="logout">{{ username }} 退出</el-button>
  • 退出按钮的逻辑
logout() {
      window.sessionStorage.clear();
      this.$router.push("/login");
      window.location.reload();
},

3.2界面的控制
1.正常的逻辑是通过登录界面,登录成功之后跳转到管理平台界面,但是如果用户直接敲入管理平台的地址,也是可以跳过登录的步骤,所以应该在某个时机判断用户是否登录

  • 如何判断是否登录
sessionStorage.setItem('token',res.data.token)
  • 什么时机
    路由导航守卫
router.beforeEach((to, from, next) => {
  //to将要访问的路径
  //from代表从哪个路径跳转而来
  //next是一个函数,表示放行
  //next()放行 next('/login')强制跳转
  if (to.path === '/login') return next();
  const tokenStr = window.sessionStorage.getItem('token')
  if (!tokenStr) return next('/login')
  next();
})

2.虽然菜单项已经被控制了,但是路由信息还是完整的存在于浏览器,正比如zhangsan这个用户并不具备角色这个菜单,但是他如果自己在地址栏中敲入/welcome的地址,依然也可以访问角色界面

  • 路由导航守卫
    路由导航守卫固然可以在每次路由地址发生变化的时候,从vuex中取出rightList判断用户将要访问的界面,这个用户到底有没有权限,不过从另外一个角度来说,这个用户不具备权限的路由,是否也应该压根就不存在呢?
  • 动态路由
    登录成功之后动态添加
    App.vue中添加
    代码如下: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/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/index'


Vue.use(Router)


const userRule = { path: '/users', component: Users }
const roleRule = { path: '/roles', component: Roles }
const goodsRule = { path: '/goods', component: GoodsList }
const categoryRule = { path: '/categories', component: GoodsCate }


const ruleMapping = {
  'users': userRule,
  'roles': roleRule,
  'goods': goodsRule,
  'categories': categoryRule
}


const router = new Router({
  routes: [
    {
      path: '/',
      redirect: '/home'
    },
    {
      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
    }
  ]
})


router.beforeEach((to, from, next) => {
  if (to.path === '/login') {
    next()
  } else {
    const token = sessionStorage.getItem('token')
    if (!token) {
      next('/login')
    } else {
      next()
    }
  }
})

export function initDynamicRoutes() {
  const currentRoutes = router.options.routes
  const rightList = store.state.rightList
  rightList.forEach(item => {
    if (item.childList) {
      if (item.childList) {
        item.childList.forEach(item => {
          if (ruleMapping[item.path]) {
            currentRoutes[1].children.push(ruleMapping[item.path])
          }
        })
      }
    }
  })
  router.addRoutes(currentRoutes)
}


export default router
  • Login.vue
import { initDynamicRoutes } from '../router'
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('登陆失败!')
    this.$store.commit('setRightList', res.rights)
    this.$store.commit('setUsername', res.data.username)
    sessionStorage.setItem('token', res.data.token)
    initDynamicRoutes()
    this.$message.success('登陆成功')
    this.$router.push('/home')
  })
}
  • App.vue(页面刷新后,页面会空白,在App.vue中调用一下解决此问题)
import { initDynamicRoutes } from '../router'
export default{
  name:'app',
  created(){
    initDynamicRoutes()
  }
}

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

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

main.js

import './utils/permission.js'
  • router.js
export function initDynamicRoutes() {
  const currentRoutes = router.options.routes
  const rightList = store.state.rightList
  rightList.forEach(item => {
    if (item.childList) {
      if (item.childList) {
        item.childList.forEach(item => {
          if (ruleMapping[item.path]) {
            const temp = ruleMapping[item.path]
            temp.meta = item.right
            currentRoutes[1].children.push(temp)
          }
        })
      }
    }
  })
  router.addRoutes(currentRoutes)
}
  • 使用指令
//第一种方法:按钮完全消失
<el-button
  type="primary"
  @click="addDialogVisible = true"
  v-permission="{ action: 'add' }"
>
    添加用户
</el-button>

//第二种方法:按钮禁用状态
<el-button
  type="primary"
  @click="addDialogVisible = true"
  v-permission="{ action: 'add', effect: 'disabled' }"
>
  添加用户
</el-button>

3.4请求和响应的控制
请求控制

  • 除了登录请求都得要带上token,这样服务器才可以鉴别你的身份
axios.interceptors.request.use((req) => {
    if (req.url !== login) {
        // 不是登录的请求,我们应该在请求头中,加入token数据
        req.headers.Authorization = sessionStorage.getItem('token')
    }
    return req
})
  • 如果发出了非权限内的请求,应该直接在前端范围内阻止,虽然这个请求发到服务器也会被拒绝
import axios from 'axios'
import Vue from 'vue'
import router from '../router'
// 配置请求的根路径,目前mock模拟数据,所以暂时把这一项注释起来
// axios.defaults.baseURL = 'http://127.0.0.1:8888/api/privates/v1/'


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


axios.interceptors.request.use((req) => {
    if (req.url !== login) {
        // 不是登录的请求,我们应该在请求头中,加入token数据
        req.headers.Authorization = sessionStorage.getItem('token')
        // 判断非权限范围内的请求(解决:改变浏览器按钮中的禁用状态,仍可点击问题)
        // 判断当前模块中具备的权限
        // restful风格请求
        // get请求 view
        // post请求 add
        // put请求 edit
        // delete请求 delete
        // [add view edit delete]
        const action = actionMapping(req.method)
        const currentRight = router.currentRoute.meta
        if (currentRight && currentRight.indexOf(action) === -1) {
            // 没有权限
            alert('没有权限')
            return Promise.reject(new Error('没有权限'))
        }
    }
    return req
})
axios.interceptors.response.use(function (res) {
    return res
})


Vue.prototype.$http = axios

响应控制

  • 得到了服务器返回的状态码401,代表token超时或者被篡改了,此时应该强制跳转到登录界面
axios.interceptors.response.use((res) => {
    if (res.data.meta.status === 401) {
        router.push('/login')
        sessionStorage.clear()
        window.location.reload()
    }
    return res
})

4.小结
前端权限的实现必须要后端提供数据支持,否则无法实现
返回的权限数据的结构,前后端需要沟通协商,怎样的数据使用起来才最方便

4.1菜单控制

  • 权限的数据需要在多组件之间共享,因此采用vuex
  • 防止刷新界面,权限数据丢失,所以需要存储在sessionStorage,并且要保证两者的同步

4.2界面控制

  • 路由的导航守卫可以防止跳过登陆界面
  • 动态路由可以让不具备权限的界面的路由规则压根就不存在

4.3按钮控制

  • 路由规则中可以增加路由元数据meta
  • 通过路由对象可以得到当前的路由规则,以及存储在此规则中的meta数据
  • 自定义指令可以很方便的实现按钮控制

4.4请求和响应控制

  • 请求拦截器和响应拦截器的使用
  • 请求方式的约定restful
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值