- 后端
- 微服务
- 云原生
【免费下载链接】midway
🍔 A Node.js Serverless Framework for front-end/full-stack developers. Build the application for next decade. Works on AWS, Alibaba Cloud, Tencent Cloud and traditional VM/Container. Super easy integrate with React and Vue. 🌈
本文以 Midway 官方租户扩展组件@midwayjs/tenant为核心,介绍多租户(SaaS 中后台)场景下租户信息的定义、写入与跨作用域读取的完整方案。你将掌握如何通过TenantManager在中间件中写入租户、在单例服务中安全读取当前请求对应的租户数据,并理解其底层依赖的异步上下文(Async Context)机制与请求隔离原理。
什么是租户(Tenant)
租户管理是中后台(Admin/Console)业务开发中几乎必备的能力。在真实业务里,不同用户的数据往往需要隔离存放于不同的数据源、数据库命名空间或区域中,这些相互隔离的数据区域在 Midway 中统称为"租户"。
在认证流程拿到用户信息后,服务端通常需要将其映射为对应的租户标识,并让后续的所有数据读写操作都落在正确的租户范围内。因此,一个可靠的多租户框架必须解决两个核心问题:
- 如何在每个请求链路中记录"当前租户是谁";
- 如何让这条链路中的所有对象(无论单例还是请求作用域)都能读到同一份租户信息。
Midway 官方扩展包@midwayjs/tenant正是为第二个问题而生:官方文档明确指出"租户信息仅仅在请求链路中使用是不够的,需要在不同的作用域都生效,这就需要新的架构来支持",该组件提供的TenantManager即承担了这一职责。该组件在 3.16 版本发布时新增(见 Release 3.16.0 说明),其核心能力是"跨作用域管理租户信息"。
安装依赖
@midwayjs/tenant是租户功能的主包,支持标准 Web 项目、Serverless 以及一体化(前后端一体)项目,且不包含独立主框架与独立日志(从官方扩展能力表可以确认)。
使用 npm 安装(v4 主版本):
$ npm i @midwayjs/tenant@4 --save或者在package.json的dependencies中声明后重新安装:
{ "dependencies": { "@midwayjs/tenant": "^4.0.0" } }当前仓库中的该组件版本为4.2.5,其运行环境要求 Node.js>= 20(见 package.json),并作为 monorepo workspace 依赖了@midwayjs/core与@midwayjs/mock(后者仅用于测试)。
引入组件
在项目入口src/configuration.ts中通过imports引入租户组件:
import { Configuration } from '@midwayjs/core'; import * as tenant from '@midwayjs/tenant'; @Configuration({ imports: [ // ... 其他组件 tenant, ], }) export class MainConfiguration { }组件的默认导出TenantConfiguration在源码中通过@Configuration({ namespace: 'tenant', ... })声明(见 configuration.ts)。值得注意的是,该组件在onReady阶段会自动执行container.getAsync(TenantManager)预加载TenantManager单例,并在importConfigs中默认开启了asyncContextManager.enable: true——这正是组件能跨作用域读写租户的前提(详见下文原理章节)。
租户信息的数据结构
租户信息的具体格式完全由业务自行定义,组件不做任何强约束,例如:
interface TenantInfo { id: string; name: string; }你可以按需扩展字段(如region、dbKey、namespace等),只要保证setCurrentTenant与getCurrentTenant使用同一类型即可。从类型声明看,TenantManager的两个方法都是泛型方法(getCurrentTenant<T = any>()/setCurrentTenant<T = any>(currentTenant: T)),因此读写时的类型可以自由指定(见 tenantManager.ts)。
在请求链路中写入租户信息
租户信息与请求强关联,官方推荐在中间件(Middleware)中完成写入。典型做法是:在认证中间件之后、业务处理之前,根据当前登录用户解析出租户并调用setCurrentTenant:
import { TenantManager } from '@midwayjs/tenant'; import { Middleware, Inject } from '@midwayjs/core'; @Middleware() class TenantMiddleware { @Inject() tenantManager: TenantManager; resolve() { return async (ctx, next) => { // 请求链路中设置租户信息 await this.tenantManager.setCurrentTenant({ id: '123', name: '我的租户' }); // 记得放行后续中间件与业务逻辑 await next(); }; } }提示:示例中的
await next()在官方文档示例中未显式写出,但真实中间件中必须调用next()才会继续执行后续链路,否则请求会被截断。
setCurrentTenant的语义是把租户数据写入"当前异步上下文"对应的请求对象ctx的属性中。从源码可以看到,TenantManager通过applicationContext.get(ASYNC_CONTEXT_MANAGER_KEY)取得异步上下文管理器,再取出当前活跃上下文中的ASYNC_CONTEXT_KEY对应的请求上下文ctx,最后调用ctx.setAttr(CURRENT_TENANT_MANAGER_KEY, currentTenant)完成写入。这里的CURRENT_TENANT_MANAGER_KEY是组件内部私有常量'tenant:current_manager_tenant_key',即租户数据实际存放在 ctx 属性表中的键名。
写入时有一个重要边界:如果当前异步上下文中没有请求上下文(例如在非请求场景调用),组件会直接抛出MidwayEmptyValueError,错误信息为'Current Async Context is Empty'(对应 framework.ts 中 MidwayEmptyValueError 定义)。测试用例 index.test.ts 中专门验证了这一行为:在未建立异步上下文时调用setCurrentTenant会捕获到该错误。
在任意作用域读取租户信息
写入完成后,无论是单例服务还是请求作用域对象,都可以通过getCurrentTenant读取到当前请求对应的租户:
import { TenantManager } from '@midwayjs/tenant'; import { Inject, Singleton } from '@midwayjs/core'; import { TenantInfo } from '../interface'; @Singleton() class TenantService { @Inject() tenantManager: TenantManager; async getTenantInfo() { const tenantInfo = await this.tenantManager.getCurrentTenant<TenantInfo>(); if (tenantInfo) { console.log(tenantInfo.name); // output => 我的租户 } // 根据租户信息选择对应的数据源 / 命名空间 } }getCurrentTenant的实现路径与写入完全对称:先拿到ASYNC_CONTEXT_MANAGER_KEY对应的AsyncContextManager,通过active()取得当前活跃上下文,再取出其中的请求上下文并执行ctx.getAttr(CURRENT_TENANT_MANAGER_KEY)。若当前无请求上下文,方法返回undefined,因此调用方建议先做空值判断(如官方示例中的if (tenantInfo))。
这里最能体现组件价值的一点是:TenantService被声明为@Singleton()(单例),但它读取到的却是当前请求的租户信息。传统思路下单例无法感知请求级数据,而TenantManager借助异步上下文打通了这一壁垒——这也就是官方强调的"不管是单例还是请求作用域,你都仅能获取到当前请求对应的租户数据"。
底层原理:异步上下文(Async Context)机制
要理解TenantManager为什么能跨作用域读写租户,需要看它依赖的核心基础设施。TenantManager的三个关键调用点都围绕一组从@midwayjs/core导出的常量和接口:
ASYNC_CONTEXT_MANAGER_KEY:值为'MIDWAY_ASYNC_CONTEXT_MANAGER_KEY',用于从容器中取出AsyncContextManager实例(见 constants.ts);ASYNC_CONTEXT_KEY:Symbol('ASYNC_CONTEXT_KEY'),用于在异步上下文中存放请求对象ctx;ASYNC_ROOT_CONTEXT:根上下文,当没有活跃上下文时作为默认父上下文。
框架在 baseFramework.ts 中做了两件事:根据配置项asyncContextManager.enable决定注册AsyncLocalStorageContextManager(基于 Node 原生async_hooks的AsyncLocalStorage)还是NoopContextManager;随后在首个中间件中用ASYNC_ROOT_CONTEXT.setValue(ASYNC_CONTEXT_KEY, ctx)把请求对象挂到上下文中,并通过contextManager.with(rootContext, ...)让整个请求链路共享该上下文(实现见 asyncContextManager.ts)。
这就是为什么租户组件在 configuration.ts 中主动开启了asyncContextManager.enable: true:只有启用基于AsyncLocalStorage的上下文管理器,active()才能返回真实、且随异步调用链传播的上下文,setCurrentTenant/getCurrentTenant才能正确读写租户。
在 Web 框架(如 Koa/Express)中,ctx.setAttr/ctx.getAttr会透传到请求容器(requestContainer)的属性表,而容器本身的setAttr/getAttr实现就是一个Map的存取(见 requestContainer.ts)。租户组件正是复用了这套属性表机制,将租户数据以'tenant:current_manager_tenant_key'为键存入其中。
隔离性与使用注意事项
官方文档在末尾给出了三条关键提示,它们直接决定了多租户方案的正确性:
- 租户信息一定关联请求:如有需要,你可以在不同的 Framework(如同时挂载 Web 与 RPC 场景)中都加入写租户的中间件,确保每个入口链路都有租户上下文;
- 每个请求保存的租户信息是隔离的:得益于
AsyncLocalStorage的异步上下文隔离,请求 A 写入的租户不会泄漏到请求 B; - 无论是单例还是请求作用域,都只能获取到当前请求对应的租户数据:这是组件设计的核心契约,也是避免跨租户数据串读的关键保证。
配套的测试用例完整验证了这套隔离语义(见 index.test.ts):
- 通过
createLightApp创建应用并导入 tenant 组件,手动启用contextManager并注册ASYNC_CONTEXT_MANAGER_KEY; - 在无上下文时调用
setCurrentTenant会抛出Current Async Context is Empty; - 构造两个独立上下文
ctx1、ctx2,分别通过contextManager.with(newContext1, ...)和with(newContext2, ...)执行:第一个上下文写入{ id: 'test1' },第二个写入{ id: 'test2' },随后从单例TestService中读取,各自只能读到本上下文写入的租户,互不干扰。
这套测试从实证层面确认了"单例服务 + 请求级租户 + 上下文隔离"三者可以同时成立,是理解组件行为最直接的参考样例。
典型落地路径小结
一个完整的多租户接入流程可以归纳为四步:
- 安装:
npm i @midwayjs/tenant@4 --save; - 引入:在
src/configuration.ts的@Configuration({ imports: [...] })中加入tenant组件(组件会自动开启异步上下文管理器); - 写入:在认证之后的中间件中,根据当前用户调用
tenantManager.setCurrentTenant(tenantInfo)写入租户; - 读取:在任意单例或请求作用域服务中注入
TenantManager,通过tenantManager.getCurrentTenant<T>()获取当前请求的租户,再据此路由到对应的数据源或命名空间。
通过这套机制,你可以在不侵入业务服务签名(无需层层透传租户参数)的前提下,让整个请求链路共享租户上下文,从而将多租户隔离能力与业务代码解耦。若需深入了解组件实现细节,可继续阅读 TenantManager 源码 与 租户官方文档。
- 后端
- 微服务
- 云原生
【免费下载链接】midway
🍔 A Node.js Serverless Framework for front-end/full-stack developers. Build the application for next decade. Works on AWS, Alibaba Cloud, Tencent Cloud and traditional VM/Container. Super easy integrate with React and Vue. 🌈
相关推荐
Midway 租户组件(@midwayjs/tenant)实战指南:基于 Async Context 的跨作用域租户信息管理
Midway 租户组件(@midwayjs/tenant)实战指南:基于 Async Context 的跨作用域租户信息管理 在开发中后台、SaaS 或多租户业
后端微服务云原生Midway 多租户组件(@midwayjs/tenant)实战指南:基于 TenantManager 实现请求级租户信息隔离与跨作用域访问
Midway 多租户组件(@midwayjs/tenant)实战指南:基于 TenantManager 实现请求级租户信息隔离与跨作用域访问 导读 在多租户中后
后端微服务云原生Midway 租户组件(@midwayjs/tenant)实战指南:基于请求上下文的多租户信息管理
Midway 租户组件(@midwayjs/tenant)实战指南:基于请求上下文的多租户信息管理 本文围绕 Midway 开源仓库中的租户扩展组件( @mid
后端微服务云原生
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考