后台管理系统—权限控制

979 阅读4分钟

前端权限控制的意义

前端权限是视图层的展示,真正的权限控制是后端数据的变化。前端权限控制的展示是为了更好的用户体验。 后台系统权限控制,主要体现在四个方面

  1. 菜单控制
  2. 界面控制
  3. 按钮控制
  4. 请求和响应控制

如何实现权限控制

一、菜单控制

  • Login组件:用户登录时获取后台返回的权限菜单列表,不同用户所拥有的权限是不一样的。 image-20220405111917089.png
<el-menu
    :unique-opened="true"
    :collapse="isCollapse"
    router
    :default-active="activePath">
    <!-- 一级菜单 -->
    <el-submenu 
    :index="item.id + ''" 
    v-for='item in menulist' 	//循环遍历获得的权限数据一级:key="item.id">
        <template slot="title">
            <i :class="iconObj[item.id]"></i>
            <span>{{item.authName}}</span>
    	</template>
    <!-- 二级菜单 -->
  	 <el-menu-item 
        :index="subItem.path + ''" 
        v-for='subItem in item.children' 	//(二级:key="subItem.id"
        @click="saveNavState(subItem.path + '' )">	//保存菜单栏的激活状态
        <template slot="title">
            <i class="el-icon-menu"></i>
            <span>{{subItem.authName}}</span>
   	    </template>
    </el-menu-item>
  </el-submenu>
</el-menu>

<script>
    login(){
    this.$refs.loginFormRef.validate(async (valid)=>{
        if(!valid) return;  
        const {data : res}  = await this.$http.post('/login', this.loginForm)
        this.$store.commit("setRightList", res.rights)	权限数据——存放到Vuex的state里
        this.$store.commit("setUsername", res.data.username)
        sessionStorage.setItem('token',res.data.token)
        this.$router.push('/home')  
    })
}
</script
  • Home组件:左侧菜单栏需要权限菜单列表对数据进行展示 image-20220405112114576.png
import { mapState } from "vuex"
computed: {
    ...mapState(['rightList','username'])
},
created() {
    this.menuList = this.rightList	初始化Home组件的菜单数据
}
  • 同一个数据【权限菜单列表】需在不同的组件中使用,可用Vuex进行数据的状态管理
const router = new Vuex.Store({
    routes: {
        state: {
            // 一开始session(刷新页面情况下使用)中无数据,会报错。 所以放一个空数组(登录情况使用)
            rightList: JSON.parse(sessionStorage.getItem("rightList") || "[]")
        },
        actions: {},
        mutations: {
            setRightList(state,data) {	
                state.rightList = data  
                //同时存一份在本地储存中,防止页面刷新,vuex重新加载数据丢失
                sessionStorage.setItem("rightList", JSON.stringify(data))
            }
        },
        getters: {}
    }
})

二、界面控制

情况1:未登录时的界面控制

描述:若未登录,则无token,在看其要去向的是否是登录页面,若是则放行;若不是,则判断是否有token在决定放行与否。

方法:前置路由守卫

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

情况2:登录后的界面控制

描述:若用户登录后,但在地址栏输入非权限页面。 固然可以用路由导航守卫每次都判断; 但是若不具有权限的路由,是否应该压根不存在? 即 动态添加路由,有权限就添加进Home路由的子路由,没有则不添加,就不会展示也跳转不过去。 方法:动态路由添加

  • 把需要动态添加的菜单路由都写在routes外面,根据用户权限动态添加进入Home路由子路由中。 写法如下 image-20220314215536189.png

  • (在router/index.js中实现该方法) 依据二级菜单权限的path属性,动态添加路由到router.options.routes中,此处是索引为2的children中 图为:console.log(router) image-20220314213844409.png

import store from '../store'   //要用到Vuex中共享的菜单数据
export function initDynamicRoute() {
    1.确定需要在哪里动态添加路由
    const currentRoutes = router.options.routes
    // currentRoutes[2].children.push()  添加到此处
    2!.根据二级权限(children里的path属性),匹配路由规则(放入对象ruleMapping以path配对。上面红色框)进行动态添加
    const rightList = store.state.rightList
    rightList.forEach( item => {  一级权限
        item.children.forEach(item => {  二级权限
            const temp = ruleMapping[item.path]
            temp.meta = item.rights  // 为了自定义事件,可在组件中拿得用户路由权限     
             currentRoutes[2].children.push(temp)
        })
    })
    
    3!.将根据权限Push完毕后的路由,【通过router.addRoutes()的方式,重新赋值给router对象】才生效
    router.addRoutes(currentRoutes) 
}
  • 什么时候需要动态添加路由? 一是,登录时(Login组件); 二是,页面刷新时(App组件创建时)
