首页
/
行业洞察
/
正文
INDUSTRY INSIGHT · 深度
GoWind ShortPlay 拆解:Ent privacy/viewer 多租户隔离——从中间件到 SQL 的零信任链
📅 2026/10/28 10:13:17
✍️ 爱科研究院
👁 阅读 3,247
GoWind ShortPlay 拆解Ent privacy/viewer 多租户隔离——从中间件到 SQL 的零信任链本文回答一个问题一条 SQL 都没手写的情况下租户 A 的用户为什么绝不可能查到租户 B 的视频——连漏一次的机会都没有多租户隔离最常见的死法是应用层 WHERE每个仓储方法都记得写tenant_id ?直到某天一个新同事写的接口忘了——于是租户 B 拉到了租户 A 的数据没有任何报错。GoWind ShortPlay 不信任任何人的记性它把隔离下沉到 ORM 层Ent 的 privacy 机制查询根本到不了 SQL 就被拦下补条件或直接拒绝。全仓 51 张业务表的 schema 里没有一行手写的租户过滤代码隔离却无处不在。这条链路涉及四个角色身份注入Kratos 中间件→ Viewer信任边界→ Privacy Policy求值规则→ 落库兜底写路径。逐个拆。一、全链路隔离发生在哪一环HTTP 请求 │ ▼ Kratos 中间件链 (BFF) │ auth.Server: JWT 校验(回调 core) → 组装 OperatorMetadata │ ※ 顺序敏感:鉴权必须在 ent.Server 之前 ▼ ent.Server 中间件 (pkg/middleware/ent) │ 有 OperatorMetadata → UserViewer(uid, tid, 数据域) │ 无(游客白名单) → TenantResolver 按请求 Host 解析租户 │ 成功 → AnonymousTenantViewer(只读匿名) │ 失败 → NoopContext(直接拒绝) ▼ viewer.WithContext(ctx) ──▶ service → repo → ent Query/Update │ ▼ Ent runtime privacy 求值 │ schema mixin.TenantID.Policy() → rule.TenantPrivacy.EvalQuery │ viewer 缺失 → Deny(报错,不是跳过) │ tid0(平台/系统) → 放行全量 │ 否则 → 注入 WHERE tenant_id :tid ▼ SQL 执行(带租户条件)隔离不是一层检查是一条链每一环只做自己那一截但任何一环缺位下一环会 fail-closed。二、三种 Viewer信任边界显式化Viewer 是整条链的信任锚点pkg/entgo/viewer/全部实现 go-crud 的viewer.Context接口——UserID / TenantID / DataScope /HasPermission()/IsSystemContext()等Viewer语义关键判定UserViewer已认证的租户/平台用户uid tid 数据域ALL/UNIT/SELF 三档 DataScope 映射AnonymousTenantViewer游客本仓新增基座没有tid0、UserID()0、HasPermission()false、只读SystemViewer后台任务/系统上下文全零 ID HasPermission()true 平台/系统上下文NoopContext解析失败什么都不满足查询直接被 Deny值得展开的是AnonymousTenantViewer为什么要存在。直觉的做法是游客嘛给个 SystemViewer 放行算了——这正是跨租户泄漏的经典入口。代码注释把这个 Viewer 与 SystemViewer 的三个关键差异写得很清楚TenantID()返回真实租户值——查询才能按租户过滤SystemViewer 全零租户过滤失效IsSystemContext()false——TenantPrivacy 不再 bypass照常按租户收口堵住 SystemViewer 跨租户可见的洞无写权限——游客天然只读。游客不是没有身份而是有租户、没用户、没权限的三元组——身份模型对了隔离才对。三种 Viewer 实现同一个接口go-crud 的viewer.Context这个接口本身值得看一眼——它就是身份在这个系统里的完整定义UserID / TenantID / OrgUnitID 我是谁、属于哪个租户与组织 Permissions / Roles / DataScope 我能做什么、数据可见范围(ALL/UNIT/SELF 三档) HasPermission(code) 权限码判定 IsPlatformContext / IsTenantContext 平台面还是租户面(tid0 分界) IsSystemContext 是否系统上下文(决定 TenantPrivacy 是否 bypass) ShouldAudit / TraceID 审计开关与追踪标识任何代码想判断当前查询以什么身份在跑问的都是这一个接口——没有第二套身份表示也就没有两套需要保持一致的判定逻辑。UserViewer的 DataScope 三档还进一步决定了行级可见范围ALL看全租户、UNIT看本部门含子部门、SELF只看自己——租户隔离与组织内数据权限是同一个 Viewer 上的两档分辨率。三、fail-closed游客租户怎么解析失败怎么办游客请求Feed / 详情 / 搜索这些白名单接口没有 JWT租户从哪来app BFF 注入了一个TenantResolver按请求 Host反查租户ResolveTenantIDByDomain经 gRPC 转发 core——每个租户一个接入域名的经典 SaaS 形态。关键在解析失败的分支pkg/middleware/ent/ent.goHost 为空 / 域名查不到租户 / 解析出 tid0 → viewer.NewNoopContext() ← 直接拒绝 → 绝不回退 SystemViewer ← 注释原话:避免跨租户泄漏这就是fail-closed身份解析不出来宁可拒绝服务不给默认放行。与之配套的还有两个精细件解析成功的隔离依然完整AnonymousTenantViewer 走的 TenantPrivacy 与登录用户同一条路径Guest 看到的 Feed 天然只含本租户视频流式 RPC 单独补位SSE 是流式接口unary 中间件链不覆盖——InjectViewerForStream(ctx)为流单独注入不留流式接口无租户的旁门。四、Policy一处 mixin51 张表生效业务表 schema 里找不到任何租户策略代码因为策略长在 mixin 上基座 go-crud 的TenantID[uint32]本仓 51 个 schema 挂载func(TenantID[IDT])Fields()[]ent.Field{return[]ent.Field{field.Uint32(tenant_id).Immutable().Default(0).Nillable().Optional(),}}func(TenantID[IDT])Policy()ent.Policy{returnrule.TenantPrivacy[IDT]{}}TenantPrivacy.EvalQuery的三分支是整个隔离体系的核心判定// 伪代码,语义忠实于原实现func(p TenantPrivacy[T])EvalQuery(ctx context.Context,q ent.Query)error{vc:viewer.FromContext(ctx)ifvcnil{returnprivacy.Deny// ① 无 viewer:报错,不是 Skip}ifvc.IsPlatformContext()||vc.IsSystemContext(){returnnil// ② 平台/系统面:跳过,放行全量}injectTenantWhere(q,vc.TenantID())// ③ 租户面:注入 WHERE tenant_id :tidreturnnil}两个设计决定值得单独说Deny 而非 Skip。查询没带 viewer比如某条代码路径绕过了中间件时策略选择报错而不是放行。多数手写多租户方案在上下文缺失时的默认行为是放行——那是把最危险的场景当成了最安全的。fail-closed 在这里是最后一道闸。反射注入 WHERE。injectTenantWhere用反射在任意生成的 Query 上构造变参Where(...)兼容不同实体生成代码的签名差异——51 张表共用一段策略代码新表挂上 mixin 即被隔离零增量成本。这是schema 即代码路线的真正回报策略跟着 schema 走而不是跟着仓储代码走。五、写路径比读路径更危险的地方读路径漏了是越权可见写路径漏了是数据污染——污染会顺着读路径扩散。TenantPrivacy 的EvalMutation对写有三层处理// 伪代码,语义忠实于原实现// 仅对 OpCreate 生效(Update/Delete 由读路径的租户 WHERE 限定范围,改不到别家行)if视图是平台/系统上下文{尊重显式 SetTenantID// 管理面代客建数据的合法场景}else{强制SetTenantID(viewer.TenantID())// 请求体里的 tenant_id 一律覆盖// 优先强类型 SetTenantID(T) 接口,兜底反射 SetField,都不可用 → error}还有一个只有踩过才知道的坑系统上下文tid0建业务行怎么办举报建单、异步抽帧、种子数据这些都运行在 SystemViewer 下直接落库会写出tenant_id0的行——租户用户永远查不到自己的数据注释管这叫孤儿数据。兜底在data/business_tenant.go业务行写入统一走businessTenantID()系统上下文缺省归入缺省业务租户进程内缓存解析shortplay租户 ID。看得到点不了这类事故多数根源就是系统路径写出了租户过滤永远命不中的行。六、中间件顺序即安全语义rest_server.go里那段注释值得整段抄录它是这条链上最容易断的一环鉴权必须在 ent.Server() 之前执行auth.Server 对非白名单请求注入 OperatorMetadata随后 ent.Server() 才能据此构建带租户作用域的 UserViewer。若顺序颠倒ent.Server() 总以 mdnil 兜底为 SystemViewer导致租户隔离失效。顺序颠倒了不会有任何报错——每个请求照常 200只是所有人突然都变成了 SystemViewer。这类配置即安全语义的约束唯一的归宿就是写在代码旁边以及像本文一样再写一遍。配套的还有 UBA 上报的UbaIdentityInjector上报接口在游客白名单内被短路了鉴权但带有效 token 时尽力解析身份写回请求头——否则观看历史全部归因到 uid0。白名单跳过的是鉴权不是身份。七、系统上下文何时允许越过隔离隔离体系必须有合法的越权通道否则异步任务没法干活。约定很克制异步任务FFmpeg 抽帧、通知投递显式构造系统上下文appViewer.NewSystemViewerContext(ctx)——越权是显式声明的代码里一眼可见管理面admin BFF / core 内部不注入 TenantResolver无操作者时兜底 SystemViewer——内部管理面跨租户可见是特性与 C 端 app BFF 的 fail-closed 形成刻意的对照收件箱这类用户自己的数据另有viewerUserIDFromContext强制按调用者过滤——租户隔离保证别的公司看不到行级归属进一步保证本公司别人也看不到我的收件箱。两级过滤是两个独立的正交维度混在一层里做迟早漏一边。三种上下文、三种默认值没有一种默认是放行所有租户——除了被显式声明为管理面的那一个。八、方法论把隔离做成结构而不是约定回头看这条链可以提炼成四条可迁移的原则隔离下沉到 ORM 层——策略长在 schema mixin 上新表零成本被隔离应用层 WHERE 靠记性privacy 靠结构每一环 fail-closed——viewer 缺失 Deny 而非放行、租户解析失败拒绝而非兜底 SystemViewer、写路径 tenant_id 强制覆盖游客是身份模型不是缺失的身份——有租户、没用户、没权限三元组各自显式显式越权 归属标记——系统上下文要在代码里喊出来NewSystemViewerContext系统路径写数据要有租户归属兜底孤儿数据是隔离体系最常见的静默事故。以及一条工程提醒链式安全的每一段依赖都有顺序把顺序敏感性写进中间件装配处的注释比写进 Wiki 有用一百倍——后者没人看前者就在 bug 旁边。九、验证想亲手验证隔离两条路# 1. 游客 fail-closed:用未注册域名打游客接口curlhttp://127.0.0.1:7700/app/v1/feed-HHost: unknown.example.com# → 空结果或拒绝;绝不会看到缺省租户以外的数据# 2. 写路径防越权:登录用户提交携带他人 tenant_id 的创建请求# → 落库行的 tenant_id 被强制覆盖为 viewer 所属租户(可查库对照)或者直接读代码策略三分支在基座库的rule/tenant.go注入链在pkg/middleware/ent/ent.go游客 Viewer 在pkg/entgo/viewer/anonymous_viewer.go——每个文件的头注释都值得读。十、结语GoWind ShortPlay 的多租户隔离没有一个开关或一个中间件而是一条从 HTTP 中间件到 SQL 条件的完整链路身份注入 → Viewer 信任边界 → Policy 求值 → 写路径兜底四个环节共享同一个哲学——不确定时拒绝而不是放行。这也是把它从 go-wind-cms 基座继承下来时唯一新增的身份原语是游客 Viewer的原因架构已经闭环缺的只是把游客当成一种正当身份。仓库GitHubgithub.com/tx7do/go-wind-shortplayGiteegitee.com/tx7do/go-wind-shortplay
📌 标签:
工业官网
设计趋势
AI 建站
SEO
获取完整报告 →
RELATED ARTICLES
推荐阅读
2026/10/28 10:13:17
NoSQL与关系数据库操作对比:MySQL vs MongoDB差异全解析
2026/10/28 10:13:17
AD芯片NVP6124B/NVP6158C、TP9930/9932/TP2856硬件设计注意事项
2026/10/28 10:13:17
基于SpringBoot的高校失物招领管理系统【源码+文档】
2026/10/28 11:29:34
PPT水印全流程:母版设置、参数计算与打印导出避坑
2026/10/28 11:29:34
使用Blockcell实现股票实时监控与量化交易策略
2026/10/28 11:29:34
python 淘宝爬虫小实例
2026/10/28 11:29:34
基于YOLOv8-seg和HGNetV2的鼠鱼智能分类系统
2026/10/28 11:29:34
Zustand切片模式:React状态管理的高效实践
2026/10/28 11:24:32
MySQL触发器与审计功能实现用户操作监控
2026/10/28 0:00:12
Stable Diffusion文生图全解析:从CLIP到UNet的扩散模型原理与实战
2026/10/28 0:00:12
从 Notebook 到 Python 项目:如何重构量化行情获取代码?
2026/10/28 0:00:12
2026版人形机器人标准:具身智能与模块化设计解析
2026/10/27 11:41:45
全国计算机二级C语言真题PDF高效刷题指南
2026/10/27 9:24:40
requests+BeautifulSoup:爬虫抓取h1标题的入门实战指南
2026/10/27 12:03:45
递推最小二乘参数辨识MATLAB编程:二阶系统与渐消记忆实现
2026/10/25 22:15:16
我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
2026/10/26 11:29:48
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026/10/25 12:42:56
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)