多租户 + 权限:企业级 AI 平台的安全基石——万悟 RBAC 与租户隔离源码拆解
📌 本文是《从零吃透企业级 AI 平台:元景万悟源码学习手记》系列第一季·源码学习篇的第 8 篇(终篇)🎯 读完本文你将:① 理解多租户架构的三种隔离模型 ② 掌握 RBAC 权限设计与中间件鉴权实现 ③ 能在万悟中配置完整的组织架构与权限策略⏱️ 预计阅读时间:35 分钟 | 动手实践:60 分钟💻 前置要求:已完成前 7 篇,对万悟整体架构有全局认知
插一句后续计划:台风“白海豚”来了,在家里蹲着顺便把第一季最后一篇完结掉,后续计划出第一季番外篇,把一些正文主干没有讲透但在落地细节也需要注意的地方写一写,然后才会展开写第二季,讲实际的项目配套。
第一季的文章中代码块偏长,后续风格会把大代码块改成"关键 10-20 行 + 说明",完整代码收进 ## 附录。
后面可能1-3天发一篇,一周发2-4篇,看时间情况安排。
一、这篇文章要解决什么问题?
前 7 篇我们拆解了万悟的所有"能力模块":模型接入、RAG、GraphRAG、Agent、MCP、工作流。但有一个问题始终没正面回答:
当 50 个部门、500 个用户同时使用这个平台时,怎么保证 A 部门看不到 B 部门的知识库?怎么保证实习生不能删除 CEO 的智能体?怎么保证每个租户的 API 调用量不互相影响?
这就是多租户(Multi-Tenancy)+ 权限控制(RBAC)要解决的问题。
Demo 级项目:一个用户,所有权限,数据混在一起 → 能跑就行企业级平台:N 个租户 × M 个角色 × K 种资源 → 安全是生命线
💡 一句话总结:前 7 篇决定了平台"能做什么",这一篇决定了平台"能不能上线"。
万悟作为面向央企/国企的 AI 平台,安全合规是第一优先级。今天我们把它的权限体系完整拆开。
二、核心概念:用大白话讲清楚
2.1 多租户:一栋公寓楼
💡 类比:多租户就像公寓楼。
一栋楼(一套代码、一个集群)住很多户(租户) 每户有自己的钥匙(API Key / Token) 每户只能进自己的房间(数据隔离) 物业(平台管理员)能进所有房间,但住户之间互相进不去 关键问题:隔离到什么程度?
2.2 三种隔离模型
tenant_id 区分 | ||||
万悟采用混合模型:
默认:共享数据库 + tenant_id字段(轻量、易运维)高安全租户:可配置独立数据库(满足等保/密评要求)
💡 信创多数据库支持:万悟的数据库层设计了一个"可插拔"的抽象——不是绑死 MySQL。通过
WANWU_DB_NAME环境变量,可以在三种关系型数据库中切换:
数据库 端口 适用场景 MySQL 3306 默认引擎,中小规模 TiDB 4000 国产分布式数据库,大规模/高可用 OceanBase 2881 蚂蚁自研,金融级强一致 所有微服务共 9 个独立 schema(iam/model/app/rag/assistant/knowledge/mcp/operate/opencoze),通过 GORM 统一访问,切换数据库只需改环境变量+连接串。
2.3 RBAC:角色-权限模型
💡 类比:RBAC 就像公司的门禁系统。
你不是直接拿钥匙开每扇门(用户 → 权限) 而是你的工牌(角色) 决定了你能进哪些门 HR 给你分配工牌(用户 → 角色),工牌上刻着权限列表(角色 → 权限) 你升职了?换工牌就行,不用重新配每扇门
用户(User) ──N:M──→ 角色(Role) ──1:N──→ 权限(Permission)│ │ │张三 ”部门管理员” ”kb:read”, ”kb:write”,李四 ”普通成员” ”agent:read”王五 ”平台管理员” ”ALL”
2.4 万悟的权限模型
2.5 API Key 与认证
三、万悟是怎么实现的?(源码篇)
3.1 定位源码
⚠️ 诚实说明:以下 3.2–3.7 的代码是教学重建版——用最小代码讲清认证、鉴权、租户隔离的核心逻辑。万悟真实的权限体系基于 Casbin(RBAC 策略引擎)+ JWT + RSA(
pkg/rsa-util/),代码分布在以下路径:
# 万悟真实源码结构(Go 单体仓库)internal/iam-service/# 身份与租户服务(gRPC 微服务)├── client/# IAM 客户端├── config/# 认证配置└── server/grpc/# gRPC 服务入口(用户/角色/权限/租户 CRUD)internal/bff-service/# BFF 网关(HTTP 入口,中间件挂载点)# 认证 → 租户注入 → 限流 → 鉴权 中间件链在此pkg/rsa-util/# RSA 加解密工具(密钥管理)# 万悟使用 Casbin 作为 RBAC 策略引擎,非自研权限框架
3.2 认证中间件
// 教学重建版(对应真实路径:internal/bff-service/ 中间件 + internal/iam-service/ JWT 验证)// AuthMiddleware 是全局认证中间件// 每个请求都必须经过这里funcAuthMiddleware(jwtService *JWTService) gin.HandlerFunc {return func(c *gin.Context) {// Step 1: 提取 Token// 支持两种来源:Authorization Header 或 API Key Headertoken := c.GetHeader(”Authorization”)apiKey := c.GetHeader(”X-API-Key”)var claims *Claimsvar err errorif token != ”” {// JWT 认证(前端用户)token = strings.TrimPrefix(token, ”Bearer ”)claims, err = jwtService.Verify(token)if err != nil {c.AbortWithStatusJSON(401, gin.H{”error”: ”invalid or expired token”})return}} else if apiKey != ”” {// API Key 认证(服务间调用)claims, err = jwtService.VerifyAPIKey(apiKey)if err != nil {c.AbortWithStatusJSON(401, gin.H{”error”: ”invalid api key”})return}} else {c.AbortWithStatusJSON(401, gin.H{”error”: ”authentication required”})return}// Step 2: 将用户信息注入上下文// 后续所有 handler 都可以从 context 中获取当前用户c.Set(”user_id”, claims.UserID)c.Set(”tenant_id”, claims.TenantID) // ← 关键!租户 IDc.Set(”roles”, claims.Roles)c.Set(”space_id”, claims.SpaceID)c.Next()}}
📌 关键洞察:
tenant_id在认证阶段就被注入上下文。后续所有数据库查询都会自动带上WHERE tenant_id = ?,实现行级数据隔离。这不是可选的——是架构强制的。
3.3 租户上下文与数据隔离
// 教学重建版(对应真实路径:internal/bff-service/ 租户中间件)// TenantMiddleware 确保所有数据操作都在租户边界内funcTenantMiddleware() gin.HandlerFunc {return func(c *gin.Context) {tenantID := c.GetString(”tenant_id”)if tenantID == ”” {c.AbortWithStatusJSON(403, gin.H{”error”: ”tenant context missing”})return}// 将 tenant_id 注入到数据库查询上下文// 所有 Repository 层的方法都会从 context 中读取这个值ctx := context.WithValue(c.Request.Context(), TenantKey, tenantID)c.Request = c.Request.WithContext(ctx)c.Next()}}// ===== Repository 层示例 =====// 教学重建版(对应真实路径:internal/knowledge-service/ 数据查询层)// ListByTenant 查询知识库列表// 注意:tenant_id 从 context 中获取,而非参数传入// 这保证了”不可能忘记加租户过滤”func(r *KBRepository) List(ctx context.Context) ([]KnowledgeBase, error) {tenantID := ctx.Value(TenantKey).(string)var kbs []KnowledgeBase// GORM 查询自动带租户过滤err := r.db.WithContext(ctx).Where(”tenant_id = ?”, tenantID). // ← 强制隔离Find(&kbs).Errorreturn kbs, err}
💡 设计哲学:万悟的租户隔离是架构级强制的,而非"开发者自觉"。
tenant_id通过 context 传递,Repository 层统一从 context 读取。即使某个开发者写新接口时"忘了"加过滤,框架层的TenantMiddleware也会拦截。
3.4 RBAC 鉴权中间件
// 教学重建版(对应真实路径:Casbin 策略引擎 + internal/bff-service/ 鉴权中间件)// RequirePermission 是一个路由级中间件// 用法:router.GET(”/kb”, RequirePermission(”kb:read”), handler.ListKB)funcRequirePermission(requiredPerm string) gin.HandlerFunc {return func(c *gin.Context) {userID := c.GetString(”user_id”)tenantID := c.GetString(”tenant_id”)roles := c.GetStringSlice(”roles”)// Step 1: 超级管理员直接放行if contains(roles, ”super_admin”) {c.Next()return}// Step 2: 查询用户的所有权限(带缓存)// 缓存策略:Redis,TTL 5 分钟// Key: ”perms:{tenant_id}:{user_id}”perms, err := getPermissionsWithCache(c, tenantID, userID)if err != nil {c.AbortWithStatusJSON(500, gin.H{”error”: ”permission check failed”})return}// Step 3: 检查是否拥有所需权限// 支持通配符:拥有 ”kb:*” 等同于拥有 ”kb:read”, ”kb:write”, ”kb:delete”if !hasPermission(perms, requiredPerm) {c.AbortWithStatusJSON(403, gin.H{”error”: fmt.Sprintf(”permission denied: requires '%s'”, requiredPerm),})return}c.Next()}}// hasPermission 检查权限(支持通配符)funchasPermission(userPerms []string, required string) bool {for _, p := range userPerms {if p == required || p == ”*” {return true}// 通配符匹配:”kb:*” 匹配 ”kb:read”if strings.HasSuffix(p, ”:*”) {prefix := strings.TrimSuffix(p, ”:*”)if strings.HasPrefix(required, prefix+”:”) {return true}}}return false}
3.5 权限定义与角色模型
// 教学重建版(对应真实路径:internal/iam-service/ 权限模型 + Casbin 策略)// 万悟的权限采用 ”资源:操作” 格式var AllPermissions = []string{// 知识库”kb:read”, ”kb:write”, ”kb:delete”, ”kb:admin”,// 智能体”agent:read”, ”agent:write”, ”agent:delete”, ”agent:admin”,// 工作流”workflow:read”, ”workflow:write”, ”workflow:execute”, ”workflow:admin”,// 模型”model:read”, ”model:write”, ”model:admin”,// MCP 工具”mcp:read”, ”mcp:write”, ”mcp:admin”,// 用户管理”user:read”, ”user:write”, ”user:admin”,// 租户管理”tenant:admin”,// 审计日志”audit:read”,}// 预定义角色var DefaultRoles = map[string][]string{”viewer”: {”kb:read”, ”agent:read”, ”workflow:read”, ”model:read”,},”member”: {”kb:read”, ”kb:write”,”agent:read”, ”agent:write”,”workflow:read”, ”workflow:execute”,”model:read”,”mcp:read”,},”admin”: {”kb:*”, ”agent:*”, ”workflow:*”, ”model:*”, ”mcp:*”,”user:read”, ”user:write”,”audit:read”,},”super_admin”: {”*”},}
3.6 租户级限流
// 教学重建版(对应真实路径:internal/bff-service/ 限流中间件)// TenantRateLimiter 基于租户的 API 限流// 防止某个租户的大量请求影响其他租户funcTenantRateLimiter(redis *redis.Client) gin.HandlerFunc {return func(c *gin.Context) {tenantID := c.GetString(”tenant_id”)// 从配置中获取该租户的限额// 不同套餐:免费版 100 RPM,企业版 10000 RPMlimit := getTenantRateLimit(tenantID)// 滑动窗口限流(Redis ZSET 实现)key := fmt.Sprintf(”ratelimit:%s:%s”, tenantID, time.Now().Format(”200601021504”))now := time.Now().UnixNano()pipe := redis.Pipeline()// 移除窗口外的记录pipe.ZRemRangeByScore(c, key, ”0”, fmt.Sprintf(”%d”, now-int64(limit.Window)))// 统计当前窗口内的请求数countCmd := pipe.ZCard(c, key)// 添加当前请求pipe.ZAdd(c, key, &redis.Z{Score: float64(now), Member: now})pipe.Expire(c, key, limit.Window*time.Second)pipe.Exec(c)count := countCmd.Val()if count >= limit.MaxRequests {c.AbortWithStatusJSON(429, gin.H{”error”: ”rate limit exceeded”,”retry_after”: limit.Window,})return}// 将剩余配额写入响应头(方便客户端感知)c.Header(”X-RateLimit-Remaining”, fmt.Sprintf(”%d”, limit.MaxRequests-count))c.Next()}}
📌 企业级细节:限流的 Key 包含
tenant_id,意味着每个租户有独立的配额。A 租户把配额用完了,不影响 B 租户。这是多租户平台的基本要求。
3.7 路由挂载(全景图)
// 教学重建版(对应真实路径:internal/bff-service/ 路由注册)func SetupRoutes(r *gin.Engine) {// 公开接口(无需认证)public := r.Group(”/api/v1”){public.POST(”/auth/login”, authHandler.Login)public.POST(”/auth/register”, authHandler.Register)public.GET(”/health”, healthHandler.Check)}// 需认证的接口authorized := r.Group(”/api/v1”)authorized.Use(AuthMiddleware(jwtService), // 1. 认证:你是谁?TenantMiddleware(), // 2. 租户:你属于哪个租户?TenantRateLimiter(redisClient), // 3. 限流:你的配额够吗?){// 知识库(需要 kb:read 权限)kb := authorized.Group(”/knowledge-bases”)kb.Use(RequirePermission(”kb:read”)){kb.GET(””, kbHandler.List)kb.GET(”/:id”, kbHandler.Get)// 写操作需要更高权限kb.POST(””, RequirePermission(”kb:write”), kbHandler.Create)kb.DELETE(”/:id”, RequirePermission(”kb:delete”), kbHandler.Delete)}// 智能体agent := authorized.Group(”/agents”)agent.Use(RequirePermission(”agent:read”)){agent.GET(””, agentHandler.List)agent.POST(””, RequirePermission(”agent:write”), agentHandler.Create)agent.POST(”/:id/chat”, agentHandler.Chat) // 对话只需 read}// 管理接口(需要 admin 权限)admin := authorized.Group(”/admin”)admin.Use(RequirePermission(”user:admin”)){admin.GET(”/users”, adminHandler.ListUsers)admin.POST(”/roles”, adminHandler.CreateRole)admin.GET(”/audit-logs”, RequirePermission(”audit:read”), adminHandler.AuditLogs)}}}
💡 中间件执行顺序:
认证 → 租户注入 → 限流 → 权限检查。这个顺序不能乱——没认证就不知道是谁,不知道是谁就不知道属于哪个租户,不知道租户就没法限流。
四、动手跑通(实践篇)
4.1 配置组织架构
在万悟管理后台:
「租户管理」→ 创建租户: - 名称: 华东分公司- 套餐:企业版(10000 RPM) - 数据隔离:共享数据库「用户管理」→ 创建用户:
「空间管理」→ 创建空间: - AI 研发部(李工、王实习) -市场部(独立空间,数据互不可见)
4.2 验证权限隔离
kb:read 权限的 API Key |
4.3 创建 API Key
# 通过管理 API 创建受限 API Keycurl -X POST https://wanwu.example.com/api/v1/admin/api-keys \-H ”Authorization: Bearer ” \-H ”Content-Type: application/json” \-d '{”name”: ”报表服务专用”,”permissions”: [”kb:read”, ”workflow:execute”],”expires_at”: ”2027-01-01T00:00:00Z”,”rate_limit”: 1000}'# 响应{”api_key”: ”wk_live_a1b2c3d4e5f6...”,”name”: ”报表服务专用”,”permissions”: [”kb:read”, ”workflow:execute”],”created_at”: ”2026-08-01T11:00:00Z”}
⚠️ 安全提醒:API Key 只在创建时返回一次明文,之后只存哈希。务必当场保存。
4.4 审计日志
万悟记录所有敏感操作:
📌 合规价值:对于央企/国企,审计日志是等保测评的硬性要求。万悟的审计日志不可篡改(写入后只追加),保留期可配置(默认 180 天)。
五、自己造一个 Mini 版(Deep Dive)
🎯 目标:用 Python 实现一个 90 行的多租户 RBAC 系统
# mini_rbac.py# 依赖:pip install flask pyjwtimport jwt, time, functoolsfrom flask import Flask, request, jsonify, gapp = Flask(__name__)SECRET = ”your-secret-key”# ===== 1. 模拟数据 =====USERS = {”u001”: {”name”: ”张总”, ”tenant”: ”T1”, ”roles”: [”admin”]},”u002”: {”name”: ”李工”, ”tenant”: ”T1”, ”roles”: [”member”]},”u003”: {”name”: ”王实习”, ”tenant”: ”T1”, ”roles”: [”viewer”]},”u004”: {”name”: ”赵总”, ”tenant”: ”T2”, ”roles”: [”admin”]},}ROLE_PERMS = {”admin”: [”kb:*”, ”agent:*”, ”user:read”],”member”: [”kb:read”, ”kb:write”, ”agent:read”],”viewer”: [”kb:read”, ”agent:read”],}# 模拟知识库(含 tenant_id)KNOWLEDGE_BASES = [{”id”: ”KB-01”, ”name”: ”研发文档”, ”tenant”: ”T1”},{”id”: ”KB-02”, ”name”: ”市场资料”, ”tenant”: ”T1”},{”id”: ”KB-03”, ”name”: ”财务数据”, ”tenant”: ”T2”},]# ===== 2. 认证中间件 =====def auth_required(f):@functools.wraps(f)def wrapper(*args, **kwargs):token = request.headers.get(”Authorization”, ””).replace(”Bearer ”, ””)try:payload = jwt.decode(token, SECRET, algorithms=[”HS256”])except jwt.ExpiredSignatureError:return jsonify({”error”: ”token expired”}), 401except jwt.InvalidTokenError:return jsonify({”error”: ”invalid token”}), 401g.user_id = payload[”user_id”]g.tenant_id = payload[”tenant_id”]g.roles = payload[”roles”]return f(*args, **kwargs)return wrapper# ===== 3. RBAC 鉴权 =====def require_permission(perm):def decorator(f):@functools.wraps(f)def wrapper(*args, **kwargs):# 收集用户所有权限user_perms = set()for role in g.roles:user_perms.update(ROLE_PERMS.get(role, []))# 检查权限(支持通配符)if not _has_perm(user_perms, perm):return jsonify({”error”: f”forbidden: requires '{perm}'”}), 403return f(*args, **kwargs)return wrapperreturn decoratordef _has_perm(user_perms, required):for p in user_perms:if p == required or p == ”*”:return Trueif p.endswith(”:*”) and required.startswith(p[:-1]):return Truereturn False# ===== 4. 业务接口 =====@app.route(”/login”, methods=[”POST”])def login():user_id = request.json[”user_id”]user = USERS.get(user_id)if not user:return jsonify({”error”: ”user not found”}), 404token = jwt.encode({”user_id”: user_id,”tenant_id”: user[”tenant”],”roles”: user[”roles”],”exp”: time.time() + 7200,}, SECRET, algorithm=”HS256”)return jsonify({”token”: token, ”user”: user[”name”]})@app.route(”/knowledge-bases”)@auth_required@require_permission(”kb:read”)def list_kb():# 租户隔离:只返回当前租户的数据result = [kb for kb in KNOWLEDGE_BASES if kb[”tenant”] == g.tenant_id]return jsonify({”data”: result, ”tenant”: g.tenant_id})@app.route(”/knowledge-bases”, methods=[”POST”])@auth_required@require_permission(”kb:write”)def create_kb():name = request.json[”name”]new_kb = {”id”: f”KB-{len(KNOWLEDGE_BASES)+1:02d}”, ”name”: name, ”tenant”: g.tenant_id}KNOWLEDGE_BASES.append(new_kb)return jsonify({”created”: new_kb}), 201@app.route(”/knowledge-bases/”, methods=[”DELETE”])@auth_required@require_permission(”kb:delete”)def delete_kb(kb_id):# 双重校验:权限 + 租户归属kb = next((k for k in KNOWLEDGE_BASES if k[”id”] == kb_id), None)if not kb or kb[”tenant”] != g.tenant_id:return jsonify({”error”: ”not found”}), 404KNOWLEDGE_BASES.remove(kb)return jsonify({”deleted”: kb_id})# ===== 5. 测试 =====if __name__ == ”__main__”:app.run(debug=True, port=5000)# 测试命令:# 1. 登录获取 Token# curl -X POST localhost:5000/login -H ”Content-Type: application/json” -d '{”user_id”:”u003”}'## 2. 用 viewer 的 Token 查知识库(成功,有 kb:read)# curl localhost:5000/knowledge-bases -H ”Authorization: Bearer ”# → 只返回 T1 的 KB-01, KB-02(看不到 T2 的 KB-03)## 3. 用 viewer 的 Token 删知识库(403,没有 kb:delete)# curl -X DELETE localhost:5000/knowledge-bases/KB-01 -H ”Authorization: Bearer ”# → {”error”: ”forbidden: requires 'kb:delete'”}## 4. 用 T2 用户的 Token 访问 T1 的知识库(404,租户隔离)# → 即使知道 KB-01 的 ID,也查不到
🎉 这 90 行就是万悟权限体系的最小可行实现:JWT 认证 → 租户注入 → RBAC 鉴权 → 数据过滤。万悟在此基础上加了:Redis 权限缓存、API Key 管理、OAuth 2.0、mTLS、审计日志、动态角色、空间级隔离、等保合规……但核心逻辑就是你看到的这三个装饰器:
@auth_required→@require_permission→WHERE tenant_id = ?。
六、系列总结 & 延伸阅读
本文要点回顾
✅ 多租户隔离三种模型:行级(tenant_id)→ Schema 级 → 实例级,万悟默认行级 + 可选实例级 ✅ RBAC = 用户 → 角色 → 权限,权限格式 资源:操作,支持通配符✅ 中间件链:认证 → 租户注入 → 限流 → 鉴权,顺序不可乱 ✅ 租户隔离是架构强制的(context 传递),非开发者自觉 ✅ 审计日志不可篡改,满足等保/密评合规要求
课后作业
在万悟中创建 2 个租户、3 个角色、5 个用户,验证隔离和权限 创建受限 API Key,用 curl 测试权限边界 查看审计日志,确认所有操作被记录 跑通 Mini RBAC,理解认证→鉴权→隔离的完整链路 思考:如果要支持"临时授权"(如:给外部审计员 7 天只读权限),架构该怎么扩展?
🎓 系列回顾:8 篇文章,一个完整的企业级 AI 平台
┌─────────────────────────────────────────────────────────────┐│ 万悟平台架构全景 ││ ││ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ││ │ 模型层 │ │ 知识层 │ │ 智能层 │ │ 编排层 │ ││ │(第2篇) │ │(第3,4篇)│ │(第5,6篇)│ │(第7篇) │ ││ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ ││ └─────────────┴─────────────┴─────────────┘ ││ │ ││ ┌──────▼──────┐ ││ │ 安全基座 │ ││ │ (第8篇) │ ││ │ 认证·鉴权 │ ││ │ 隔离·限流 │ ││ │ 审计·合规 │ ││ └─────────────┘ │└─────────────────────────────────────────────────────────────┘
下一步学什么?
参考资源
元景万悟 GitHub:https://github.com/UnicomAI/wanwu Casbin(万悟使用的 RBAC 策略引擎):https://casbin.org/ RBAC 论文:Role-Based Access Controls (Sandhu et al., 1996) JWT 规范:RFC 7519 https://datatracker.ietf.org/doc/html/rfc7519 等保 2.0:GB/T 22239-2019 Gin 中间件文档:https://gin-gonic.com/docs/examples/using-middleware/
📮 这是本系列的最后一篇。 感谢你从第 1 篇一路跟到这里 🎉如果这个系列对你有帮助,请 点赞 + 收藏 + 关注,这是对创作者最大的鼓励🔔 后续可能推出「番外篇」:性能调优、生产部署、故障排查等实战主题
我们下一个系列见 👋
📱 关注公众号,追更不迷路
本系列文章首发于微信公众号「农夫三拳有点癫」,每周更新源码拆解与架构实战。
在微信扫描下方二维码即可关注:
夜雨聆风