Skip to content

租户 Tenant

租户是平台里业务数据的隔离边界——同一套部署里,A 公司的设备、位号、数据和 B 公司的彼此看不见。每一条业务记录都带一个 tenantId,平台据它把数据切成互不串台的几份。

租户回答的是"这条数据归谁、谁能看见它"。它不是一个功能、也不是一个角色,而是一道数据围墙:你登录后拿到的令牌绑定了一个 tenantId,之后你创建的设备、采到的位号值、下发的指令,全都自动打上这个标签;按 ID 或批量访问别家租户的记录会被判为不存在、或被剔除。

容易混淆的是租户和主体(principal)、角色。一句话区分:租户管"能碰哪条数据",角色管" 能做哪类操作",主体是"谁在操作"。三者正交——你可能有 device:get 权限(角色给的),但去 get 别家租户的设备依然失败(租户拦的)。就像一栋写字楼:门禁卡决定你能进哪一层(租户),职级决定你在自己那层能开哪些会议室(角色),工牌上印的是你本人(主体)。

关键字段

租户 TenantBO(表 dc3_tenant,继承 BaseBOid / remark / 审计字段):

字段类型含义
tenantNameString租户名称(展示用)
tenantCodeString租户唯一编码,登录时用它定位租户;编码为 default 的租户是系统管理员租户
tenantExtTenantExt(JSON)扩展配置,预留字段
enableFlagEnableFlagEnum启用标志,见下

租户不是孤立的:身份"属于哪个租户"由租户成员关系 TenantMembershipBO(表 dc3_tenant_membership)一行一行声明,唯一索引建在 (tenant_id, principal_id)

字段类型含义
tenantIdLong归属的租户
principalIdLong归属的主体(principal)
principalTypePrincipalTypeEnum主体类型:USER / SERVICE_ACCOUNT / SYSTEM
membershipStatusMembershipStatusEnum成员状态:ACTIVE / SUSPENDED / INVITED
joinedTimeLocalDateTime加入时间

一个人可以属于多个租户

因为唯一索引在 (tenant_id, principal_id),同一个 USER 主体可以在多个租户下各有一行成员关系(多租户成员)。登录时由 name + tenant 一起定位是哪一段成员关系。按设计 SERVICE_ACCOUNT 服务账号只属于一个租户。

启用标志 enableFlag

EnableFlagEnum数据库说明
ENABLE0启用
DISABLE1禁用

与其它概念的关系

TenantOwned 业务实体 · 每行带 tenantIdprincipalId · 一人可属多租户NMtenantId · 1:N 拥有tenantId · 1:N 拥有入租户后由 RBAC 授权一人多租户成员租户 TENANT · dc3_tenantidPKtenantNametenantCodeUKtenantExt · enableFlagtenantCode = default 的租户即系统管理员租户主体 PRINCIPALidPKprincipalType: USER /SERVICE_ACCOUNT / SYSTEMname · salt · passwordenableFlag租户成员 TENANT_MEMBERSHIPtenantId · principalIdFKUKUK (tenant_id, principal_id)principalTypemembershipStatus:ACTIVE / SUSPENDED / INVITED · joinedTimeDEVICEidPKdeviceNametenantIdFKPROFILEid · profileCodeprofileShareFlagtenantIdFKPOINT_VALUEdeviceId · pointIdrawValue · calValuetenantIdFKCOMMANDid · commandCodecallTypeFlag · timeouttenantIdFK+ POINT / EVENT / EVENT_HISTORY / DRIVER / 属性配置 …getTenantId() → 隔离的施加对象角色与租户正交dc3_role_principal_bind 决定能做哪些操作租户管"能碰哪条数据" · 角色管"能做哪类操作"多租户成员alice 可同时属 default 与 acme(各一行成员关系)SERVICE_ACCOUNT 按设计只属一个租户PKPK 主键FKFK 外键UKUK 唯一键1:N 拥有N:M 成员授权 / 注记(虚线)
  • 一切实现了 TenantOwned(提供 getTenantId())的业务实体都归某个租户拥有,是隔离的施加对象。
  • 主体经 dc3_tenant_membership 加入租户;进入租户后再由 RBAC(dc3_role_principal_bind )决定能做哪些操作。详见 鉴权 · 租户 · RBAC

隔离是怎么落实的

租户隔离落在控制器层:取数后比对实体 tenantId 与调用方租户,跨租户访问被判为不存在或被剔除。

所有请求经网关 · 三个鉴权头 X-Auth-Tenant / X-Auth-Login / X-Auth-Token · 令牌绑定调用方 tenantIdHTTP进入[否] 批量过滤[是] 单条比对[是] 本租户[否] 跨租户/不存在未来统一兜底API 请求带鉴权头网关 · 控制器解出调用方租户批量 filterTenant()list / latest / history剔除别家租户条目只返回本租户数据 · 不报错查库取实体返回数据 200requireTenant 通过抛 NotFoundException对外 404 · 不泄露存在性单条按 ID?tenantId 一致?规划中 · 当前未启用库级自动 WHERE tenant_id = ?MybatisPlus 拦截器统一兜底查不到 = 真不存在 或 属于别家租户——对调用方两者无差别(404 而非 403)请求入口网关 / 服务决策分支拒绝(404)规划中(虚线)放行
  • 控制器层(单条按 ID):查到实体后,BaseController.requireTenant() 比对实体的 tenantId 与调用方租户,不一致(或实体不存在)就抛 NotFoundException,对外返回 404
  • 控制器层(批量)BaseController.filterTenant() 只保留属于本租户的条目,直接剔除别家租户的记录。
  • 库级自动追加 WHERE tenant_id = ?:当前未启用(MybatisPlusConfig 只注册了 PaginationInnerInterceptor ),作为统一兜底仍在规划中。

跨租户访问返回 404,不是 403

故意用"不存在"而非"无权限"——避免泄露"某个跨租户资源是否存在"。所以你查不到一台设备时,可能它真不存在,也可能它属于别的租户:对你而言两者无差别。批量查询走 filterTenant(),直接把不属于本租户的条目剔除,而不是报错。

示例

开发环境通常只有一个默认租户,其 tenantCode = default——它同时是系统管理员租户:只有 default 租户里的用户才能创建/删除/修改其它租户( TenantController 显式判定 "default".equals(tenantCode))。

设想 SaaS 部署里再开一个客户租户 tenantCode = acmeacme 的运维 alice 登录后(令牌绑定 acmetenantId)创建设备 泵房-01,这台设备落库时 tenant_id 自动写成 acme。此时 default 租户的管理员即便手握 device:get 权限,按 泵房-01 的 ID 去查,也会因 requireTenant() 比对失败而得到 404——除非他先切换到 acme 租户上下文。alice 反过来也看不到 default 租户的任何数据。

管理 API

租户管理端点在鉴权中心,前缀 /tenant(经网关为 /api/v3/auth/tenant)。非管理员只能操作自己所属的租户:

方法路径说明
POST/tenant/add新增租户(仅 default 租户管理员)
POST/tenant/delete删除租户
POST/tenant/update修改租户
GET/tenant/get_by_id按 ID 查询
GET/tenant/get_by_code按编码查询
POST/tenant/list分页查询

延伸阅读

基于 AGPL-3.0 协议发布