系统设计 / 路由设计 · 稳定入口与可变目标 / 多租户 · 用 host 路由到租户 待审核 15 / 23

多租户 · 用 host 路由到租户

前面的路由都看 path,SaaS 多租户换了个维度看 host。同一套代码、同一批服务器,给每个客户一个专属子域(acme.app.comglobex.app.com),路由层从 Host 请求头里把租户标识解析出来,再决定加载哪个租户的数据与配置。入口(一套程序)稳定,目标(哪个租户的数据)由 host 决定,这是「稳定入口 ↔ 可变目标」落在租户隔离这根轴上。

它靠一条通配子域 *.app.com 撑起:DNS 上一条泛解析、TLS 用一张通配证书,新客户开通即得子域、零运维。但通配会吞掉一切,于是要处理三类例外——保留子域(wwwapi 不能被当成租户)、站点根域(app.com 是官网),以及客户想用自己的域名时的 CNAME 映射。

图 0-1 · 一个 host 被解析成哪个租户。可输入 api.app.com 看它命中保留规则而非被当成租户,或输入 api.acme.app.com 看多级子域被拒。

警示 · 保留子域必须在注册流程里拉黑。通配 *.app.com 会把任意子域都当成租户,包括 apiwwwadminmail。若不在开通流程里拦下,某个用户注册了名叫 api 的租户,就会劫持 api.app.com 这个 API 网关子域。所以多租户系统都维护一张保留子域黑名单,优先级排在通配规则之前。

1 · 自定义域与 CNAME 映射

大客户往往不愿意用 acme.app.com,想要 dash.acme-corp.com 这样的自有域名。做法是客户在自己 DNS 把该域 CNAME 指到服务方的 cname.app.com,服务方记一条「自定义域到租户」的映射,并为该域签发证书(通常走 ACME 自动化)。请求到达时 Host 头是 dash.acme-corp.com,它不在 *.app.com 里,于是先查自定义域映射表,查到就解析到对应租户。这张表的优先级要排在通配之前。

路由层从 Host 头解析租户,四条规则按优先级自上而下(自定义域 → 站点根 → 保留子域 → 通配),第一条命中即返回:

const APEX = 'app.com';
const RESERVED = ['www', 'api', 'admin', 'mail'];      // 注册时也禁止占用
const CUSTOM = { 'dash.acme-corp.com': 'acme' };       // 自定义域 → 租户 映射表

function tenantOf(host) {
  if (CUSTOM[host]) return { tenant: CUSTOM[host] };   // 自定义域: 查映射表 (优先)
  if (host === APEX) return { site: 'marketing' };     // 站点根域: 官网

  if (host.endsWith('.' + APEX)) {
    const sub = host.slice(0, -(APEX.length + 1));
    if (sub.includes('.')) throw '多级子域: 通配证书只覆盖一层';   // *.app.com 保不了 a.b.app.com
    if (RESERVED.includes(sub)) return { service: sub };          // 保留子域: 内部服务
    return { tenant: sub };                                       // 通配: 子域即租户
  }
  return null;                                          // 不属于本服务
}

建议 · 通配证书 *.app.com 只覆盖一层:它能保 acme.app.com,但保不了 api.acme.app.com,那需要 *.acme.app.com 这另一张证书。所以多数 SaaS 规定租户子域只能一层,解析时遇到多级 label 直接判非法。host 路由的边界常常是被证书覆盖范围而非代码逻辑划定的。

网关的 location 也能按 server_name(即 host)分流,多租户是把 host 维度用到极致,一条通配规则覆盖无限个租户子域。host 本身也有结构(subdomain、二级域、TLD),URL Anatomy 系列逐段拆开讲,是理解通配匹配的前提。自定义域那张「客户 CNAME 到映射表」同样是一层 indirection,与远程配置入口同型。