vue+element ui实现前端权限控制:菜单、界面、按钮、请求

1,293 阅读5分钟

一、权限分类:

  1. 后端权限:后端权限才是权限的关键,可以控制某个用户是否能够对数据进行增删改查等操作,后端可以通过cookie,session,token来识别发送请求的用户是谁,其核心是RBAC(role based access ctroll),即基于角色的权限控制,因为权限是由后端先分给角色,再由角色分给用户的
  2. 前端权限:控制视层的展示和前端所有发送的请求,一般权限是后端去实现就足够了,但是增加前端权限也会有一定的好处:
  • 降低非法操作的可能性:在页面中展示出一个就算点击了最终也会失败的按钮,会增加有心者非法操作的可能性,前端权限可以将页面的这类按钮失效或隐藏掉
  • 尽可能排除不必要请求,减轻服务器压力:没必要的请求,操作失败的请求,不具备权限的请求,应该压根就不需要发送,请求少了,自然也会减轻服务器的压力
  • 提高用户体验:根据用户具备的权限,为用户展示自己权限范围内的内容,避免在界面上给用户带来困扰,让用户关注自己分内之事

二、前端权限控制思路

  1. 菜单的控制:根据登录请求回来的包含有权限相关的数据,前端根据数据展示对应的菜单,点击菜单才能查看相关界面

  2. 界面的控制:

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

    如果用户已经登录,可是手动输入非权限内的地址,则需要跳转404界面

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

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

三、实现

1、菜单的控制

//./store/index.js
export default new Vuex.Store{
  state:{
    rightList:JSON.parse(sessionStorage.getItem('rightList') || '[]') //存储权限数据,方便共享给Home页面
    username:sessionStorage.getItem('username')  //登录后将用户名存储在sessionStorage
  }, 
  mutations:{
    setRightList(state,data){
      state.rightList=data  //Vuex中的数据只在Vue实例时执行一次,后续不再变化,所以需要手动赋值
      sessionStorage.setItem('rightList',JSON.stringify(data)) //将权限数据存储在本地,防止网页刷新导致Vuex中的数据丢失
    },
    setUsername(state,data){
      state.username=data
      sessionStorage.setItem('username',data)
    }
  }
}
//@/components/Login.vue中调用setRightList后将权限数据存储在vuex的state中,这样可以共享给Home页面
export default{
  methods:{
    login(){
      //进行表单验证,验证通过后发起登录请求,返回的数据中携带权限数据(此处省略详细代码)
      this.$store.commit('setRightList',res.rights)
      this.$store.commit('setUsername',res.data.username)
      this.$router.push('/home')
    }
  }
}
// @/components/Home.vue
<template>
  <button @click="logout">{{username}}退出</button>
</template>
<script> 
  import {mapState}from 'vuex'
  export default{
    data(){
      return{
        menuList:[] //存储vuex的state获取来的权限数据
      } 
    },
    computed:{
      ...mapState(['rightList','username'])
    },
    created(){
      this.menuList=this.rightList
    },
    methods:{
      logout(){
        //退出要删除sessionStorage中的数据
        sessionStorage.clear() 
        this.$router.push('/login')
        //退出也要删除Vuex中的数据,让当前页面刷新即可
        window.location.reload()
      }
    }
  }
</script>

2、界面的控制:通过路由导航守卫+动态路由添加

//./Login.vue
<script>
  import  {initDynamicRoutes} from '@/router.js'
  
  export default{
    method:{
      login(){
        sessionStorage.setItem('token',res.data.token)
        //根据用户所具备的权限,动态添加路由规则
        initDynamicRoutes()
      }
    }
  }
