WeKnora 文档权限管理实战:一次请求如何走完全程的 RBAC 访问控制与多租户隔离
【免费下载链接】WeKnoraOpen-source LLM knowledge platform: turn raw documents into a queryable RAG, an autonomous reasoning agent, and a self-maintaining Wiki.项目地址: https://gitcode.com/GitHub_Trending/we/WeKnora
WeKnora 是一个开源 LLM 知识平台,让文档变成可查询的 RAG 知识库。当多个团队共用同一套知识库时,怎么保证"别家的文档绝不可见"?本文跟随一次 HTTP 请求的完整生命周期,拆解 WeKnora 的文档权限管理链路:JWT 与 API Key 双通道认证、空间级 RBAC 访问控制、查询层自动附加 tenant_id 过滤,最后落到多租户数据隔离的令牌回收闭环。
登录发令牌,还是传 API Key:中间件怎么认出你是谁
先看问题。权限系统的第一道关卡不是"你能做什么",而是"你是谁"。WeKnora 里有两类调用方:浏览器里登录的真人,和脚本里拿着密钥的机器。两者的凭据形态完全不同,如果中间件写两套逻辑,路由配置很容易漏。
认证中间件的做法是把识别收敛成三条固定通道,按顺序尝试:
// Auth 中间件:按固定顺序尝试三条通道 if isNoAuthAPI(path, method) { c.Next(); return } // 登录、注册等白名单接口 if user, err := userService.ValidateToken(ctx, bearer); err == nil { authenticateJWTUser(c, user, tenantID) // 通道一:JWT,解析空间与角色 return } authenticateAPIKeyRequest(c, xAPIKey) // 通道二:X-API-Key // 三通道全部未命中 → 返回 401JWT 通道服务真人:登录成功后签出一对令牌,短的 access_token 用于日常请求,长的 refresh_token 用来续期,令牌会落库登记,支持后续撤销。API Key 通道服务机器:请求头带上X-API-Key,服务端反查出所属空间。值得注意的细节是,API Key 路由门禁给每条路由单独声明策略,未声明的路由对 API Key 默认拒绝(fail-closed),避免"忘了加限制"这种经典事故。
两条通道殊途同归:认证结果会被写进请求上下文,为后面的角色判断和租户过滤做准备。
怎么给路由挂角色门槛:RequireRole 的接法
认出身份后,下一个问题是:这个身份能不能碰这条路由?这就是 RBAC 访问控制的用武之地。
WeKnora 的空间成员有四级角色:viewer < contributor < admin < owner,高角色继承低角色权限。角色检查集中在 RequireRole 中间件,接法很直白——路由注册时声明最低角色即可:
// 路由声明最低角色:访客进不了写操作的门 group.POST("/knowledge-bases/:id/knowledge", middleware.RequireRole(types.TenantRoleContributor, cfg), h.AddKnowledge)三个设计决定值得记住:
- fail-closed 兜底:如果上下文里没解析出角色,读取方默认按
viewer处理,宁可直接拒绝也不放行。 - 灰度开关:配置项
tenant.enable_rbac关闭时,越权请求只记日志不拦截,方便生产环境平滑上线;开启后同样的代码开始真正返回 403。 - 归属优于角色:contributor 在"自己创建的知识库"里拥有完全控制权,在别人的库里等同 viewer。资源表上的
creator_id字段是归属判断的依据,配合OwnedKBOrAdmin这类归属守卫,写操作要求"是创建者或至少 Admin"。角色矩阵与归属模型的完整说明见空间 RBAC 文档。
怎么为查询自动附加租户过滤
角色只管"操作级别",真正的多租户数据隔离发生在数据访问层——而且它不需要业务代码"记得"加条件。
关键动作发生在认证收尾处:applyAuthSession 把身份一次性写入请求的两个读取面(gin 键与 request context),保证下游无论用哪种方式取都一致:
// 认证通过后统一写入,供全链路共享 if s.TenantID != 0 { set(types.TenantIDContextKey, s.TenantID) // 租户隔离的关键 } set(types.UserContextKey, s.User) set(types.TenantRoleContextKey, s.Role) // 空间内角色下游的 repository 层从这里取出 tenantID,拼进每条 SQL。以知识库为例:
// 知识库的所有读取都强制带上租户条件 db.Where("id = ? AND tenant_id = ?", id, tenantID).First(&kb) db.Where("tenant_id = ?", tenantID).Find(&kbs)这就是"自动"的含义:租户 ID 不是前端传来的参数,而是认证链路单方面注入的,业务代码无法绕过。跨空间超级管理员是明确的例外,需要CanAccessAllTenants标记且通过canAccessTenant门禁后才生效,属于受控通道而非默认行为。
登出时怎么把令牌作废旧
权限的最后一环常常被忽略:登出后,旧令牌还该不该有效?
WeKnora 的令牌不是"签出去就不管"。用户服务维护一张令牌登记表,access_token 与 refresh_token 分开标记类型。登出时调用RevokeToken把令牌状态置为已撤销,之后即使签名仍有效、未过期,ValidateToken校验登记状态后也会拒绝放行。refresh 换发新令牌时,旧 refresh_token 同步作废,形成滚动回收。配合较短的 access_token 有效期,泄露窗口的影响被压到最小。
至此一次请求走完了全程:认证认出你是谁 → 角色判断你能做什么 → 查询过滤你能看什么 → 登出终止你的令牌。
延伸阅读
- 空间 RBAC 完整说明
- 认证中间件源码
- 角色检查中间件源码
- 用户数据模型
【免费下载链接】WeKnoraOpen-source LLM knowledge platform: turn raw documents into a queryable RAG, an autonomous reasoning agent, and a self-maintaining Wiki.项目地址: https://gitcode.com/GitHub_Trending/we/WeKnora
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考