===文件: Login.vue===    时机1.在登录组件中—登录成功后动态添加
import {initDynamicRoute} from '../router'
methods: {
    login() {
        initDynamicRoute()
    }
}

===文件: App.vue===    时机2.【 若仅仅只放在login中,一旦刷新,而又没重新登录,则数据没了;
因为就只在登录时执行了一次initDynamicRoute()方法,没有动态添加路由进去所以刷新时,还需在执行一次】
import {initDynamicRoute} from '../router'
creagted() {
    initDynamicRoute()   //刷新时,由App.vue动态添加路由
}

三、 按钮控制

描述:不同的用户有不同的权限(如下图,该用户商品列表权限中,只有查看view和添加add;没有修改edit和删除delete权限),若某用户没有一些权限,则把该权限所对应的按钮,隐藏/禁用

方法:自定义事件 image-20220314220729360.png

  • 在自定义事件之前,把按钮权限rights通过元数据meta传入到各个路由组件中。【以便拿到该权限(通过router.currentRoute.meta)与v-perssion自定义事件传的值(binding.value.action)进行比较判断】
===文件: router.js===
import store from '../store'   
export function initDynamicRoute() {
    const currentRoutes = router.options.routes
    currentRoutes[2].children.push()
    const rightList = store.state.rightList
    rightList.forEach( item => {  一级权限
        item.children.forEach(item => {  二级权限
            const temp = ruleMapping[item.path]
            //为了在使用自定义事件时,使用的组件可以拿得该组件所有的路由权限(通过meta传递)     
            temp.meta = item.rights
    // === {path:'/users' component: Users, meta:{action:'add', effect:'disabled'}}
             currentRoutes[2].children.push(temp)
        })
    })
}
  • 创建自定义事件v-permission
import Vue from 'vue'
import router from '@/router'
Vue.directive("permission",{
    inserted(el,binding) {
        const action = binding.value.action
        const effect =binding.value.effect
        // 判断!:当前路由对应的组件中,判断是否有action所对应的权限.
        if(router.currentRoute.meta.indexOf(action) == -1) {
            // 如果没有action就删除  要么effect: disabled;那么就隐藏
            if(effect === 'disabled') {
                el.disabled = true
                el.classList.add('is-disabled')  //Element要求,若是禁用,需要加这个属性
            } else {
                el.parentNode.removeChild(el)
            }
        }
    }
})
  • 使用自定义事件,哪个组件需要就写在哪个组件对应按钮上。v-permission="{action:'add/views/edit/delete', effect:'disabled'}"
===文件:User.vue组件===      
若button按钮没有action:'add'权限,则会隐藏(也可实现删除)
<el-button v-permission="{action:'add', effect:'disabled'}">添加用户</el-button>

四、请求和响应的控制

描述:如果发出非权限范围内(即:后台返回的rights中所有权限之外)的请求,要阻断。(即:若rights:['add','view']只有这两个,delete,edit按钮处于禁止用状态。通过F12改变状态发送请求,是阻止的。

方法:使用restful风格进行判断比对

image-20220314223157702.png

import axios from 'axios'
import Vue from 'vue'
import router from 'router'

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(config => {
  if(config.url !== '/login') { 除登录界面以外的请求,都要把token加到headers.Authorization
       config.headers.Authorization = sessionStorage.getItem('token')
      
       const action = actionMapping(config.method)
       const currentRight = router.currentRoute.meta
       if(currentRight && currentRight.meta.indexOf(action) === -1) {
           alert('没有权限')
           return Promise.reject( new Error('没有权限'))
       }
       return config
  }
 
  return config  
})
// 若token过时或被修改,返回登录界面
axios.interceptors.response.use( res => {
	if(res.data.meta.status === 401) {
        router.push('/login')
        sessionStorage.clear()
        window.location.reload()	// 刷新Vuex数据
    }
    return res
})