链接:https://juejin.cn/post/6844903478880370701
先说一说我权限控制的主体思路,前端会有一份路由表,它表示了每一个路由可访问的权限。当用户登录之后,通过 token 获取用户的 role ,动态根据用户的 role 算出其对应有权限的路由,再通过router.addRoutes
动态挂载路由。但这些控制都只是页面级的,说白了前端再怎么做权限控制都不是绝对安全的,后端的权限验证是逃不掉的。
我司现在就是前端来控制页面级的权限,不同权限的用户显示不同的侧边栏和限制其所能进入的页面(也做了少许按钮级别的权限控制),后端则会验证每一个涉及请求的操作,验证其是否有该操作的权限,每一个后台的请求不管是 get 还是 post 都会让前端在请求 header
里面携带用户的 token,后端会根据该 token 来验证用户是否有权限执行该操作。若没有权限则抛出一个对应的状态码,前端检测到该状态码,做出相对应的操作。
权限 前端or后端 来控制?
有很多人表示他们公司的路由表是于后端根据用户的权限动态生成的,我司不采取这种方式的原因如下:
- 项目不断的迭代你会异常痛苦,前端新开发一个页面还要让后端配一下路由和权限,让我们想了曾经前后端不分离,被后端支配的那段恐怖时间了。
- 其次,就拿我司的业务来说,虽然后端的确也是有权限验证的,但它的验证其实是针对业务来划分的,比如超级编辑可以发布文章,而实习编辑只能编辑文章不能发布,但对于前端来说不管是超级编辑还是实习编辑都是有权限进入文章编辑页面的。所以前端和后端权限的划分是不太一致。
- 还有一点是就vue2.2.0之前异步挂载路由是很麻烦的一件事!不过好在官方也出了新的api,虽然本意是来解决ssr的痛点的。。。
addRoutes
在之前通过后端动态返回前端路由一直很难做的,因为vue-router必须是要vue在实例化之前就挂载上去的,不太方便动态改变。不过好在vue2.2.0以后新增了router.addRoutes
动态添加更多路由到路由器。参数必须是一个数组,使用与routes构造函数选项相同的路由配置格式。
有了这个我们就可相对方便的做权限控制了
(下面代码都是 vue element admin 中的,请下载自看)
具体实现
- 创建vue实例的时候将vue-router挂载,但这个时候vue-router挂载一些登录或者不用权限的公用的页面。
- 当用户登录后,获取用role,将role和路由表每个页面的需要的权限作比较,生成最终用户可访问的路由表。
- 调用router.addRoutes(store.getters.addRouters)添加用户可访问的路由。
- 使用vuex管理路由表,根据vuex中可访问的路由渲染侧边栏组件。
router.js
首先我们实现router.js路由表,这里就拿前端控制路由来举例(后端存储的也差不多,稍微改造一下就好了)
// router.js import Vue from 'vue'; import Router from 'vue-router'; import Login from '../views/login/'; const dashboard = resolve => require(['../views/dashboard/index'], resolve); //使用了vue-routerd的[Lazy Loading Routes ](https://router.vuejs.org/en/advanced/lazy-loading.html) //所有权限通用路由表 //如首页和登录页和一些不用权限的公用页面 export const constantRouterMap = [ { path: '/login', component: Login }, { path: '/', component: Layout, redirect: '/dashboard', name: '首页', children: [{ path: 'dashboard', component: dashboard }] }, ] //实例化vue的时候只挂载constantRouter export default new Router({ routes: constantRouterMap }); //异步挂载的路由 //动态需要根据权限加载的路由表 export const asyncRouterMap = [ { path: '/permission', component: Layout, name: '权限测试', meta: { role: ['admin','super_editor'] }, //页面需要的权限 children: [ { path: 'index', component: Permission, name: '权限测试页', meta: { role: ['admin','super_editor'] } //页面需要的权限 }] }, { path: '*', redirect: '/404', hidden: true } ];
这里我们根据 vue-router官方推荐 的方法通过meta标签来标示改页面能访问的权限有哪些。如meta: { role: ['admin','super_editor'] }
表示该页面只有admin和超级编辑才能有资格进入。
404
页面一定要最后加载,如果放在
constantRouterMap
一同声明了
404
,后面的所以页面都会被拦截到
404
,详细的问题见
addRoutes when you've got a wildcard route for 404s does not work
main.js
关键的main.js
// main.js router.beforeEach((to, from, next) => { if (store.getters.token) { // 判断是否有token if (to.path === '/login') { next({ path: '/' }); } else { if (store.getters.roles.length === 0) { // 判断当前用户是否已拉取完user_info信息 store.dispatch('GetInfo').then(res => { // 拉取info const roles = res.data.role; store.dispatch('GenerateRoutes', { roles }).then(() => { // 生成可访问的路由表 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 => { console.log(err); }); } else { next() //当有用户权限的时候,说明所有可访问路由已生成 如访问没权限的全面会自动进入404页面 } } } else { if (whiteList.indexOf(to.path) !== -1) { // 在免登录白名单,直接进入 next(); } else { next('/login'); // 否则全部重定向到登录页 } } });
import router from '@/router' import store from '@/store' import { getToken, getUserInfo } from '@/utils/auth' import { constantRoutes } from './router' import { tip } from '@/utils/Tip/tip' import NProgress from 'nprogress' import 'nprogress/nprogress.css' NProgress.configure({ showSpinner: false }) const whiteList = ['/login', '/auth-redirect'] // no redirect whitelist router.beforeEach(async(to, from, next) => { // start progress bar NProgress.start() // determine whether the user has logged in const hasToken = getToken() if (hasToken) { if (to.path === '/login') { // if is logged in, redirect to the home page next({ path: '/index' }) NProgress.done() // hack: https://github.com/PanJiaChen/vue-element-admin/pull/2939 } else { // determine whether the user has obtained his permission roles through getInfo const hasRoles = store.getters.roles && store.getters.roles.length > 0 console.log(store.state.user.roles) console.log(hasRoles, '----------')
// 登录玩成之后并没有获取 角色信息,获取角色信息由下玩成,所以第一次进入到else if (hasRoles) { console.log(router) next() } else { try { // get user info const roles = store.state.user.roles console.log(accessRoutes, '-------------' ,roles) // const roles = store.state.user // console.log(roles) // this.$store.dispatch('../permission/generateRoutes',roles) // generate accessible routes map based on roles const accessRoutes = await store.dispatch('permission/generateRoutes', roles) // dynamically add accessible routes router.addRoutes(accessRoutes) // hack method to ensure that addRoutes is complete // set the replace: true, so the navigation will not leave a history record next({ ...to, replace: true }) } catch (error) { // remove token and go to login page to re-login await store.dispatch('user/resetToken') Message.error(error || 'Has Error') next(`/login?redirect=${to.path}`) NProgress.done() } } } } else { /* has no token*/ if (whiteList.indexOf(to.path) !== -1) { // in the free login whitelist, go directly next() } else { // other pages that do not have permission to access are redirected to the login page. next(`/login?redirect=${to.path}`) NProgress.done() } } }) router.afterEach(() => { // finish progress bar NProgress.done() })
这边逻辑是,登录完成之后并没有获取 角色信息,获取角色信息由下玩成,所以第一次进入到else,
为什么要这么做呢?
首先,这样子做可以将角色信息保存在 vuex 中, 如果页面刷新 vuex 也会随着刷新,但是获取 角色信息都是写在 导航前置守卫中,
所以当 vuex 中的角色信息为空时,将会根据 token 发起获取角色的请求,这样只要 token 是有效的,就能保证角色信息一直存在。
然后看看看后面的代码
// generate accessible routes map based on roles const accessRoutes = await store.dispatch('permission/generateRoutes', roles) console.log(accessRoutes, '-------------') // dynamically add accessible routes router.addRoutes(accessRoutes)
后面紧跟着动态路由,这说明了 ,只要角色失效了,就会重新执行动态路由,这样路由也不用缓存到本地了,刷新页面动态添加的
路由也不会失效了。
这里害有一个小问题,就是 router.addRoutes
之后的next()
可能会失效,因为可能next()
的时候路由并没有完全add完成,好在查阅文档发现
next('/') or next({ path: '/' }): redirect to a different location. The current navigation will be aborted and a new one will be started.
这样我们就可以简单的通过next(to)
巧妙的避开之前的那个问题了。这行代码重新进入router.beforeEach
这个钩子,这时候再通过next()
来释放钩子,
就能确保所有的路由都已经挂在完成了。
这里再附上更加详细的答案:
next({ ...to, replace: true }) // hack方法 确保addRoutes已完成。有大佬理解并解释一下这句吗?
这行代码重新进入router.beforeEach这个钩子,这时候再通过next()来释放钩子,就能确保所有的路由都已经挂在完成了。还是没太懂
router.addRoutes是同步方法,整体流程:
1. 路由跳转,根据目标地址从router中提取route信息,由于此时还没addRouters,所以解析出来的route是个空的,不包含组件。
2. 执行beforeEach钩子函数,然后内部会动态添加路由,但此时route已经生成了,不是说router.addRoutes后,这个route会自动更新,如果直接next(),最终渲染的就是空的。
3. 调用next({ ...to, replace: true }),会abort刚刚的跳转,然后重新走一遍上述逻辑,这时从router中提取的route信息就包含组件了,之后就和正常逻辑一样了。
主要原因就是生成route是在执行beforeEach钩子之前。
如果没有看懂,请去看我的《vue-router使用next()跳转到指定路径时会无限循环》这篇文章。只能帮到这了
store/permission.js
就来就讲一讲 GenerateRoutes Action
import router, { asyncRoutes, constantRoutes } from '@/router' /** * Use meta.role to determine if the current user has permission * @param roles * @param route */ function hasPermission(roles, route) { if (route.meta && route.meta.roles) { return roles.some(role => route.meta.roles.includes(role)) } else { return true } } /** * Filter asynchronous routing tables by recursion * @param routes asyncRoutes * @param roles */ export function filterAsyncRoutes(routes, roles) { const res = [] routes.forEach(route => { const tmp = { ...route } if (hasPermission(roles, tmp)) { if (tmp.children) { tmp.children = filterAsyncRoutes(tmp.children, roles) } res.push(tmp) } }) return res } const state = { routes: [], addRoutes: [] } const mutations = { SET_ROUTES: (state, routes) => { state.addRoutes = routes state.routes = constantRoutes.concat(routes) } } const actions = { generateRoutes({ commit }, roles) { return new Promise(resolve => { let accessedRoutes if (roles === '总经理') { accessedRoutes = asyncRoutes || [] // dynamically add accessible routes router.addRoutes(accessedRoutes) // router.options.routes = [...router.options.routes, ...accessedRoutes] } else { accessedRoutes = filterAsyncRoutes(asyncRoutes, roles) } commit('SET_ROUTES', accessedRoutes) resolve(accessedRoutes) }) } } export default { namespaced: true, state, mutations, actions }
由于我这边接口角色和token登录一起获取的,所以里面还动态添加了一下路由,现实不应这么写
这里的代码说白了就是干了一件事,通过用户的权限和之前在router.js里面asyncRouterMap的每一个页面所需要的权限做匹配,最后返回一个该用户能够访问路由有哪些。
侧边栏
最后一个涉及到权限的地方就是侧边栏,不过在前面的基础上已经很方便就能实现动态显示侧边栏了。这里侧边栏基于element-ui的NavMenu来实现的。 代码有点多不贴详细的代码了,有兴趣的可以直接去github上看地址,或者直接看关于侧边栏的文档。
说白了就是遍历之前算出来的permission_routers
,通过vuex拿到之后动态v-for渲染而已。不过这里因为有一些业务需求所以加了很多判断 比如我们在定义路由的时候会加很多参数
/** * hidden: true if `hidden:true` will not show in the sidebar(default is false) * redirect: noredirect if `redirect:noredirect` will no redirct in the breadcrumb * name:'router-name' the name is used by <keep-alive> (must set!!!) * meta : { role: ['admin','editor'] will control the page role (you can set multiple roles) title: 'title' the name show in submenu and breadcrumb (recommend set) icon: 'svg-name' the icon show in the sidebar, noCache: true if fasle ,the page will no be cached(default is false) } **/
这里仅供参考,而且本项目为了支持无限嵌套路由,所有侧边栏这块使用了递归组件。如需要请大家自行改造,来打造满足自己业务需求的侧边栏。
侧边栏高亮问题:很多人在群里问为什么自己的侧边栏不能跟着自己的路由高亮,其实很简单,element-ui官方已经给了default-active
所以我们只要
:default-active="$route.path" 将default-active一直指向当前路由就可以了,就是这么简单
按钮级别权限控制