一、权限分类:
- 后端权限:后端权限才是权限的关键,可以控制某个用户是否能够对数据进行增删改查等操作,后端可以通过cookie,session,token来识别发送请求的用户是谁,其核心是RBAC(role based access ctroll),即基于角色的权限控制,因为权限是由后端先分给角色,再由角色分给用户的
- 前端权限:控制视层的展示和前端所有发送的请求,一般权限是后端去实现就足够了,但是增加前端权限也会有一定的好处:
- 降低非法操作的可能性:在页面中展示出一个就算点击了最终也会失败的按钮,会增加有心者非法操作的可能性,前端权限可以将页面的这类按钮失效或隐藏掉
- 尽可能排除不必要请求,减轻服务器压力:没必要的请求,操作失败的请求,不具备权限的请求,应该压根就不需要发送,请求少了,自然也会减轻服务器的压力
- 提高用户体验:根据用户具备的权限,为用户展示自己权限范围内的内容,避免在界面上给用户带来困扰,让用户关注自己分内之事
二、前端权限控制思路
-
菜单的控制:根据登录请求回来的包含有权限相关的数据,前端根据数据展示对应的菜单,点击菜单才能查看相关界面
-
界面的控制:
如果用户没有登录,手动在地址栏输入管理界面的地址,则需要跳转到登录界面
如果用户已经登录,可是手动输入非权限内的地址,则需要跳转404界面
-
按钮的控制:在某个菜单的界面中,还得根据权限数据展示出可进行操作的按钮,如增删改
-
请求和相应的控制:如果用户通过非常规操作,如通过浏览器调试工具将某些禁用的按钮变成启用状态,此时发的请求也应该被前端拦截
三、实现
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.js
import {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、按钮的控制:对用户不具备权限的按钮进行隐藏或者禁用,此时可以通过自定义指令来完成 下图为后端返回的数据格式:
//@/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