1. 权限是啥?

权限是对特定资源的访问许可,确保用户只能访问到被分配的资源。其原理是请求发起权,请求发起有两种形式:

  • 页面加载触发
  • 页面上的按钮点击触发
    实现目标:
  • 路由方面,只能看到有权访问的菜单
  • 视图方面,只能看到自己有权浏览的内容和有权操作的控件
  • 请求控制,越权请求在前端进行拦截

2. 如何实现?

权限控制可以分四个方面:

  • 接口权限
  • 按钮权限
  • 菜单权限
  • 路由权限

接口权限

接口权限一般采用jwt的形式来严重,登录完拿到token将token保存,每次请求拦截器进行拦截,请求头部带token

路由权限控制

方案一:在路由标记相应的权限信息,路由跳转前做校验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const routerMap = [
{
path: '/permission',
component: Layout,
redirect: '/permission/index',
alwaysShow: true, // will always show the root menu
meta: {
title: 'permission',
icon: 'lock',
roles: ['admin', 'editor'] // you can set roles in root nav
},
children: [{
path: 'page',
component: () => import('@/views/permission/page'),
name: 'pagePermission',
meta: {
title: 'pagePermission',
roles: ['admin'] // or you can only set roles in sub nav
}
}, {
path: 'directive',
component: () => import('@/views/permission/directive'),
name: 'directivePermission',
meta: {
title: 'directivePermission'
// if do not set roles, means: this page does not require permission
}
}]
}]

缺点:

  • 加载所有的路由,如果路由很多,而用户并不是所有路由都有权限访问,会性能会有影响
  • 全局路由守卫里,每次路由跳转都要做权限判断
  • 菜单信息写死在前端,要改个显示文字或权限信息,需要重新编译
  • 菜单跟路由耦合在一起,定义路由的时候还有添加菜单显示标题,图标之类的信息,而且路由不一定作为菜单显示,还要多加字段进行标识
    方案二:
    初始化的时候挂载公共路由,比如404,登录页。登录后获取用户的权限信息筛选有权限访问的路由在全局路由守卫调用addRoutes添加路由
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    // permission judge function
    function hasPermission(roles, permissionRoles) {
    if (roles.indexOf('admin') >= 0) return true // admin permission passed directly
    if (!permissionRoles) return true
    return roles.some(role => permissionRoles.indexOf(role) >= 0)
    }

    const whiteList = ['/login', '/authredirect']// no redirect whitelist

    router.beforeEach((to, from, next) => {
    NProgress.start() // start progress bar
    if (getToken()) { // determine if there has token
    /* has token*/
    if (to.path === '/login') {
    next({ path: '/' })
    NProgress.done() // if current page is dashboard will not trigger afterEach hook, so manually handle it
    } else {
    if (store.getters.roles.length === 0) { // 判断当前用户是否已拉取完user_info信息
    store.dispatch('GetUserInfo').then(res => { // 拉取user_info
    const roles = res.data.roles // note: roles must be a array! such as: ['editor','develop']
    store.dispatch('GenerateRoutes', { roles }).then(() => { // 根据roles权限生成可访问的路由表
    router.addRoutes(store.getters.addRouters) // 动态添加可访问路由表
    next({ ...to, replace: true }) // hack方法 确保addRoutes已完成 ,set the replace: true so the navigation will not leave a history record
    })
    }).catch((err) => {
    store.dispatch('FedLogOut').then(() => {
    Message.error(err || 'Verification failed, please login again')
    next({ path: '/' })
    })
    })
    } else {
    // 没有动态改变权限的需求可直接next() 删除下方权限判断 ↓
    if (hasPermission(store.getters.roles, to.meta.roles)) {
    next()//
    } else {
    next({ path: '/401', replace: true, query: { noGoBack: true }})
    }
    // 可删 ↑
    }
    }
    } else {
    /* has no token*/
    if (whiteList.indexOf(to.path) !== -1) { // 在免登录白名单,直接进入
    next()
    } else {
    next('/login') // 否则全部重定向到登录页
    NProgress.done() // if current page is login will not trigger afterEach hook, so manually handle it
    }
    }
    })

    router.afterEach(() => {
    NProgress.done() // finish progress bar
    })
    缺点:
  • 全局路由守卫里,每次路由跳转都要做判断
  • 菜单信息写死在前端,要改个显示文字或权限信息,需要重新编译
  • 菜单跟路由耦合在一起,定义路由的时候还有添加菜单显示标题,图标之类的信息,而且路由不一定作为菜单显示,还要多加字段进行标识

菜单权限

