路由权限
权限路由
sunlight-admin 采用基于角色的权限控制(RBAC)来管理路由访问权限。在权限路由模式下,默认的路由都需要登录才能访问,通过配置路由的 meta.roles 字段可以进一步限制只有特定角色的用户才能访问该路由。
权限控制原理
- 用户登录:用户登录后,系统获取用户信息和角色列表
- 路由前置守卫:在
router.beforeEach中验证用户是否已登录,并根据用户角色生成可访问的动态路由 - 路由筛选:通过
filterAsyncRoutes函数递归筛选异步路由表,只保留用户角色有权访问的路由 - 路由注入:将筛选后的路由动态添加到路由实例中
权限控制实现
角色定义
sunlight-admin 支持以下角色:
- administrator:开发者账号,拥有所有路由的访问权限(最大管理员)
- admin:普通管理员,拥有大部分路由的访问权限
- visitor:游客,拥有有限的路由访问权限
注意:在模拟数据中仅提供了 admin 和 visitor 两种角色的用户,administrator 作为开发者账号需通过特定方式(如开发环境或特定 token)获取。
路由权限配置
在路由配置中,通过设置 meta.roles 字段来控制路由的访问权限:
ts
{
path: '/project',
name: 'project',
component: Layout,
meta: {
title: '项目列表',
icon: 'menu-project',
roles: ['administrator'],
isKeepAlive: true
},
children: [
{
path: 'project-test',
component: () => import('@/views/project/index.vue'),
name: 'ProjectTest',
meta: {
title: '项目测试',
icon: 'menu-project',
roles: ['administrator'],
isKeepAlive: true
}
}
]
}权限验证逻辑
权限验证的核心逻辑在 authStore 中实现:
路由筛选函数
ts
/**
* 通过递归筛选异步路由表
* @param routes asyncRoutes
* @param roles
*/
function filterAsyncRoutes(routes: MenuType.MenuOptions[], roles: string[]) {
const res: MenuType.MenuOptions[] = []
routes.forEach(route => {
const tmp = { ...route }
if (hasPermission(roles, tmp)) {
if (tmp.children) {
tmp.children = filterAsyncRoutes(tmp.children, roles)
}
res.push(tmp)
}
})
return res
}权限验证函数
ts
/**
* 使用meta.roles,以确定当前用户是否具有权限
* @param roles 角色
* @param route
*/
function hasPermission(roles: string[], route: MenuType.MenuOptions) {
if (route.meta && route.meta.roles) {
return roles.some(role => route.meta?.roles.includes(role))
} else {
return true
}
}权限路由生成
动态路由生成
在用户登录后,系统会根据用户角色生成可访问的动态路由:
ts
// 获取动态路由
function generateAsyncRoutes(roles: string[]): any {
return new Promise(resolve => {
let accessedRoutes
if (roles.includes('administrator')) {
// administrator 暂定为开发者账号,为最大管理员
accessedRoutes = asyncRoutes || []
} else {
accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)
}
initData.addRoutes = accessedRoutes
resolve(accessedRoutes)
})
}路由前置守卫
在路由前置守卫中,系统会验证用户登录状态和权限,并动态注入路由:
ts
router.beforeEach(async (to, from, next) => {
const globalStore = useGlobalStore()
const authStore = useAuthStore()
const userStore = useUserStore()
NProgress.start()
// 根据路由元信息设置页面标题
document.title = `${to.meta.title}` || globalStore.title
// 获取Token,确定用户是否已登录
const hasToken = getToken()
if (hasToken) {
// 保存到 store 中,防止是页面刷新情况下 store 中已经被清除了
userStore.setToken(hasToken)
// 初始化静态路由
authStore.addAllRoutes(router.options.routes)
if (to.path === '/login') {
// 如果已登录,则重定向到主页
next({ path: '/' })
NProgress.done()
} else {
// 确定用户是否通过getInfo获取了权限角色
const hasRoles = userStore.roles && userStore.roles.length > 0
if (hasRoles) {
next()
} else {
try {
// 获取用户信息
// 角色必须是数组!例如:['admin']或,['developer','visitor']
const { roles } = await userStore.getInfo()
// 基于角色生成可访问路由数组
const accessRoutes = await authStore.generateAsyncRoutes(roles)
// 动态添加可访问路由
accessRoutes.forEach(async (item: any) => {
const { name } = item
if (name && !router.hasRoute(name)) {
router.addRoute(item)
router.options.routes = router.options.routes.concat(item)
}
})
authStore.addAllRoutes(router.options.routes) // 保存授权路由,便于菜单展示
// 路由的重定向
// 设置replace:true,这样导航就不会留下历史记录,登录后回退不会回到登录页面
next({ ...to, replace: true })
} catch (error) {
// 删除令牌并转到登录页面重新登录
await userStore.resetToken() // 重置token信息和登录用户信息
ElMessage({
type: 'error',
message: JSON.stringify(error) || 'Has Error'
})
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
}
} else {
if (whiteList.includes(to.path)) {
// 在免费登录白名单中,直接进入
next()
} else {
// 其他没有访问权限的页面将被重定向到登录页面。
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
})权限路由使用案例
只有特定角色才能访问的路由
ts
{
path: '/project',
name: 'project',
component: Layout,
meta: {
title: '项目列表',
icon: 'menu-project',
roles: ['administrator'], // 只有administrator角色才能访问
isKeepAlive: true
},
children: [
{
path: 'project-test',
component: () => import('@/views/project/index.vue'),
name: 'ProjectTest',
meta: {
title: '项目测试',
icon: 'menu-project',
roles: ['administrator'],
isKeepAlive: true
}
}
]
}多个角色可以访问的路由
ts
{
path: '/sunlightInput',
name: 'SunlightInput',
component: Layout,
meta: {
title: 'sunlightInput',
icon: 'menu-components',
roles: ['admin', 'administrator'], // admin和administrator角色都可以访问
isKeepAlive: true
},
children: [
{
path: 'input1',
component: () => import('@/views/sunlightInput/input1/index.vue'),
name: 'Input1',
meta: {
title: '基础使用',
icon: 'menu-component',
roles: ['admin', 'administrator'],
isKeepAlive: true
}
}
]
}所有登录用户都可以访问的路由
ts
{
path: '/home',
component: () => import('@/views/home/index.vue'),
name: 'Home',
meta: {
title: '首页',
icon: 'menu-home' // 没有设置roles字段,所有登录用户都可以访问
}
}路由重置
当用户退出登录或角色发生变化时,系统需要重置路由,恢复初始路由状态:
ts
/**
* @description 重置路由
*/
export const resetRouter = () => {
const authStore = useAuthStore()
authStore.addRoutes.forEach(route => {
const { name } = route
if (name && router.hasRoute(name)) router.removeRoute(name)
})
const newRouter = CreateRouter()
router.options.routes = newRouter.options.routes
}权限路由最佳实践
- 合理设计角色:根据系统需求设计合理的角色体系,避免角色过多或权限过于复杂
- 最小权限原则:为用户分配最小必要的角色和权限
- 角色继承:考虑实现角色继承机制,减少重复配置
- 权限缓存:合理使用权限缓存,提高系统性能
- 权限验证:在前端和后端都进行权限验证,确保系统安全
- 权限管理界面:提供权限管理界面,方便管理员配置和管理权限
注意事项
- 角色命名一致性:确保角色名称在系统中保持一致,避免命名混乱
- 路由权限优先级:'administrator' 角色拥有最高权限,不受路由
meta.roles字段限制 - 权限更新:当用户角色发生变化时,需要调用
resetRouter()重置路由并重新生成权限路由 - 静态路由:静态路由不受权限控制,所有登录用户都可以访问
- 错误处理:合理处理权限验证失败的情况,提供友好的错误提示
- 模拟数据限制:当前系统的模拟数据中仅包含
admin和visitor角色的用户,administrator角色需通过特定方式获取