标签 RBAC 下的文章

前言

本篇文章主要讲解 RBAC 权限方案在中后台管理系统的实现

在公司内部写过好几个后台系统,都需要实现权限控制,在职时工作繁多,没有系统性的来总结一下相关经验,现在人已离职,就把自己的经验总结一下,希望能帮助到你

本文是《通俗易懂的中后台系统建设指南》系列的第九篇文章,该系列旨在告诉你如何来构建一个优秀的中后台管理系统

权限模型有哪些?

主流的权限模型主要分为以下五种:

  • ACL模型:访问控制列表
  • DAC模型:自主访问控制
  • MAC模型:强制访问控制
  • ABAC模型:基于属性的访问控制
  • RBAC模型:基于角色的权限访问控制

这里不介绍全部的权限模型,有兴趣你可以看看这篇文章:权限系统就该这么设计,yyds

如果你看过、用过市面上一些开源后台系统及权限设计,你会发现它们主要都是基于 RBAC 模型来实现的

为什么是 RBAC 权限模型?

好问题!我帮你问了下 AI

对比维度ACL (访问控制列表)RBAC (基于角色)ABAC (基于属性)
核心逻辑用户 ↔ 权限
直接点对点绑定,无中间层
用户 ↔ 角色 ↔ 权限
引入“角色”解耦,权限归于角色
属性 + 规则 = 权限
动态计算 (Who, When, Where)
优点模型极简,开发速度快,适合初期 MVP结构清晰,复用性高,符合企业组织架构,维护成本低极度灵活,支持细粒度控制
(如:只能在工作日访问)
缺点用户量大时维护工作呈指数级增长,极易出错角色爆炸:若特例过多,可能导致定义成百上千个角色开发复杂度极高,规则引擎难设计,有一定的性能消耗
适用场景个人博客、小型内部工具中大型后台系统、SaaS 平台 (行业标准)银行风控、AWS IAM、国家安全级系统

总结来说,在后台系统的场景下,RBAC 模型在灵活性(对比ACL)和复杂性(对比ABAC)上取得了一个很好的平衡

RBAC 概念理解

RBAC 权限模型,全称 Role-Based Access Control,基于角色的权限访问控制

模型有三要素:

  • 用户(User):系统主体,即操作系统的具体人员或账号
  • 角色(Role):角色是一组权限的集合,代表了用户在组织中的职能或身份
  • 权限(Permission):用户可以对系统资源进行的访问或操作能力

RBAC 的设计是将角色绑定权限,用户绑定角色,从而实现权限控制

RBAC 权限模型

并且,它们之间的逻辑关系通常是多对多的:

用户 - 角色 (User-Role): 一个用户可以拥有多个角色(例如:某人既是“项目经理”又是“技术委员会成员”)

角色 - 权限(Role-Permission): 一个角色包含多个权限(例如:“人事经理”角色拥有“查看员工”、“编辑薪资”等权限)

主导权限控制的前端、后端方案

市面上这些开源 Admin 的权限控制中,存在两种主要的权限主导方案:前端主导的权限方案和后端主导的权限方案

前端主导的权限方案

前端主导的权限方案,一个主要的特征是菜单数据由前端维护,而不是存在数据库中

后端只需要在登录后给到用户信息,这个信息中会包含用户的角色,根据这个角色信息,前端可以筛选出具有权限的菜单、按钮

这种方案的主要逻辑放在前端,而不是后端数据库,所以安全性没保障,灵活性也较差,要更新权限,就需要改动前端代码并重新打包上线,无法支持“动态配置权限”

适合一些小型、简单系统

后端主导的权限方案

后端控制方案,即登录后在返回用户信息时,还会给到此用户对应的菜单数据和按钮权限码等

菜单数据、按钮权限码等都存在数据库,这样一来,安全性、灵活性更高,要更新权限数据或用户权限控制,提供相应接口即可修改

倒也不是说前端完全不用管菜单数据,而是前端只需要维护一些静态菜单数据,比如登录页、异常页(404、403...)

在企业级后台系统中,后端主导的权限方案是比较常用的,本文只介绍后端主导的权限方案

