02

门禁、身份与租户

无论请求最终去 `im-app-server` 还是 `im-admin-server`,都逃不过一条底线:先认人,再认租户,再决定你能看哪份数据。多租户系统最怕的不是慢,而是串线。

同一套服务,为什么不会把甲公司的数据给乙公司

秘密不在某个超复杂算法,而在一串 中间件。认证回答“你是谁”,租户中间件回答“你该看哪一摊数据”。这类规则一旦写错,比普通功能 bug 更危险。

请求入口

📱
客户端请求

门禁链路

🪪
Authentication
🏢
resolveTenant
validateTenantAccess

业务层

🗂️
Controller / Repo
点任一模块,看它在门禁链中的职责。

看一场中间件群聊

把请求当成一个访客,就容易理解这一串检查。谁先说话,谁后放行,顺序绝不能乱。

⚠️
常见误判

很多人把“401 未登录”和“400/404 租户问题”混成一团。这里其实是两层不同的门禁,修法完全不同。

真实代码:租户 ID 是怎样找出来的

下面这段是 `tenant.js` 的核心片段。顺序很关键,因为它定义了系统优先信谁,也决定了一个 UUID 格式的租户 ID 从哪里来。

代码

    // Priority 1: request header
    tenantId = ctx.headers[TenantConfig.HEADER_NAME.toLowerCase()];

    // Priority 2: tenant ID from JWT token
    if (!tenantId && ctx.state.user && ctx.state.user.tenant_id) {
      tenantId = ctx.state.user.tenant_id;
    }

    // Priority 3: query parameter
    if (!tenantId && ctx.query[TenantConfig.QUERY_PARAM_NAME]) {
      tenantId = ctx.query[TenantConfig.QUERY_PARAM_NAME];
    }

    // Tenant ID must exist
    if (!tenantId) {
      ctx.throw(400, "Tenant ID is required");
    }

    // Validate tenant ID format
    if (tenantId && !isValidUUID(tenantId)) {
      ctx.throw(400, "Invalid tenant ID format");
    }

    // Persist tenant ID
    ctx.state.tenantId = tenantId;
          
白话

先信请求头。这通常是客户端最明确地告诉服务器:我要进哪个租户。

如果头里没有,再看登录后的 JWT 数据。也就是“你上次登录时系统记住的租户”。

前两处都拿不到,才退而求其次去查 Query 参数。

如果三处都没有,系统直接打回,因为它根本不知道你该看哪份数据。

就算有值,也要校验 UUID 格式,避免拿一串乱值继续向后传染。

最后把结果挂进 `ctx.state`,这样后面的控制器和仓储层都能复用。

小测:你能看出是哪道门没开吗

用户明明带着有效登录态,却收到 `Tenant ID is required`。第一站该查哪里?

为什么系统不把“认证”和“租户校验”写成一个大函数?

如果用户看到了别的租户的数据,你会先怀疑哪一层?