菜单权限可以理解成将页面与路由进行解耦
方案一
菜单与路由分离,菜单由后端返回
前端定义路由信息,name属性不得为空,需要此字段与后端返回菜单做关联,后端返回的菜单信息中必须要有name对应的字段,并且做唯一性校验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
function hasPermission(router, accessMenu) {
if (whiteList.indexOf(router.path) !== -1) {
return true;
}
let menu = Util.getMenuByName(router.name, accessMenu);
if (menu.name) {
return true;
}
return false;

}

Router.beforeEach(async (to, from, next) => {
if (getToken()) {
let userInfo = store.state.user.userInfo;
if (!userInfo.name) {
try {
await store.dispatch("GetUserInfo")
await store.dispatch('updateAccessMenu')
if (to.path === '/login') {
next({ name: 'home_index' })
} else {
//Util.toDefaultPage([...routers], to.name, router, next);
next({ ...to, replace: true })//菜单权限更新完成,重新进一次当前路由
}
}
catch (e) {
if (whiteList.indexOf(to.path) !== -1) { // 在免登录白名单,直接进入
next()
} else {
next('/login')
}
}
} else {
if (to.path === '/login') {
next({ name: 'home_index' })
} else {
if (hasPermission(to, store.getters.accessMenu)) {
Util.toDefaultPage(store.getters.accessMenu,to, routes, next);
} else {
next({ path: '/403',replace:true })
}
}
}
} else {
if (whiteList.indexOf(to.path) !== -1) { // 在免登录白名单,直接进入
next()
} else {
next('/login')
}
}
let menu = Util.getMenuByName(to.name, store.getters.accessMenu);
Util.title(menu.title);
});

Router.afterEach((to) => {
window.scrollTo(0, 0);
});

缺点:

  • 菜单需要与路由做一一对应,前端添加了新功能,需要通过菜单管理功能添加新的菜单,菜单配置不对会导致应用不能正常使用
  • 全局路由守卫里,每次路由跳转都要做判断
    方案二
    菜单和路由都由后端返回,前端统一定义路由组件
    1
    2
    3
    4
    5
    6
    const Home = () => import("../pages/Home.vue");
    const UserInfo = () => import("../pages/UserInfo.vue");
    export default {
    home: Home,
    userInfo: UserInfo
    };
    后端路由组件返回以下格式
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    [
    {
    name: "home",
    path: "/",
    component: "home"
    },
    {
    name: "home",
    path: "/userinfo",
    component: "userInfo"
    }
    ]
    前后端都需要做相应的路由处理
    缺点:
  • 全局路由守卫里,每次路由跳转都要做判断
  • 前后端的配合要求更高

按钮权限

方案一
按钮权限用v-if判断,但是页面过多情况下,每个页面都要获取用户权限role和路由表里的meta.Permission,再做判断
方案二
通过自定义指令进行权限判断
配置路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
{
path: '/permission',
component: Layout,
name: '权限测试',
meta: {
btnPermissions: ['admin', 'supper', 'normal']
},
//页面需要的权限
children: [{
path: 'supper',
component: _import('system/supper'),
name: '权限测试页',
meta: {
btnPermissions: ['admin', 'supper']
} //页面需要的权限
},
{
path: 'normal',
component: _import('system/normal'),
name: '权限测试页',
meta: {
btnPermissions: ['admin']
} //页面需要的权限
}]
}

自定义权限鉴定指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**权限指令**/
const has = Vue.directive('has', {
bind: function (el, binding, vnode) {
// 获取页面按钮权限
let btnPermissionsArr = [];
if(binding.value){
// 如果指令传值,获取指令参数,根据指令参数和当前登录人按钮权限做比较。
btnPermissionsArr = Array.of(binding.value);
}else{
// 否则获取路由中的参数,根据路由的btnPermissionsArr和当前登录人按钮权限做比较。
btnPermissionsArr = vnode.context.$route.meta.btnPermissions;
}
if (!Vue.prototype.$_has(btnPermissionsArr)) {
el.parentNode.removeChild(el);
}
}
});
// 权限检查方法
Vue.prototype.$_has = function (value) {
let isExist = false;
// 获取用户按钮权限
let btnPermissionsStr = sessionStorage.getItem("btnPermissions");
if (btnPermissionsStr == undefined || btnPermissionsStr == null) {
return false;
}
if (value.indexOf(btnPermissionsStr) > -1) {
isExist = true;
}
return isExist;
};
export {has}

小结

根据项目进行方案考虑,如考虑路由与菜单是否分离
权限需要前后端结合,前端尽可能的去控制,更多的需要后台判断