Lino 中安全的 multi-tenancy
Multi-tenancy 允许一个应用服务多个客户、公司或组织,同时保持数据和权限隔离。在 Lino 中,Tenant 被视为应用流程的一部分:该功能会生成 tenancy 基础,通过 ITenantContext 按 subdomain、host 或 slug 解析 Tenant,并通过 IUserContext 暴露由 JWT 认证的用户。
JWT 还携带用户被认证时对应的 TenantId。Lino 会将该 Tenant 与为请求解析出的 Tenant 进行比较,因此本页聚焦于该基础存在之后你需要决定的内容:实体建模、按组织划分的权限,以及正确使用生成的抽象。
添加 Tenant 支持
当系统是 SaaS,或不同客户的数据需要在同一个应用中共存且不能在上下文之间泄漏时,请使用 Tenant 功能。
lino feature tenant add
助手会生成 Tenants 注册、用户关联、当前 Tenant 解析、在应用上下文中的传播以及身份验证集成的基础。ITenantContext 表示通过 subdomain、host 或 slug 解析出的 Tenant,而 IUserContext 表示通过 JWT 认证的用户,其中包括该用户被认证时对应的 TenantId。
生成后,请审查哪些实体真正属于某个 Tenant,哪些是 global。这项审查是产品建模的一部分:multi-tenancy 不只是创建一张 Tenants 表,而是决定哪些规则、页面、权限、数据和操作依赖 Lino 验证过的当前 Tenant。
- Tenant-scoped: 订单、某个客户的产品、公司设置、CRM 客户,或任何属于特定组织的数据。
- Global: 平台公共目录、套餐、内部运维资源、管理设置,或在选择 Tenant 之前使用的数据。
- Hybrid: 带有按 Tenant 自定义的 global 数据,需要显式建模,以避免错误重复或规则泄漏。
在模块化单体中,tenancy 模块通常拥有原始 Tenant 实体。其他模块(如 catalog 或 CRM)不应直接访问该实体。当它们需要了解 Tenant 时,可以维护包含最少必要字段的 shadow entities,并由 Tenant 创建或更新等 integration events 填充。
Tenant context
启用 Tenant 功能后,Lino 会集中解析当前 Tenant,并通过 ITenantContext 暴露该值。此上下文表示从请求的 subdomain、host 或 slug 解析出的 Tenant;当找到 Tenant 时,还包括 TenantId 和 slug 等信息。
在认证流程中,IUserContext 从 JWT 读取用户。该 JWT 还包含用户被认证时对应的 TenantId。Lino 会将 IUserContext 中的 Tenant 与 ITenantContext 中的 Tenant 进行比较,避免业务规则信任不一致的上下文。
在生成的功能或后续添加的功能中,请始终优先使用这些抽象,而不是手动重新读取 claims,或在每个 handler 中接收任意 TenantId。这样可以让 commands、queries、页面和集成使用同一个 source of truth,并保留 Lino 已经实现的安全机制。
Background Jobs、事件消费者和集成也需要上下文。处理 tenant-scoped 产品、客户或用户的 job 必须显式接收正在处理的 Tenant,因为此时不存在 active HTTP request 可以自动推断 ITenantContext 和 IUserContext。
Tenant-scoped 实体
Tenant-scoped 实体携带 Tenant identifier,并参与 query filters。这样常规流程不需要手动记住在每个 query 中应用 TenantId;功能应使用验证过的上下文以及 Lino 生成的 filters。
- 在属于特定组织的 aggregates 中包含 Tenant。
- 配置 indexes 时考虑 Tenant 和经常查询的字段。
- 在 commands 和 queries 中使用
ITenantContext和IUserContext,而不是把 Tenant 当作孤立的权限来源接收。 - 在 imports 和 jobs 中,显式定义正在处理哪个 Tenant。
tenant-scoped 实体属于一个 Tenant。如果某个实体看起来属于多个 Tenants,请将其视为建模决策:它可能是 global,可能应该按 Tenant 存在一份副本,也可能缺少用于表示关系的中间实体。产品、客户或用户在不同模块中也可能具有不同含义;在这种情况下,带有少量字段的 shadow entity 可能比共享原始实体更正确。
还要区分引用和 ownership。catalog 中的产品可以引用 Tenant,但这并不意味着 catalog 控制 Tenant 的 lifecycle。tenancy 模块仍然拥有创建、激活、域名、branding 和隔离模式;catalog 只保留过滤和验证自身流程所需的数据。
- Indexes: 将
TenantId与用于搜索、排序和按 Tenant 唯一性的字段组合。 - Commands: 当实体是 tenant-scoped 时,在验证过的当前 Tenant 中应用操作。
- Queries: 将分页、filters 和计数器保持在 Lino 验证过的同一上下文中。
- Events: 包含足够上下文,使 consumers 能在不不当查询 producer 的情况下更新 projections。
数据隔离
隔离可以按列、schema 或独立数据库实现。选择取决于成本、数据量、合同要求和运维能力。
| 策略 | 优势 | 何时使用 |
|---|---|---|
| TenantId 列 | 更简单且成本更低 | 许多 SaaS 的良好初始选择,并结合 filters 和 Lino context。 |
| 每个 Tenant 一个 schema | 中等隔离 | 当需要更强分离但尚未达到每个 Tenant 一个数据库时很有用。 |
| 每个 Tenant 一个数据库 | 强隔离 | 当合同、数据量或运维能证明更高成本和自动化合理时适用。 |
对于大多数早期 SaaS,按 Tenant 使用列并结合 global filters 和集中式上下文,是务实的起点。Lino 会帮助建立这套基础;你的审查应确认 tenant-scoped aggregates 携带正确的 Tenant,并且新的 queries 继续使用由生成抽象验证过的当前 Tenant。
在模块化服务中,Tenant 隔离和模块隔离是不同关注点。Catalog 模块可以有带 TenantId 的产品表;CRM 模块可以有带 TenantId 的客户;而 Tenancy 模块仍然拥有原始 Tenants 注册。相同 identifier 的存在并不授权模块之间直接访问。
按 Tenant 的 roles 和权限
在 SaaS 中,用户可以在一个 Tenant 中是管理员,在另一个 Tenant 中是操作员。Lino 会生成处理 user-Tenant 关联的基础,功能权限必须在 framework 验证过的 active context 中评估。
也存在 Tenant 之外的管理权限。平台运维页面可能只能在 system context 中访问,而产品和客户等页面可能需要 active Tenant。这种分离可以明确某个操作何时是 global,何时依赖 ITenantContext 中解析出的 Tenant 以及 IUserContext 中认证的用户。
- 当权限因组织而异时,按 Tenant 建模 roles。
- 在页面、commands 和 queries 的授权检查中使用当前上下文。
- 保持 claims 紧凑,只在 token 中携带需要参与认证流程的内容。
- 当关键权限变化时,撤销或更新 tokens。
| 上下文 | 示例 | 处理方式 |
|---|---|---|
| System | 平台管理、套餐、Tenants 和 global 设置。 | 在特定 Tenant 的 scope 之外执行。 |
| Tenant | 该 Tenant 的产品、客户、订单、用户和 roles。 | 使用 Lino 验证过的当前 Tenant。 |
| Both | 一些同时存在于 system 和 Tenant 中的 identity 或管理功能。 | 在 use case 中声明正在使用哪个上下文。 |
JWT 和 Tenant 安全
当 Tenant 功能启用时,Lino 使用 JWT 作为认证用户上下文的来源。IUserContext 读取用户的 claims 和 token 中的 TenantId,但不会把 JWT 变成数据库副本。
- 保持 issuer、audience、签名和过期时间配置正确。
- 使用 Lino 验证过的 Tenant,而不是仅根据 header、route 或页面 payload 接受手动切换。
- 避免过大的 claims;膨胀的 tokens 会使运维和续期更困难。
- 当流程需要审计时,记录包含用户、Tenant 和操作的访问 logs。
在使用 subdomains 的应用中,例如 acme.dev.localhost 和 globex.dev.localhost,ITenantContext 表示为请求解析出的 Tenant。IUserContext 表示已认证用户以及 JWT 中存在的 Tenant。Lino 会比较这两个上下文,以避免为一个 Tenant 签发的 token 被用于另一个 Tenant 上下文。
授权也仍然是上下文相关的。例如,查看产品的权限只在授予该权限且用户被认证的 Tenant 内有效。创建新功能时,请使用已经可用的权限和上下文,而不是创建接收松散 TenantId 并忽略认证流程的捷径。