</script>
//./router.jsimport {store} from '@/store'//将要不同用户不同权限的页面的路由提前声明成一个变量
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:[{
    ...  //各种路由配置略
  }]
    
  router.beforeEach((to,from,next)=>{
    //如果是跳转到登录页就放行
    if(to.path==='/login'){
      next()
    }else{
      //如果没有token说明没有登录,要跳转到登录页
      const token=sessionStorage.getItem('token')
      if(!token){
        next('/logi n')
      }else{
        //其他情况就放行
        next()
      }
    }
  })
})
export function initDynamicRoutes(){
  //根据二级权限动态添加路由规则
  const currentRoutes=router.options.routes
  //currentRoutes[2].children.push()     //在'/home'下的children动态添加路由规则
  const rightList=store.state.rightList
  rightList.forEach(item=>{ 
    item.children.forEach(item=>{
      const temp=ruleMapping[item.path]
      //将权限数据中的权限rights赋值给路由规则的元数据(第三级、按钮的控制用到)
      temp.meta=item.rights
      currentRoutes[2].children.push(temp) 
    })
  })
  router.addRoute(currentRoutes)
}

因为刷新页面后,router.js也会重新加载,路由规格就只剩初始设置的路由,动态添加的路由规则丢失,动态路由只有登录成功后才添加,所以要在App.vue再次调用initDynamicRoutes,此时刷新页面运行的就是根组件中的initDynamicRoutes,动态添加的路由不会丢失

<script>
  import  {initDynamicRoutes} from '@/router.js'
  export default{
    created(){
      initDynamicRoutes()
    }
  }
</script>

3、按钮的控制:对用户不具备权限的按钮进行隐藏或者禁用,此时可以通过自定义指令来完成 下图为后端返回的数据格式:

截屏2022-11-01 23.37.29.png

//@/components/user.vue
<template>
  <div>
    //添加按钮
    <el-button type="primary" 
               @click="addDialogVisible"
               v-permission="{action:'add',effect:'disabled'}"
               >添加用户</el-button>
    //修改按钮
    <el-button type="primary"
               icon="el-icon-edit"
               v-permission="{action:'edit',effect:'disabled'}"
               size="mini"></el-button>
    //删除按钮
    <el-button type="danger"
               icon="el-icon-delete"
               size="mini"
               v-permission="{action:'delete',effect:'disabled'}"
               @click="removeById(scope.row.id)"></el-button>
  </div>
</template>
//@/utils/permission.js
import Vue from 'vue'
import router from '@/router.js'
Vue.directive('permission',{
  //当前指令被插入时调用inserted方法
  inserted(el,binding){
    const action=binding.value.action
    const effect=binding.value.effect
    //判断当前路由所对应的组件中,如何判断用户是否具备action的权限:
    //1、删除元素:如果当前路由规则的元数据中无该权限,则删除该标签的父元素中的该标签
    //2、禁用元素:如果binding.effect值为disabled,则禁用该标签
    if(router.currentRoute.meta.indexOf(action)==-1){ 
      if(effect==='disabled'){
        el.disabled=true
        //在element ui环境下还要多加以下一步
        el.classList.add('is-disabled')
      }else{
        el.parentNode.removeChild(el)
      }
    }
  }
})
//main.js中引入permission.js
import '@/utils/permission.js'

4、请求和响应的控制:如果发生非权限内的请求,应该直接在前端范围内阻止,虽然这个请求发到服务器也会被拒绝

//除了登录请求都要带上token,这样服务器才能鉴别用户身份
//对axios二次封装,方便在请求和响应时做一些操作
//http.js
import axios from 'axios'
import Vue from 'vue'
import router from '../router.js'
const service = axios.create({
    baseURL: '/api', // baseURL会自动加在请求地址上
    timeout: 3000
})
//创建请求方法和对应权限的映射
const actionMapping={
  get:'view',
  post:'add',
  put:'edit',
  delete:'delete'
}
//请求拦截
service.interceptors.request.use(function(req){
  if(req.url!==='login'){
    req.headers.Authorization=sessionStorage.getItem('token')
    const action=actionMapping[req.method]
    //判断非权限范围内的请求
    const currentRight=routes.currentRoute.meta
    if(currentRight && currentRight.indexOf(action)==-1){
      alert('没有权限')
      return Promise.reject(new Error('没有权限'))
    }
    //判断当前请求的行为
    //restful风格的请求:get请求 view;post请求 add;put请求 edit;delete请求 delete
  }
  return req
})
//响应拦截:比如自己修改控制台的token,再去查看页面应该自动跳到登录页面
service.interceptors.response.use(function(res)=>{
  if(res.data.meta.status===401){
    router.push('/login')
    window.location.reload()
  }
})
export default service