前言
之前重构公司的管理系统项目,到现在基本就剩权限部分了。整个项目由于前期需求不明确,逻辑不清晰等,导致一开始就没有做权限的划分,后续随着需求的增加,整个系统的前后端的用户鉴权部分均出现一些问题,无法满足现有业务的需求,所以重构这部分的内容也就迫在眉睫了。
正文
逻辑梳理
项目现有的权限划分基于功能模块,而每个功能模块又对应前端一个页面,每个用户所拥有的权限不尽相同。因此,前端可以根据后端存储的权限,动态地生成用户的路由表来维护用户的权限。
项目的功能结构按页面划分,分为两类
- 公共页面,包含登录页面,404页面,错误页面等。
- 权限页面,具体权限功能对应的页面。
如何根据每个用户的权限进行前端的鉴权,实现的思路是,前端根据用户登录后获取到的权限表,生成用户的路由表,然后根据路由表生成一级菜单栏。用户每次进行路由切换时,检查用户是否拥有对应路由的权限。基本的流程如下图
具体实现
路由表
将项目所有路由按照权限划分的页面分为公共路由和权限路由,在 router.js 中定义划分好的路由
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
export const publicRoutes = [
{
path: '/index',
component: () => import('@/views/index/index'),
name: 'index',
meta: { title: '首页' },
isTab: true // 是否在菜单栏显示
},
{
path: '/login',
component: () => import('@/views/login/index'),
name: 'Login',
meta: { title: '登录' },
isTab: false
},
{
path: '/404',
component: () => import('@/views/error/404.vue'),
meta: { title: '404' },
isTab: false
}
]
export const PermissionRoutes = [
{
path: '/account',
component: () => import('@/views/user/index'),
name: 'index',
meta: { title: '账号管理' },
isTab: true,
permissionIndex: 0 // 在权限列表中的索引
},
{
path: '/article',
component: () => import('@/views/article/index'),
name: 'Article',
meta: { title: '文章管理' },
isTab: true,
permissionIndex: 1
},
{ path: '*', redirect: '/404' }
]
const createRouter = () => new Router({
// 初始仅实例化公共路由
routes: publicRoutes
})
export default createRouter()
路由中的 permissionIndex 对应着后端返回的用户的权限列表中对应功能的索引。因为公司后端一开始采用的方案就是用一个数组记录某个用户的权限功能,值为 1 代表有权限,0 代表无权限。如 [1,0,1],代表该用户第一个功能模块有权限操作,第二个功能模块无权限操作,第三个功能模块有权限操作。至于第几项对应哪个功能,是前后端共同维护的一个功能表,有点类似内存管理中的位图记录法。
权限表映射至权限路由
utils下新建 permission-routes.js文件,用于处理将权限映射至用户权限路由表的操作
import { deepClone } from '@/utils/deepClone'
export const userRoutes = (routes, permission) => {
const tempRoutes = deepClone(routes)
function filterPermissionRoutes(currentRoutes) {
for (let i = 0; i < currentRoutes.length; i++) {
if (permission[currentRoutes[i].permissionIndex]) {
// 处理嵌套路由(包含children)
if (currentRoutes[i].children) {
filterPermissionRoutes(currentRoutes[i].children)
}
continue
}
currentRoutes.splice(i, 1)
}
return tempRoutes
}
return filterPermissionRoutes(tempRoutes)
}
import { permissionRoutes } from '@/router/index'
// permissionList为后端返回的用户权限列表
const userPermission = userRoutes(permissionRoutes, permissionList)
即可得到登录用户的权限路由表。
vuex维护用户数据(userInfo + userRoutes)
得到用户的路由表以后,剩下的就是将这个用户的权限路由表通过 router.addRoutes添加至routers,然后菜单栏组件监听用户权限路由表,数据变化时重新渲染菜单栏。这部分的数据,由vuex统一维护,具体实现如下
store下新建 user.js
import { login, getUserInfo } from '@/api/user'
import { permissionRoutes } from '@/router/index'
import router from '../router'
import cookies from 'js-cookie'
import { userRoutes } from '@/utils/permission-routes'
const state = {
token: cookies.get('token'),
userRoutes: []
}
const mutations = {
SET_TOKEN: (state, token) => {
cookies.set('token', token)
state.token = token
},
SET_ROUTES: (state, routes) => {
state.userRoutes = routes
}
}
const actions = {
login({ commit, state, dispatch }, data) {
return new Promise((resolve, reject) => {
login(data).then(res => {
commit('SET_TOKEN', res.data.token)
}).then(res => {
dispatch('user/getInfo', null, { root: true })
}).then(() => resolve())
.catch(err => reject(err))
})
},
getInfo({ commit, state }) {
return new Promise((resolve, reject) => {
getUserInfo(state.token).then((res) => {
const routes = userRoutes(permissionRoutes, res.data.permission)
commit('SET_ROUTES', routes)
router.addRoutes(routes)
router.options.routes.push(...routes)
resolve()
}).catch(err => reject(err))
})
}
}
export default {
namespaced: true,
state,
mutations,
actions
}
流程图中的从 跳转登录 到生成路由表然后添加至路由 通过 dispatch('user/login') 或者 dispatch('user/getInfo) 完成。
可以做一个简单的登录页面的逻辑
views下的 login下新建 index.vue
<template>
<div>
<h1>Login</h1>
<input v-model="name" type="text" placeholder="请输入用户名">
<input v-model="password" type="password" placeholder="请输入密码">
<button @click="toLogin">提交登录</button>
</div>
</template>
<script>
export default {
name: 'Login',
data() {
return {
name: '',
password: '',
redirect: ''
}
},
mounted() {
this.getRedirect()
},
methods: {
toLogin() {
this.$store.dispatch('user/login', {
name: this.name,
password: this.password
}).then(() => {
this.$router.push({ path: this.redirect || '/index' })
})
},
getRedirect() {
const query = this.$route.query
if (query) this.redirect = query.redirect
}
}
}
</script>
这里的 redirect 用来处理流程图中的重定向跳转的分支。后面的就是渲染Tab栏的步骤,公司项目UI库采用的elementui,可以直接通过 compute store中的 userRoutes ,来动态渲染elementui提供的菜单栏,渲染时只需过滤掉 isTab=false 的路由即可,所以就不做代码展示了。
路由变化
上述完成了从跳转登录到渲染Tab栏的流程,还有一部分要做的就是监听路由切换,根据用户权限进行路由跳转的逻辑
新建 router-authority.js 文件
import router from './router'
import store from './store'
router.beforeEach(async(to, from, next) => {
const token = store.state.user.token
if (to.path === '/login') {
next()
} else {
if (token) {
const userRoutes = (store.state.user.userRoutes && store.state.user.userRoutes.length > 0)
if (userRoutes) {
next()
} else {
try {
await store.dispatch('user/getInfo')
next()
} catch (error) {
next(`/login?redirect=${to.path}`)
}
}
} else {
next(`/login?redirect=${to.path}`)
}
}
})
mian.js 引入该文件
import '@/router-authority'
到这里,整个流程图的逻辑基本完成了,剩下的一些message提示可以根据需求在不同的流程点进行添加。
路由鉴权这部分的重构,基本是按这个思路完成的。不同项目权限表的划分,后端存储用户权限的数据结构不尽相同,但思路基本是一致的。如果项目还有颗粒度细度的权限,具体到某个页面的某个按钮之类的,还需要结合其他的鉴权方式去做进一步的划分。
发现已经有大佬写过vue中的权限管理实现的文章了,学习可移步 手摸手,带你用vue撸后台 系列二(登录权限篇)。 文章的思路应该会更清晰一点,这篇文章就当一个工作总结了。