权限方案整体流程

在开始写代码之前,要清晰知道整体实现流程,我画了一张图来直观展示:

权限方案整体流程

后台系统中的 RBAC 权限实战

权限菜单类型定义

首先,在前后端人员配合中,我们最好约定一套菜单数据的结构,比如:

import type { RouteMeta, RouteRecordRaw, RouteRecordRedirectOption } from 'vue-router';
import type { Component } from 'vue';
import type { DefineComponent } from 'vue';
import type { RouteType } from '#/type';

declare global {
  export interface CustomRouteRecordRaw extends Omit<RouteRecordRaw, 'meta'> {
    /**
     * 路由地址
     */
    path?: string;
    /**
     * 路由名称
     */
    name?: string;
    /**
     * 重定向路径
     */
    redirect?: RouteRecordRedirectOption;
    /**
     * 组件
     */
    component?: Component | DefineComponent | (() => Promise<unknown>);
    /**
     * 子路由信息
     */
    children?: CustomRouteRecordRaw[];
    /**
     * 路由类型
     */
    type?: RouteType;
    /**
     * 元信息
     */
    meta: {
      /**
       * 菜单标题
       */
      title: string;
      /**
       * 菜单图标
       */
      menuIcon?: string;
      /**
       * 排序
       */
      sort?: number;
      /**
       * 是否在侧边栏菜单中隐藏
       * @default false
       */
      hideMenu?: boolean;
      /**
       * 是否在面包屑中隐藏
       * @default false
       */
      hideBreadcrumb?: boolean;
      /**
       * 当只有一个子菜单时,是否隐藏父级菜单直接显示子菜单内容
       * @default false
       */
      hideParentIfSingleChild?: boolean;
    };
  }

  /**
   * 后端返回的权限路由类型定义
   */
  export type PermissionRoute = Omit<CustomRouteRecordRaw, 'component' | 'children' | 'type'> & {
    /**
     * 路由ID
     */
    id?: number;
    /**
     * 路由父ID
     */
    parentId?: number;
    /**
     * 组件路径(后端返回时为字符串,前端处理后为组件)
     */
    component: string;
    /**
     * 子路由信息
     */
    children?: PermissionRoute[];
    /**
     * 路由类型
     */
    type: RouteType;
  };
}
router.d.ts 找到类型文件

以上面的类型定义为例,我们约定 PermissionRoute 类型是后端返回的权限路由类型:

我这里使用 ApiFox 来 Mock 权限路由数据,数据是这样的:

clean-admin ApiFox 文档在线地址

路由表 Mock 数据

从登录页到路由守卫

权限方案的第一步,是登录并拿到用户信息

假设我们现在用 Element Plus 搭建起了一个登录页面,当用户点击登录时,我们需要做这几件事:

  1. 调用登录接口,将账号、密码发送给后端进行验证,验证通过则返回 JWT 信息
  2. 将返回的 JWT 信息保存到本地,后续每次请求都携带 Token 来识别用户身份并决定你能拿到的权限路由数据
  3. 触发路由守卫拦截

登录操作

account-login.vue 找到全部代码

基本 Vue Router 配置

登录完成后,我们就可以触发路由守卫了,但在写路由守卫之前,我们先来配置一下基本的 Vue Router

在整个权限系统中,我们将路由数据分为两种:

  1. 静态路由:系统固定的路由,比如登录页、异常页(404、403...)
  2. 动态路由:由后端接口返回的用户角色对应的菜单路由数据

静态路由是直接由前端定义,不会从后端接口返回、不会根据用户角色动态变化,所以这部分路由我们直接写好然后注册到 Vue Router 中即可

Vue Router 配置:

import { createRouter, createWebHashHistory } from 'vue-router';
import type { RouteRecordRaw } from 'vue-router';
import type { App } from 'vue';
import type { ImportGlobRoutes } from './typing';
import { extractRoutes } from './helpers';
import { afterEachGuard, beforeEachGuard } from './guards';

