Skip to content

路由权限

权限路由

sunlight-admin 采用基于角色的权限控制(RBAC)来管理路由访问权限。在权限路由模式下,默认的路由都需要登录才能访问,通过配置路由的 meta.roles 字段可以进一步限制只有特定角色的用户才能访问该路由。

权限控制原理

  1. 用户登录:用户登录后,系统获取用户信息和角色列表
  2. 路由前置守卫:在 router.beforeEach 中验证用户是否已登录,并根据用户角色生成可访问的动态路由
  3. 路由筛选:通过 filterAsyncRoutes 函数递归筛选异步路由表,只保留用户角色有权访问的路由
  4. 路由注入:将筛选后的路由动态添加到路由实例中

权限控制实现

角色定义

sunlight-admin 支持以下角色:

  • administrator:开发者账号,拥有所有路由的访问权限(最大管理员)
  • admin:普通管理员,拥有大部分路由的访问权限
  • visitor:游客,拥有有限的路由访问权限

注意:在模拟数据中仅提供了 adminvisitor 两种角色的用户,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
}

权限路由最佳实践

  1. 合理设计角色:根据系统需求设计合理的角色体系,避免角色过多或权限过于复杂
  2. 最小权限原则:为用户分配最小必要的角色和权限
  3. 角色继承:考虑实现角色继承机制,减少重复配置
  4. 权限缓存:合理使用权限缓存,提高系统性能
  5. 权限验证:在前端和后端都进行权限验证,确保系统安全
  6. 权限管理界面:提供权限管理界面,方便管理员配置和管理权限

注意事项

  1. 角色命名一致性:确保角色名称在系统中保持一致,避免命名混乱
  2. 路由权限优先级:'administrator' 角色拥有最高权限,不受路由 meta.roles 字段限制
  3. 权限更新:当用户角色发生变化时,需要调用 resetRouter() 重置路由并重新生成权限路由
  4. 静态路由:静态路由不受权限控制,所有登录用户都可以访问
  5. 错误处理:合理处理权限验证失败的情况,提供友好的错误提示
  6. 模拟数据限制:当前系统的模拟数据中仅包含 adminvisitor 角色的用户,administrator 角色需通过特定方式获取