很多人把AI SaaS平台的用户权限体系设计想简单了,觉得不就是“用户表加角色表,登录后查一下角色,再判断能不能点某个按钮”嘛。但真把一个带模型调用、数据集管理、Agent任务、API配额计费等功能的AI SaaS产品上线之后,你会发现权限问题永远不只出现在菜单层级,更多是出现在对象层级、动作层级甚至Token层级的越权里。今天趁着一个周末复盘的空档,我把做AI SaaS时梳理权限模型到最终落地RBAC的这套实践完整写下来,全程不贴官方废话,只讲我在选型、建表、编码和排障过程中真正用到并且验证有效的方案。
本文适合至少已经了解基本RBAC概念、准备在AI SaaS产品里动手做权限体系的技术负责人或后端开发。如果你还在犹豫是纯自研还是接现成框架,这篇也能帮你看清各自边界。我会重点拆解资源维度的权限设计、AI Agent操作时的授权难题、以及多租户下容易出现的越权场景,尽量做到直接抄作业。
1. AI SaaS平台的权限需求远比你想象的复杂
1.1 传统RBAC解决的只是一半问题
先说为什么我不建议直接照搬十年前论坛系统的角色权限模型。传统RBAC的核心是“用户-角色-权限”三层,权限往往是一个个功能点,比如“创建数据集”“删除模型”“查看账单”。这种模型适合业务边界清晰、资源类型少的后台系统,但对AI SaaS而言,用户的权限经常要和具体资源绑定:A用户只能看到自己创建的LoRA模型,B用户只能调用某个已开通的模型服务。如果权限只做到功能点级别,那你只能允许用户“查看模型”,但他能看哪些模型,还需另配一套规则。
在搭建我们自己的AI智能体编排平台时,一开始我就是加的“普通成员/超级管理员”两级角色,上线第三天就接到一条故障反馈:某个普通用户能通过直接改URL里的项目ID,访问到同租户另一个项目下的Prompt调试记录。问题并不出在登录鉴权,而在于我的接口只校验了“该用户是不是该项目的成员”,却没有校验“该用户在项目内是否拥有读取调试记录这个动作的权限”。传统RBAC里“角色”这个中间层管住了动作,却管不住动作作用在哪个对象上。
所以我对AI SaaS平台的权限模型做了这样的拉高:RBAC要解决的是“谁能做什么”,但要真正避免越权,还需要在每一个资源实例前回答“谁能对哪一条数据做”。这正是业界常说的对象级权限,通俗一点就是把菜单级权限和行级权限拆开设计。很多入门教程告诉你RBAC已经足够,那是指“角色管理本身”,你的平台若要接API Key、模型租户隔离,那RBAC一定要突破五张表的概念,落成“角色+资源范围+权限约束”的组合。
1.2 AI产品的资源类型长什么样
为了讲清模型设计,我先给出一份很典型的AI SaaS资源清单。你可以对照自己的平台进行删减:
- 应用:可视化搭建的对话应用、Agent应用、工作流应用,每一项都有自己的粒度。
- 模型服务:为每个租户开通的模型API、私有化部署的模型服务、模型版本。
- Dataset数据集:用户上传的训练数据、标注数据、评测集文件。
- Prompt模板:可复用的提示词资产。
- 知识库及文档分段:企业版几乎都是按数据源和知识库做隔离。
- Agent任务实例:一次自动化任务运行,包含输入、中间步骤日志、输出结果、Token消耗。
- 批量推理任务:定时执行任务、异步任务组。
- 费用与配额:账户余额、模型配额、调用次数。
- 团队成员:邀请成员、管理成员角色。
你会发现前六项属于典型的“业务数据”,而最后两项既是资源,也经常被当作权限管控的元数据。比如一个子账号在调用某个模型功能时,系统要同时检查“该子账号所在工作区是否开通了此模型”“子账号的角色允许不允许调用外部模型”“该子账号的配额是否足够”。这三层任何一个没打通,都会出现权限或计费上的漏洞。
1.3 先治权限设计的两个典型病
每个把权限做崩的项目,踩的坑都差不多,我在Code Review和线上事故复盘里总结了两种典型:一是“前端藏按钮,后端裸奔”,很多团队只是把没权限的按钮从界面上隐藏起来,但接口只要知道了ID就能调通。二是“租户看到别的租户”,开发时全员使用同一份开发数据,导致SQL在过滤租户ID时漏了一个查询条件,结果线上用户通过翻页能拉到其他公司的私有知识库片段。
用一句话概括我的结论:AI SaaS平台必须采用“三层权限检查”的降级方案。第一层,基础身份认证;第二层,RBAC角色判定,解决“能不能触发该功能”的问题;第三层,资源Owner和数据范围判定,解决“能触发该功能但能不能动这条记录”的问题。每一层都不能省略,实践中也少了任何一层都会成为隐患加高危险。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数据模型设计:从五张表到可落地的九张表
2.1 基础表设计要避开的坑
很多老项目里权限相关表只有五张左右,但到了真实AI产品中,我需要额外增加“资源范围”“动作权限策略”等概念。先展示基础RBAC表的建表方式,加了编号便于回忆:
sql复制--用户主表,这里的字段会和租户/成员关系整合
CREATE TABLE auth_user (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(128) NOT NULL,
status TINYINT NOT NULL DEFAULT 1,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
--角色表,作用域是全局还是某个租户
CREATE TABLE auth_role (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
role_code VARCHAR(64) UNIQUE NOT NULL,
role_name VARCHAR(128) NOT NULL,
scope VARCHAR(32) NOT NULL DEFAULT 'WORKSPACE',
-- 可选值:GLOBAL/WORKSPACE
data_scope VARCHAR(32) NOT NULL DEFAULT 'SELF',
description VARCHAR(512),
is_system TINYINT NOT NULL DEFAULT 0
);
-- 权限资源表(权限点)
CREATE TABLE auth_permission (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
perm_code VARCHAR(128) UNIQUE NOT NULL,
display_name VARCHAR(128) NOT NULL,
resource_type VARCHAR(64) NOT NULL,
action VARCHAR(64) NOT NULL,
is_enabled TINYINT NOT NULL DEFAULT 1
);
-- 角色-权限关联表
CREATE TABLE auth_role_permission (
role_id BIGINT NOT NULL,
permission_id BIGINT NOT NULL,
PRIMARY KEY (role_id, permission_id)
);
如果你没有多租户概念,将角色表的scope去掉即可,但实际AI SaaS几乎都会面对企业级多人协作问题,至少都要挂靠“工作空间”。真正容易忽略的是resource_type和action的联合判断。perm_code建议写成dataset:create或model:invoke这种格式,后面做权限匹配时直接用两段拆分,比纯自增ID好排查。
2.2 加一张资源归属表,打通对象级权限
只有角色权限表,遇到“能否读取这个数据集”这种问题时还是很尬尴。我增加一张“资源归属表”,用一个通用抽象把各类资源的一层层Owner关系记录起来,可以让重复代码少很多:
sql复制CREATE TABLE resource_owner (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
resource_type VARCHAR(64) NOT NULL,
resource_id VARCHAR(64) NOT NULL,
owner_user_id BIGINT DEFAULT NULL,
owner_workspace_id BIGINT NOT NULL,
parent_resource_type VARCHAR(64) DEFAULT NULL,
parent_resource_id VARCHAR(64) DEFAULT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_resource (resource_type, resource_id)
);
在AI平台里,一条Prompt模板可能属于某个项目,项目又属于某个用户所在的工作空间。资源Owner表会冗余整个链路,查询性能更好。当A用户尝试GET /v1/prompt-template/{id} 时,我们会先用模板ID查到这一行的owner_workspace_id,再判断当前用户是不是这个工作空间的成员。
如果让SQL直接去业务表里判断有没有workspace_id,也没毛病,但每加一种资源类型就要多写一份查询分支。有了这张通用表,我可以在权限中间件里用一个注释解决一大片问题,权限判断从“查应用表或数据集表”统一翻译成“查resource_owner表”。
2.3 再增加角色-数据范围关联
数据范围是RBAC的重要增强点。我常举一个例子:运维角色和生产管理员角色都能“查看系统日志”,但运维只能看自己负责节点上的推理日志,而管理员能看整个工作空间的日志。因此我单独把数据范围配置抽出来:
- ALL:查看当前工作空间内全部数据。
- WORKSPACE_GROUP_ONLY:只能查看指定用户组或资源组的数据。
- SELF_AND_SUBORDINATE:可查看自己和下级成员创建的数据。
- SELF:只能查看自己创建的数据。
写表时不需要把这种策略也揉进角色权限关联表。建议角色表里新增一个字段data_scope,然后另加一个表存储“数据范围限定条件”。
sql复制CREATE TABLE auth_role_data_scope (
role_id BIGINT NOT NULL,
scope_type VARCHAR(32) NOT NULL,
scope_value VARCHAR(128) DEFAULT NULL,
PRIMARY KEY (role_id, scope_type, scope_value)
);
scope_type 可能是一个部门ID、一个用户组ID,甚至一个正则表达式,比如限制只能访问前缀名以“beta-”开头的模型项目。虽然这在系统里很少用,但做私有化交付时经常会需要这种灵活配置,提前写进设计会让后续拓展容易很多。
3. 授权链路设计:登录、角色解析、资源过滤一鼓作气
3.1 一次请求的权限校验全链路
举个实际场景:前端调用“运行一次Agent任务”,后端在处理这个请求时不能只检查一次权限。我的做法是分三处接入权限检查:
第一步,网关层或过滤器层处理身份认证。这里使用JWT还是临时Token取决于你的会话格式。AI SaaS通常会有两个访问端:控制台用户使用用户Token,开放平台API用户使用独立的API Key。因此我在身份层就拆分两个上下文,分别取出代表身份的Principal。
第二步,服务入口处做粗粒度的action级判定。业务服务收到请求后,先根据当前用户的所有角色解析是否拥有agent_task:create权限。这一层可以使用缓存的权限集合,不用一上来就查数据库,效率会高很多。若判定失败,直接返回403。
第三步,执行逻辑里做数据级判定。在真正读取业务对象时,通过一个权限SQL或一个守卫方法,过滤掉用户不能操作的数据。这一步最难,也是防止越权的最后防线。
我把三步整理成一个PHP思想但用Java表达的表单:
text复制前置伪代码:
1. principal = auth.token.getPrincipal()
2. roles = roleCache.getRoles(principal.workspaceUserId)
3. if !permissionChecker.hasAnyPermission(roles, "agent_task:create") -> return 403
4. task = agentTaskRepository.findById(taskId)
5. if !resourceOwnerChecker.canAccess(principal, "agent_task", taskId) -> return 403
只有同时通过两层,任务才能继续。实际上还会有一些特殊限制,比如是否超过了并发任务数,这属于配额语义,和权限分开比较好,不要让权限系统承载计量功能。
3.2 权限判断为什么容易出现“查不到权限”
这类问题是实现中最常见的:判断成功了或失败了结果跟角色配置完全不一致。这里往往有几个原因。
第一个原因是把角色解析结果存错地方。用户加入一个新角色后,由于缓存未刷新,权限判断还是会用旧角色数据。解决办法是在成员角色变更时,主动删除auth_workspace_user_role相关缓存。如果不想引入Redis,也可以直接使用进程内Caffeine并设置较短过期时间。
第二个原因是使用“OR”权限拼接。一个用户拥有多个角色,判定某个动作时需要把多个角色的权限union起来。很多初学者直接用第一次查到的角色结果去判断,结果隐藏了超级管理员的权限。正确做法是:
java复制Set<String> permissionCodes = rolePermissionCache.getPermissionsByRoleIds(roleIds);
boolean allowed = permissionCodes.contains("agent_task:create")
|| permissionCodes.contains("agent_task:*")
|| permissionCodes.contains(":*:create");
所以权限码是支持通配符的,一个admins角色的权限集合可以存储成无限大模型,也可以精简为“ * ”。实现时只要在匹配阶段多一层通配符处理即可。注意不要把通配符判断放在数据库SQL里用LIKE,数据量上来后会拖垮整个资源校验接口。
第三个原因是把“全局策略”混进“用户角色”的代码上。比如管理员需要禁止某个用户调用指定模型,平台实际逻辑可能来自一个“模型级禁用列表”。这里的来源权重高于角色,因此角色判断再通过也不能越过。实际操作中不要试图把所有判定都抽象成一个万能方法,我建议至少拆出角色权限和策略限制两个方法,最后统一做决策。
3.3 不单独设计角色也能实现资源范围过滤的临时方案
如果你现在接手的是个老项目,表结构一时半会儿改不过来,也可以先用最简单的方式把数据权限兜住:所有涉及查询的Mapper方法都强制拼接数据范围条件。如记录每个业务表里的creator_id和workspace_id,然后在Repository层写入一个具有强制约束的底层查询对象:
java复制public <T> PageResult<T> selectUserVisibleRows(
Long workspaceUserId, Long workspaceId, String userIdColumn, String workspaceIdColumn) {
QueryWrapper<T> wrapper = new QueryWrapper<>();
wrapper.eq(workspaceIdColumn, workspaceId);
wrapper.and(w -> w.eq(userIdColumn, workspaceUserId)
.or().eq(userIdColumn, -1));
// -1 表示工作空间内公共资源
}
这样即便没有复杂的resource_owner表,也能先挡住越权查询。缺点是每一种业务查询都要维护拼接条件,写漏一个字段就是一只“漏网之鱼”。所以我专门留了一句话在团队规范里:不要在自己的Service层里用不带租户过滤条件的Page查询。
4. AI Agent与开放API场景下的临时授权实践
4.1 Agent自主调用工具时,角色权限会失灵
说完传统控制台管理,再补充AI SaaS里更容易出幺蛾子的部分——Agent运行时授权。Agent的核心特点是:它会根据用户输入自主判断需要调用哪些工具,因此很难提前把“用户触发调用的所有动作”列出来。比如用户只是说“帮我查一下上个月的模型调用费用”,Agent可能会自动去调账务工具的查询接口。若这个Agent拥有管理员角色,那么它可能能够直接访问全工作空间的大量私密数据。
曾经在某个客户环境里出现过这样一个事故:企业内部通过Agent查内部费用时,Agent私自横向遍历了一次API账单数据。按理说,Agent没有权限去执行“加载账单列表”这个动作,但因为人的身份会话传递下去之后,Agent从上下文里继承了一个超管Token,等于把业务漏洞放大了几十倍。
为了避免这类问题,我给我们的Agent任务设计了一套“动态授权最小化”机制。核心是三步:
- 将Agent执行时的身份与发起人身份解耦,生成临时的Agent执行身份。
- 对这个执行身份分配一个最低权限的角色,默认只给工具调用的基础权限,比如“读取当前用户信息”。
- 在Agent要调用的每一个外部工具前,都增加一次“工具级授权预检”。
每一条Agent消息进入调度前,系统会先执行工具权限决策表。工具权限决策表类似:
text复制工具名 敏感级别 是否允许自动调用 需要用户确认
datasets.query 高 否 是
billing.summary 高 否 是
llm.invoke 中 是 否
app.template.get 低 是 否
mcp.github_read 中 是 否
如果“需要用户确认”为是,系统会暂停Agent执行,向用户推送一条确认弹窗。用户确认后的授权有效期可以设置为单次或五分钟。这个模型的本质是OAuth中的短时授权思想,但没有真的标准化成为OAuth框架。相比直接放开管理员权限,这样既保持了Agent的使用体验,又对高风险动作做了拦截。
4.2 API Key与用户角色之间怎么映射
AI SaaS经常会对外开放平台。如果用户自己申请了API Key,该Key的调用主体通常不是“用户本人”,而是“一个应用或项目”。在设计时要把API Key当作一个独立Principal,而不是用户本身的另一个口令。
我设计的API Key表格字段可以类似:
sql复制CREATE TABLE api_key (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
workspace_id BIGINT NOT NULL,
creator_user_id BIGINT NOT NULL,
api_token_hash VARCHAR(128) NOT NULL,
role_id BIGINT NOT NULL,
rate_limit_per_minute INT DEFAULT 60,
allowed_ip_list VARCHAR(1024) DEFAULT NULL,
expires_at DATETIME DEFAULT NULL,
last_used_at DATETIME NULL,
status VARCHAR(16) NOT NULL
);
通过API Key访问接口时,不直接把某个用户的完整用户角色授予Key。我建议给Key单独绑定一个低权限角色,比如只授予model:invoke和billing:read。这样即使有员工拖库拿到API Key,也无法通过Role继承去读取企业内部工作空间的任何私有知识点。
有一个常见教训是,团队为让API Key能完成某些“看似理所当然”的调用,往往会绑定一个项目管理员角色。这样一旦Key查询类的接口发生逻辑漏洞,攻击者就能跨越资源范围去读取他人数据。核心还是要把最小权限理念落实到“Key”这个实体上,而不是重情面。
4.3 会话内临时提权:让权限系统支持即时扩大
我有时会在产品里碰到这样的需求:一个普通成员想把某个Agent发布到生产环境,按照常态权限他是不允许操作的。但这个操作是一锤子买卖,你不应该临时到数据库里把这个人的角色改成管理员,操作完再改回来。这种“数据库改角色法”风险极高,权限变更日志也很难管控。
正确做法是实现一版“会话抬权”功能。比如用户提交一个发布申请,管理员审批通过后,系统给该发布任务生成一个有效期为一小时的临时权限票据。票据中记录可执行的动作集合,如下所示:
json复制{
"grant_id": "pub_grant_8f0a",
"principal": {
"user_id": 1024,
"workspace_id": 88
},
"permissions": [
"pipeline:release",
"pipeline:rollback"
],
"resource_constraints": {
"app_ids": [301, 302]
},
"valid_from": "2025-05-01T10:00:00Z",
"valid_to": "2025-05-01T11:00:00Z"
}
在权限判定时,把临时票据作为一种额外角色叠加,判断顺序为黑名单策略、显式禁用策略、临时票据、用户静态角色。票据到期后自动失效,也不需要后台定时任务扫表。这是RBAC模型在AI SaaS里非常有用的“活变体”。
5. 权限判断的代码实现与性能优化技巧
5.1 需要自己造轮子还是引入Casbin/Spring Security
在对几套方案做了横向对比后,我的建议是:AI SaaS产品优先选择自研轻量RBAC + 策略判断,而不是一上来引全家桶。原因在于权限系统再复杂,本质也只是集合运算和过滤条件,框架给你最多的价值其实是策略引擎,但学习成本、版本升级坑也不少。
如果你只想直接用人家的策略语法,Casbin确实不错,能把权限模型写入conf文件,支持自定义函数。我对它的担心集中在两点:一是多租户项目的模型调试文档少,二是Agent工具调用这种短时授权的上下文动态语义不好表达。如果中后期不打算做特别复杂的ABAC场景,直接用Spring Security的@PreAuthorize注解来做入口收敛,再用我自己的权限Service做资源范围判断,反而更干净。
一个稳定可用的最小自研方案由四部分组成:
- 权限码枚举或静态常量类。
- 当前用户上下文解析器。
- 角色权限缓存。
- 资源过滤工具包。
最忌讳的就是在业务类里直接注入一个RoleMapper去查库。时间久了,各种业务代码都会自成一派,你根本没法统一贯彻权限策略。我在项目里强制要求业务代码只能面向PermissionService接口,才能放心从Controller层一路下沉到Service层。
5.2 一个Java注解与AOP实现的小例子
以Spring Boot为例,展示在Controller方法或服务方法上添加权限注解是最高效的做法:
java复制@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequirePermission {
String value();
String resourceType() default "";
String resourceIdParam() default "";
}
AOP里的核心逻辑:
java复制@Aspect
@Component
public class PermissionAspect {
@Around("@annotation(permission)")
public Object checkPermission(ProceedingJoinPoint pjp, RequirePermission permission) throws Throwable {
// 1. 获取当前登录用户
AuthPrincipal principal = AuthContext.get();
// 2. 判定是否有该动作权限
Set<String> codes = permissionService.getPermissionCodes(principal);
if (!permissionMatcher.match(codes, permission.value())) {
throw new ForbiddenException("没有操作权限");
}
// 3. 如果声明了资源类型,进一步做数据级权限校验
if (StringUtils.hasText(permission.resourceType())) {
Object resourceId = AopUtils.getParameterValue(pjp, permission.resourceIdParam());
if (resourceId != null) {
boolean canAccess = permissionService.canAccessResource(
principal, permission.resourceType(), String.valueOf(resourceId));
if (!canAccess) {
throw new ForbiddenException("不能访问该资源");
}
}
}
return pjp.proceed();
}
}
这种做法很适合平时业务边界清晰的规则。注意,注解仅适合判断某个固定的resourceId,如果查询行为自带一个复杂的筛选条件,例如“批量删除Agent任务”,注解做起来就很不顺。我的原则是:删除/修改/详情类单点操作尽量用注解,列表查询必须走Mapper层的数据范围过滤。
5.3 权限判断性能瓶颈与优化
在AI SaaS里,一次对话可能要调用十余个异步工具,每个工具都需要做一次权限判断。如果判断逻辑里多次查库,接口的P99会很容易被拖爆。为此我总结了三个经验:
第一个经验是缓存要分层。用户角色缓存为五到十分钟;角色权限缓存为十到三十分钟;资源归属信息用本地缓存,在数据更新时主动失效。这样做的好处是权限信息是读多写少的典型数据,在分布式部署时对Redis的压力也不会太大。
第二个经验是批量判断要提供批量接口。不要业务层循环去调canAccessResource,每次循环都发一次SQL。提供类似Map<Long, Boolean> filterVisibleResourceIds(user, resourceType, ids)的接口,把“能看的数据ID列表”一次性取回来。
第三个经验是不要把权限码做成一堆超级长的字符串还去倒排索引。权限码字符串比较只有在HotPath较复杂时才明显。可以改用两级Set存储,前半部分hash成资源类型枚举,后半部分保留动作集合。判定时先检查资源类型是否存在,再判断动作是否命中,性能上开销很小,但代码复杂度也随之上升。
6. 权限体系上线前必做的十条安全审查清单
6.1 从越权视角逆向测试
每次权限系统开发完成之后,我习惯拉一份“越权测试清单”,它比常规功能测试更接近安全视角。清单不是给测试同学简单点点点,而是让后端工程师配置好自测数据后逐条走:
- 用户A能直接访问用户B的应用详情吗?通过把URL中的ID替换为B的ID。
- 用户A能调用用户B的Dataset创建文件接口吗?尝试使用默认密钥。
- 普通用户能否给自己提升角色?调用更新成员角色接口,传自己的新角色。
- 跨工作空间的API Key能否访问到其他工作空间的模型?尝试用旧环境的OpenAPI访问。
- 批量查询接口是否存在翻页越界?修改pageNo并检查是否出现别人名字出现。
- 导出的文件内是否完全隔离?在导出大量内容里混入其他租户的数据。
- Agent执行时的“用户确认”弹窗是否可以绕过?注意恶意的“不使用工具直接生成伪造结果”。
- 临时权限票据是否可以重复使用多个业务资源?尝试在granted权限外调用其他接口。
- 被禁用用户的活动Token是否还能继续访问?在Redis里保留一份旧JWT做测试。
- 通配符权限是否被解释成“可越权”?确认不能通过
resource:*直接访问不属于该用户的资源。
以我经验来说,上面第一至第三条出错概率最高,往往是SQL的滥用或前端不当传参导致。上线前专门拿半天时间做“越权盲测”的效果,胜过安全工程师事后溯源。
6.2 从审计日志里发现问题
权限越权不一定都发生在被攻击时,很容易出现在业务员偶尔点错、或者权限配置异常上。所以在权限系统设计之初就要记录足够完整的关键审计日志。至少包括以下维度:谁在什么时间,通过哪个客户端,调用了哪个接口,操作了哪个资源ID,最终是allow还是deny。
在AI场景里,还需要额外记录“外部工具调用”的日志,尤其要记下触发的Agent运行ID和Prompt的引用ID。这样如果有用户执行结果中出现权限判定异常,运维才不只能看到“无”和“允许”,才有能力复现当时的上下文。
审计日志不要写到业务数据库里,建议直接通过事件总线发到ElasticSearch或者S3存储。权限操作的频次可能很高,全丢弃的关键bug会造成严重影响。我见过一位同事把审计日志放在MySQL中,导致启动时整体数据库CPU升高到80%,后来不得不把写入方式改成异步批量后,CPU又降到了5%以下。这说明审计设计不能晚于权限功能本身。
6.3 让权限配置能够可回滚
这部分属于进阶细节。企业管理员配置角色权限时,如果勾选错一项权限,可能要影响数十个成员。为支持随时回滚,我会在角色权限变更时保存一份快照,我习惯在数据库里存JSON diff,而不是单纯存旧值。比如:
json复制{
"before": {
"roles": {
"173": {
"employee": ["dataset:read", "model:invoke"]
}
}
},
"after": {
"roles": {
"173": {
"employee": ["dataset:read", "model:invoke", "billing:read"]
}
}
},
"operator_id": 1024,
"change_reason": "临时支持财务查询"
}
用开源工具像javers也可以。实际项目里不需要把每个字段都落进角色表,只需要做好这类变更事件。等到出了问题时,我们才是真正能恢复回去的:先从事件记录找到“before”内容,再一键写入缓存和数据表。
7. 以个人习惯总结:高可用权限系统的三个不被文档强调的原则
最终我想留下三个原则,能长期在复杂的权限体系里压住风险,也可以当作团队下一阶段继续演进的指针。
第一,权限永远要显式禁止大于隐式允许。很多系统默认返回false就不让过,这当然是对的。但有时候会出现“新增一个导入功能时忘了给任何角色权限,于是所有人皆不能导入”,这倒也是安全的。更难的是另一种情况:新增功能A后,允许了“管理员”角色访问,但管理员实际并不拥有这份数据的所有权,就会产生越权灰产。因此,系统里要保留一个“策略顺序”的概念,禁用策略必须排在白名单之前,哪怕该用户拥有通配符权限也必须遵守。
第二,权限上下文必须打印在每条关键日志旁。有一次线上排查越权问题时,我发现后端日志里只有“用户请求失败”,没有任何角色与资源上下文,被迫从网关日志中翻了很久。现在代码在返回403时会将Principal、资源类型、资源ID、当前角色标识、击中策略打印出来,几近等于把所有权限决策过程透明化了,排查速度提高了一倍多。
第三,定期做最小权限自动巡检。有些管理员喜欢把角色全部绑成“系统管理员”,虽图一时方便,但安全隐患相当大。我写了一个定时任务,每天扫描拥有“*”权限的角色及其成员,推送给租户Owner。它能自动找出处于30天以上不活跃状态的Key且权限过大的,再发送一封预警告警邮件。授权长期不可能一直是静态的,持续收敛才能避免“权限膨胀”。
这次AI SaaS权限体系从RBAC到对象级鉴权,又到Agent临时授权,再细化到自研实现和审计方案,整个过程如果只挑一个核心经验,那就是:把RBAC从“角色功能点”升级为“角色+资源范围+动作预检”的三元模型,你的权限体系才能适应AI产品的多对象和动态执行特征。等我下一次再延展,可能会把实验空间里更复杂的ABAC规则补上,但基础不乱,加什么规则都稳得住。