/** 静态路由 */
const staticRoutes = extractRoutes(
  import.meta.glob<ImportGlobRoutes>(['./modules/constant-routes/**/*.ts'], {
    eager: true,
  }),
);

/** 系统路由 */
const systemRoutes = extractRoutes(
  import.meta.glob<ImportGlobRoutes>(['./modules/system-routes/**/*.ts'], {
    eager: true,
  }),
);

const router = createRouter({
  history: createWebHashHistory(),
  routes: [...staticRoutes, ...systemRoutes] as RouteRecordRaw[],
  strict: true,
  scrollBehavior: () => ({ left: 0, top: 0 }),
});

beforeEachGuard(router);
afterEachGuard(router);

/** 初始化路由 */
function initRouter(app: App<Element>) {
  app.use(router);
}

export { router, initRouter, staticRoutes };
图中的静态路由和系统路由是同一类路由数据,即静态路由

这个配置文件可以在 router/index.ts 找到

这个基本的 Vue Router 配置,做了这么几件事:

  1. 导入 modules 文件夹下的静态路由进行注册
  2. 路由初始化配置 initRouter ,在 main.ts 中调用
  3. 注册全局前置守卫 beforeEach、全局后置守卫 afterEach

我们实现动态路由注册的逻辑就写在 beforeEach

值得一提的是,使用了 import.meta.glob 来动态导入指定路径下的文件模块,这是 Vite 提供的一种导入方式,参考:Vite Glob 导入

路由守卫与动态注册

路由守卫是 Vue Router 提供的一种机制,主要用来通过跳转或取消的方式守卫导航:Vue Router 路由守卫

重头戏在全局前置守卫 router.beforeEach 中实现,来看看我们做哪些事:

import { ROUTE_NAMES } from '../config';
import type { RouteRecordNameGeneric, RouteRecordRaw, Router } from 'vue-router';
import { getLocalAccessToken } from '@/utils/permission';
import { userService } from '@/services/api';
import { nprogress } from './helpers';
import { storeToRefs } from 'pinia';

/** 登录认证页面:账号登录页、短信登录页、二维码登录页、忘记密码页、注册页... */
const authPages: RouteRecordNameGeneric[] = [
  ROUTE_NAMES.AUTH,
  ROUTE_NAMES.ACCOUNT_LOGIN,
  ROUTE_NAMES.SMS_LOGIN,
  ROUTE_NAMES.QR_LOGIN,
  ROUTE_NAMES.FORGOT_PASSWORD,
  ROUTE_NAMES.REGISTER,
];

/** 页面白名单:不需要登录也能访问的页面 */
const pageWhiteList: RouteRecordNameGeneric[] = [...authPages];

export function beforeEachGuard(router: Router) {
  router.beforeEach(async (to) => {
    /** 进度条:开始 */
    nprogress.start();

    const { name: RouteName } = to;

    const userStore = useUserStore();
    const { getAccessToken, getRoutesAddStatus, registerRoutes } = storeToRefs(userStore);
    const { setRoutesAddStatus, setUserInfo, logout } = userStore;

    /** 访问令牌 */
    const accessToken = getAccessToken.value || getLocalAccessToken();

    // 1.用户未登录(无 Token)
    if (!accessToken) {
      const isWhitePage = pageWhiteList.includes(RouteName);
      // 1.1 未登录,如果访问的是白名单中的页面,直接放行
      if (isWhitePage) return true;

      nprogress.done();

      // 1.2 未登录又不在白名单,则拦截并重定向到登录页
      return { name: ROUTE_NAMES.ACCOUNT_LOGIN };
    }

    // 如果已登录用户试图访问登录页,避免重复登录,要强制重定向到首页
    if (authPages.includes(RouteName)) {
      nprogress.done();
      return { name: ROUTE_NAMES.ROOT };
    }

    // 判断是否需要动态加载路由的操作
    if (!getRoutesAddStatus.value) {
      // isRoutesAdded 默认为 false(未持久化),在已经动态注册过时会设置为true,在页面刷新时会重置为 false
      try {
        // 1.拉取用户信息
        const userInfo = await userService.getUserInfo();

        // 2.将用户信息存入 Store
        setUserInfo(userInfo);

        // 3.动态注册路由,registerRoutes 是处理后的路由表
        registerRoutes.value.forEach((route) => {
          router.addRoute(route as unknown as RouteRecordRaw);
        });

        // 4.标记路由已添加
        setRoutesAddStatus(true);

        // 5.中断当前导航,重新进入守卫
        return { ...to, replace: true };
      } catch (error) {
        // 获取用户信息失败(如 Token 过期失效、网络异常)
        logout();
        nprogress.done();
        // 重定向回登录页,让用户重新登录
        return { name: ROUTE_NAMES.ACCOUNT_LOGIN };
      }
    }

    return true;
  });
}
before-each-guard.ts 找到全部代码

