前端权限控制的意义
前端权限是视图层的展示,真正的权限控制是后端数据的变化。前端权限控制的展示是为了更好的用户体验。 后台系统权限控制,主要体现在四个方面
- 菜单控制
- 界面控制
- 按钮控制
- 请求和响应控制
如何实现权限控制
一、菜单控制
- Login组件:用户登录时获取后台返回的权限菜单列表,不同用户所拥有的权限是不一样的。
<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组件:左侧菜单栏需要权限菜单列表对数据进行展示
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路由子路由中。 写法如下
-
(在router/index.js中实现该方法) 依据二级菜单权限的path属性,动态添加路由到router.options.routes中,此处是索引为2的children中 图为:console.log(router)
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权限),若某用户没有一些权限,则把该权限所对应的按钮,隐藏/禁用
方法:自定义事件
- 在自定义事件之前,把按钮权限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风格进行判断比对
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
})