在任何一个前后端项目中都会涉及到权限验证的问题,在vue-router中由于有了vue-router函数从而就使得权限验证很简单,基本的逻辑为用户登陆成功之后获取用于的token,再根据token去获取用户的权限信息,根据权限信息去匹配路由,从而动态生成路由表,再根据路由表去渲染侧边栏。其中需要使用到vuex进行全局的状态管理。
目录
1.addRoutes
2.代码实现
(1) router中的路由
(2)vuex中的状态管理
(3)permisson.js
3. 最终效果
4.Github地址
1.addRoutes
vue-router中对addRoutes的解释是动态的添加路由,需要注意的是它虽然可以动态的添加路由,但是却不会改变router.options中的值,所以在动态添加完之后还需要使用vuex来更新对应的总的路由表,并根据这个路由表去渲染侧边栏。
2.代码实现
(1) router中的路由
router文件中的index.js包括了动态路由和静态路由两部分,动态路由即为需要根据用户权限去动态生成的路由,静态路由即为不需要权限的路由。
import Vue from 'vue'
import Router from 'vue-router'
import DownLoad from '@/view/download' // DownLoad组件中包含了所有页面公共的侧边栏
Vue.use(Router)
// 不需要用户权限的静态路由, path: 路由的路径,redirect: 重定向路由的路径
// component: 跳转的组件, meta:元数据{title: 页面的标题, icon: 侧边栏的按钮,
// hidden: 该路由是否在侧边栏被隐藏, 默认为false}
const constantRoutes = [
{
path: '/',
redirect: '/login',
meta: { hidden: true }
},
{
path: '/login', // 登陆界面
name: 'Login',
component: () => import('@/view/login'),
meta: { title: '登陆', hidden: true }
},
{
path: '/download', // 首页
name: 'DownLoad',
component: DownLoad,
meta: { title: '首页', icon: 'el-icon-download', hidden: true },
},
{
path: '/search',
component: DownLoad,
name: 'Search',
meta: { title: '资源搜索', icon: 'el-icon-search', hidden: false },
children: [
{
path: '/movie',
name: 'Movie',
component: () => import('@/view/search/movie'),
meta: { title: '电影', icon: 'el-icon-film', hidden: false },
},
{
path: '/music',
name: 'Music',
component: () => import('@/view/search/music'),
meta: { title: '音乐', icon: 'el-icon-headset', hidden: false }
},
{
path: '/image',
name: 'Image',
component: () => import('@/view/search/image'),
meta: { title: '图片', icon: 'el-icon-picture', hidden: false }
}
]
},
{
path: '/news',
component: DownLoad,
name: 'News',
meta: { title: '新闻头条', icon: 'el-icon-document', hidden: false },
children: [
{
path: '/weibo',
name: 'WeiBo',
component: () => import('@/view/news/weibo'),
meta: { title: '微博热搜', icon: 'el-icon-document', hidden: false },
},
{
path: '/today',
name: 'Today',
component: () => import('@/view/news/today'),
meta: { title: '今日头条', icon: 'el-icon-document', hidden: false }
},
{
path: '/tecentNews',
name: 'TecentNews',
component: () => import('@/view/news/tencentNews'),
meta: { title: '腾讯新闻', icon: 'el-icon-document', hidden: false }
}
]
},
{
path: '/video',
component: DownLoad,
name: 'Video',
meta: { title: '热门视频1', icon: 'el-icon-document', hidden: false },
children: [
{
path: '/hotVideo',
name: 'HotVideo',
component: () => import('@/view/video/'),
meta: { title: '热门视频', icon: 'el-icon-document', hidden: false },
}
]
}
]
const asyncRoutes = [
{
path: '/user',
component: DownLoad,
name: 'User',
meta: { title: '用户中心', role: ['developer', 'super_user', 'common_user'], icon: 'el-icon-user-solid', hidden: false },
children:[
{
path: '/api',
name: 'Api',
component: () => import('@/view/user/api'),
meta: { title: '接口管理', role: ['developer'], icon: 'el-icon-menu', hidden: false } // 路由元数据
},
{
path: '/downloadCneter',
name: 'DownLoadCenter',
component: () => import('@/view/user/downloadCenter'),
meta: { title: '下载中心', role: ['developer', 'super_user', 'common_user'], icon: 'el-icon-download', hidden: false } // 路由元数据
},
{
path: '/spiderLog',
name: 'SpiderLog',
component: () => import('@/view/user/spiderLog'),
meta: { title: '爬虫日志', role: ['developer', 'super_user', 'common_user'], icon: 'el-icon-picture', hidden: false } // 路由元数据
},
{
path: '/userManagement',
name: 'UserManagement',
component: () => import('@/view/user/userManagement'),
meta: { title: '用户管理', role: ['developer', 'super_user'], icon: 'el-icon-user-solid', hidden: false } // 路由元数据
},
]
},
{
path: '*',
name: 'Page404',
component: () => import('@/view/404'),
meta: { role: ['developer', 'super_user', 'common_user'], hidden: true }
}
]
// 解决VUE路由跳转出现Redirected when going from "/xxx" to "/yyy" via a navigation guard.报错
const originalPush = Router.prototype.push
Router.prototype.push = function push(location, onResolve, onReject) {
if (onResolve || onReject) return originalPush.call(this, location, onResolve, onReject)
return originalPush.call(this, location).catch(err => err)
}
const router = new Router({
routes: constantRoutes,
mode: 'history'
})
// addRoutes 方法仅仅是帮你注入新的路由,并没有帮你剔除其它路由,所以需要手动清空路由
// 为了确保其完成,需要使用async异步操作
const resetRouter = async() =>{
const newRouter = createRouter()
router.matcher = newRouter.matcher // reset router
}
export default router
export { constantRoutes, asyncRoutes, resetRouter }
每个路由中的meta元信息中的role字段即为该路由所需要的权限,title为该页面的标题。hidden为是否在侧边栏渲染该路由,icon为侧边栏按钮的样式。需要注意的是addRoutes方法只会添加路由,并不会删除已添加的路由,所以在每次使用之前需要先清空路由,即restRouter()方法,并且为了确保该方法执行后再执行addRoutes,需要写成异步方法。
(2)vuex中的状态管理
如前所述,我们需要在vuex中对整个路由进行全局的管理,对应的代码也很简单,需要注意的是由于路由可能有嵌套的情况,所以在进行权限判断的时候需要进行递归。递归判断的逻辑为:循环动态路由列表中的每一个路由对象,首先获取该路由对象父级的权限,如果没有的话则直接跳过,如果有权限,并且没有子路由,则将该路由对象添加到函数返回的路由列表中,如果有子路由的话,首先在函数返回的路由列表中添加该及路由对象的路由参数,以及空的children,然后再将该及路由对象的children作为参数进行递归。具体的代码实现如下:
/**
* 和动态路由相关的vuex
*/
import { constantRoutes, asyncRoutes } from '@/router'
const state = {
asyncRoutes: [] // 异步路由
}
const getters = {
asyncRoutes: state => state.asyncRoutes // 最终用户的异步路由
}
const mutations = {
getAsyncRoutes: (state, dynamicRoutes) => {
state.asyncRoutes = constantRoutes.concat(dynamicRoutes) // 最终的路由
// 是由没有权限的路由的动态生成的路由所组合而成
}
}
const actions = {
getAsyncRoutes: ({ commit }, userRole) => {
return new Promise((resolve, reject) => {
let dynamicRoutes = []
checkPermission(asyncRoutes, userRole, dynamicRoutes)
commit('getAsyncRoutes', dynamicRoutes) // 根据用户角色筛选路由
resolve({ dynamicRoutes }) // 该异步操作返回的是动态路由,通过addRoutes
// 去进行动态的挂载
})
}
}
// 检查给定路由的权限
// 检查的逻辑为如果含有子路由,则进行递归检查,此时的route参数是包含了路由对象的列表
// dynamicRoutes为根据权限生成的动态路由
const checkPermission = (route, userRole, dynamicRoutes = []) => {
route.forEach(item => {
const { meta: { role } } = item // 整个父级路由的权限
if (role.indexOf(userRole) === -1) {
// 没有权限,forEach中不可使用continue
return true
}
else {
// 有父级路由的权限
if (item.children && item.children.length > 0) {
// 有子路由的时候则进行递归调用
let temp = Object.assign({}, item) // 深复制,不可以使用JSON.parse
// 因为组件对象使用parse页面会出现错误
temp.children = [] // 子路由为空
dynamicRoutes.push(temp) // 加入该级路由
checkPermission(item.children, userRole, dynamicRoutes[dynamicRoutes.length - 1].children)
}
else {
// 没有子路由或者子路由为空
dynamicRoutes.push(item) // 直接添加子路由
}
}
})
}
export default {
namespaced: true,
state,
mutations,
actions,
getters
}
(3)permisson.js
在permission.js中我们通过vue-router提供的全局前置守卫beforeEach来对每一个页面进行权限验证,验证的基本逻辑为:
如果要进入的是登陆页面,则直接跳转,否则先获取用户的token,如果获取失败则跳转到登陆页面,获取成功则根据token去获取该用户的权限,再根据权限去vuex中动态生成用户特定的路由,并将异步执行的结果返回使用addRoutes来动态添加路由。代码如下:
/**
* 整个页面的权限验证,在每次进入一个页面之前都验证对应的权限
* 并且动态的生成路由
*/
import router from './router'
import store from './store'
import { getCookie } from '@/utils/cookie'
import { Message } from 'element-ui'
import {resetRouter} from './router'
// 每次进入一个路由之前进行权限验证
// 验证的规则为如果是登陆页面则直接跳转,否则根据token去获取用户权限,再根据用户权限去动态
// 挂载路由,如果没有token或者是获取用户权限失败则跳转到登陆页面
router.beforeEach(async (to, from, next) => {
document.title = to.meta.title || '404' // 设置页面的标题
if (to.path === '/login') {
next() // 如果是登陆页则直接跳转
}
else {
const token = getCookie('token') // 获取cookie中的用户token
if(token){
// 如果有token则根据token去获取用户信息,否则跳转到登陆也页面
let userName = store.getters['user/userRole'] // 获取用户的角色
if (!userName) {
// 如果用户名为空则根据token去重新获取用户的权限信息
store.dispatch('user/setRole', { token }).then(({ role }) => {
store.dispatch('routes/getAsyncRoutes', role).then(async ({ dynamicRoutes }) => {
await resetRouter() // 清空路由
router.addRoutes(dynamicRoutes) // 动态挂载路由
if(from.path === '/login'){
// 登陆成功以后提示登陆成功
Message({
message: '登陆成功',
type: 'success'
})
}
next({...to, replace: true}) // 确保addRoutes方法被调用
})
}).catch((error) => {
Message({
message: '获取用户权限失败' + error,
type: 'error'
})
next('/') // 跳转到登陆界面
})
}
else {
next()
}
}
else{
next('/')
}
}
})
至于侧边的栏渲染以及无限递归的组件后面的文章会向大家进行介绍。
3. 最终效果
先去数据库随便插入两条数据进行测试,可以看到第一个admin用户是有test页面的权限的,第二个用户是没有的
最终的效果如下:
4.Github地址
https://github.com/JustKeepSilence/DownLoad,欢迎大家和我一起探讨交流~~~