上面的代码已经给出了很详细的注释,从整体角度来讲,我们做了两件事:

  1. 处理一些情况,比如用户未登录、登录后访问登录页、白名单等情况
  2. 拉取用户信息,动态注册路由

路由守卫逻辑图

在路由守卫中“拉取用户信息”,一般来说,除了返回用户本身的信息外,还会给到权限路由信息、权限码信息,这里的数据结构可以跟后端进行约定

比如在 vue-clean-admin 中,返回的数据结构是这样的:

在 ApiFox 文档可以找到用户接口说明:ApiFox 文档 - 用户信息

后端路由结构的转化

在通过“拉取用户信息”拿到路由数据后,并不是直接注册到 Vue Router,而是需要进行处理转化,才能符合 Vue Router 定义的路由表结构,registerRoutes 就是处理后的路由表,处理后的类型定义可以参考 CustomRouteRecordRaw

处理什么内容呢?

比如,接口拿到的路由数据字段 component 是一个字符串路径,这是一个映射路径,映射到前端项目下的真实组件路径

路由表结构转换

实现路由结构转换的代码,我写在了 router/helpers.ts,最主要逻辑是 generateRoutes 函数:

/**
 * 生成符合 Vue Router 定义的路由表
 * @param routes 未转化的路由数据
 * @returns 符合结构的路由表
 */
export function generateRoutes(routes: PermissionRoute[]): CustomRouteRecordRaw[] {
  if (!routes.length) return [];
  return routes.map((route) => {
    const { path, name, redirect, type, meta } = route;
    const baseRoute: Omit<CustomRouteRecordRaw, 'children'> = {
      path,
      name,
      redirect,
      type,
      component: loadComponent(route),
      meta: {
        ...meta,
        // 是否在侧边栏菜单中隐藏
        hideMenu: route.meta?.hideMenu || false,
        // 是否在面包屑中隐藏
        hideBreadcrumb: route.meta?.hideBreadcrumb || false,
        // 当只有一个子菜单时,是否隐藏父级菜单直接显示子菜单内容
        hideParentIfSingleChild: route.meta?.hideParentIfSingleChild || false,
      },
    };

    // 是目录数据,设置重定向路径
    if (type === PermissionRouteTypeEnum.DIR) {
      baseRoute.redirect = redirect || getRedirectPath(route);
    }
    // 递归处理子路由
    const processedChildren =
      route.children && route.children.length ? generateRoutes(route.children) : undefined;

    return {
      ...baseRoute,
      ...(processedChildren ? { children: processedChildren } : {}),
    };
  });
}

经过 generateRoutes 处理的路由表,再 addRoute 到 Vue Router 中

侧边栏菜单的渲染

当路由守卫的逻辑走完后,就进入到首页,在首页中,我们会根据路由表(转换过的)来渲染侧边栏菜单

侧边栏菜单是拿 Element Plus 的 el-menu 组件来做的,我们封装了一个菜单组件,除了渲染路由数据外,也更方便自定义配置菜单属性(meta)来实现一些功能

封装不难,就是拿处理后的路由表循环渲染 menu-item,根据 meta 配置项来实现"是否隐藏菜单","当只有一个子菜单时,是否隐藏父级菜单直接显示子菜单内容"等

菜单组件封装

菜单组件的封装代码在 basic-menu 文件夹中

到这一步,已经实现了动态权限路由及侧边栏菜单的渲染,但还不算完

因为我们还不能自由定义菜单信息、角色信息、用户信息来实现权限控制,在下一篇文章来聊聊管理模块

了解更多

系列专栏地址:GitHub 博客 | 掘金专栏 | 思否专栏

实战项目:vue-clean-admin

交流讨论

文章如有错误或需要改进之处,欢迎指正

Apache Gravitino Introduction.png

Apache Gravitino 概要介绍

作者: shaofeng shi
最后更新: [2025-12-29]

背景

在大数据时代,企业往往需要管理来自多云多域、异构数据源的元数据,如 Apache Hive、MySQL、PostgreSQL、Iceberg、Lance、S3、GCS 等; 此外,随着 AI 模型训练和推理的大量应用,海量的多模态数据、模型元数据等也需要一种方案进行管理。传统的做法是为每个数据源单独管理元数据,这不仅增加了运维复杂度,还容易造成数据孤岛。Apache Gravitino 作为一个高性能、支持地理分布式的联邦元数据湖,为我们提供了统一管理多源元数据的解决方案。

Gravitino 最初是由 Datastrato 公司发起并创立,在2023年开源,2024年捐赠给 Apache 孵化器,在2025年5月从 Apache 孵化器毕业,成为 Apache Top Level Project。目前已经在小米、腾讯、知乎、Uber、Pinterest 等企业落地生产环境。

什么是 Apache Gravitino?

Apache Gravitino 是一个高性能、地理分布式、联邦化的元数据湖管理系统,为用户提供统一的数据和AI资产管理平台,它能够:

  • 统一元数据管理:为不同类型的数据源提供统一的元数据模型和API
  • 直接元数据管理:直接管理底层系统,变更会实时反映到源系统
  • 多引擎支持:支持Trino、Spark、Flink等多种查询引擎
  • 地理分布式部署:支持跨区域、跨云的部署架构
  • AI资产管理:不仅管理数据资产,还支持AI/ML模型的元数据管理

核心概念包括:

  • Metalake:元数据的容器/租户,通常一个组织对应一个metalake
  • Catalog:来自特定元数据源的元数据集合
  • Schema:第二级命名空间,对应数据库中的schema概念
  • Table:最底层的对象,表示具体的数据表

Gravitino 整体架构

Apache Gravitino 核心特性概述

统一元数据管理

Gravitino 提供了一个统一的元数据管理层,支持多种数据源的集成:

支持的数据源类型:

  • 关系型数据库:MySQL、PostgreSQL、OceanBase、Apache Doris、StarRocks 等
  • 大数据存储:Apache Hive、Apache Iceberg、Apache Hudi、Apache Paimon、Delta Lake(开发中)
  • 消息队列:Apache Kafka
  • 文件系统:HDFS、S3、GCS、Azure Blob Storage、阿里云 OSS
  • AI/ML 数据格式:Lance(专为AI/ML工作负载设计的列式数据格式)

REST API 服务

Gravitino 提供了丰富的 REST API 服务,支持不同数据格式的标准化访问:

Gravitino 核心 REST API

  • 完整的元数据管理 RESTful API 接口
  • 支持 Metalake、Catalog、Schema、Table 等所有元数据对象的 CRUD 操作
  • 支持用户、组、角色和权限管理的完整 API
  • 提供标签、策略、模型等高级功能的 API 接口
  • 支持多种认证方式(Simple、OAuth2、Kerberos)

Iceberg REST 服务

  • 遵循 Apache Iceberg REST API 规范
  • 支持多种后端存储(Hive、JDBC、自定义后端)
  • 提供完整的表管理和查询能力
  • 支持多种存储系统(S3、HDFS、GCS、Azure等)

Lance REST 服务

  • 实现 Lance REST API 规范
  • 专为 AI/ML 工作负载优化
  • 支持高效的向量数据存储和检索
  • 提供命名空间和表管理功能

元数据实时获取和修改

Gravitino 采用直接元数据管理模式,确保数据的实时性和一致性:

  • 实时同步:对元数据的变更会立即反映到底层数据源
  • 双向同步:支持从 Gravitino 到数据源,以及从数据源到 Gravitino 的元数据同步
  • 事务支持:保证元数据操作的原子性和一致性
  • 版本管理:支持元数据的版本控制和历史追踪

统一访问控制

Gravitino 实现了跨多数据源的统一权限管理:

核心特性:

  • 基于角色的访问控制(RBAC):支持用户、组、角色的灵活权限管理
  • 所有权模型:每个元数据对象都有明确的所有者
  • 权限继承:支持层次化的权限继承机制
  • 细粒度控制:从 Metalake 到具体表的多层级权限控制

支持的权限类型:

  • 用户和组管理权限
  • 目录和模式创建权限
  • 表、topic、fileset的读写权限
  • 模型注册和版本管理权限
  • 标签和策略应用权限

统一数据血缘

基于 OpenLineage 标准,Gravitino 提供了完整的数据血缘追踪能力:

  • 自动血缘收集:通过 Spark 插件自动收集数据血缘信息
  • 统一标识符:将不同数据源的标识符转换为 Gravitino 统一标识符
  • 多数据源支持:支持 Hive、Iceberg、JDBC、文件系统等多种数据源的血缘追踪

高可用性和扩展性

部署模式:

  • 单机部署:适合开发和测试环境
  • 集群部署:支持高可用和负载均衡
  • Kubernetes 部署:支持容器化部署和自动扩缩容
  • Docker 支持:提供官方 Docker 镜像

存储后端:

  • 支持多种元数据存储后端(MySQL、PostgreSQL等)
  • 支持分布式存储系统

安全特性

认证方式:

  • Simple 认证(用户名/密码)
  • OAuth2 认证
  • Kerberos 认证(针对 Hive 后端)

凭证管理:

  • 支持云存储凭证代理(S3、GCS、Azure等)
  • 动态凭证刷新
  • 安全的凭证传递机制

Apache Gravitino 的集成能力

Gravitino 与主流计算引擎和数据处理框架深度集成,为用户提供统一的数据访问体验。

计算引擎集成

Apache Spark

  • 通过 Gravitino Spark Connector 实现无缝集成
  • 支持 Spark SQL 和 DataFrame API
  • 自动数据血缘收集和追踪
  • 支持多种数据源的统一访问

Trino

  • 通过 Gravitino Trino Connector 服务集成
  • 支持跨数据源的联邦查询
  • 高性能的分析查询能力

Apache Flink

  • 通过 Gravitino Flink Connector 服务集成
  • 支持流批一体化数据处理
  • 实时数据处理和分析

Python 生态集成

PyIceberg

  • 支持 Python 环境下的 Iceberg 表访问
  • 与 Gravitino Iceberg REST 服务集成
  • 支持数据科学和机器学习工作流
  • 提供 Pandas 兼容的数据接口

Daft

  • 现代化的分布式数据处理框架
  • 专为 AI/ML 工作负载优化
  • 支持多模态数据处理
  • 与 Gravitino 元数据管理集成

云原生集成

Kubernetes

  • 支持 Kubernetes 原生部署
  • 提供 Helm Charts 和 Operator
  • 支持自动扩缩容和故障恢复
  • 集成云原生监控和日志系统

API 和 SDK

REST API

  • 完整的 RESTful API 接口
  • 支持所有元数据管理操作
  • 标准化的 HTTP 接口
  • 支持多种认证方式

Java SDK

  • 原生 Java 客户端库
  • 类型安全的 API 接口
  • 支持连接池和重试机制
  • 完整的异常处理

Python SDK

  • Python 客户端库
  • 支持异步操作
  • 与 Jupyter Notebook 集成
  • 支持数据科学工作流

这些集成能力使得 Gravitino 能够无缝融入现有的数据基础设施,为用户提供统一、高效的数据管理体验。后续文章将详细介绍 Gravitino 的各项能力、各个集成组件的配置和使用方法,敬请关注。

下一步


Apache Gravitino正在快速发展中,本文基于最新版本编写。如遇到问题,建议查阅官方文档或在GitHub上提交issue。