<!-- generated-by: gsd-doc-writer -->
# 租户、操作身份与上下文

规则 `CORE-TENANT-001`。新增实体、自定义查询、关系写入、后台任务、缓存均需检查本规则。

## 先区分四个概念

- 操作身份：谁执行操作，由受信任会话/任务身份建立。
- 目标租户：本次选择在哪个租户范围工作。
- 实体租户：数据实际归属。
- 数据范围：租户内部还允许访问哪些数据，由业务与授权模块进一步约束。

租户过滤不能代替 API 授权，角色授权也不能替代数据行的租户校验。客户端传入一个 tenantKey 不会自动成为可信操作上下文。

## 实体模式

来源：[TenantScopeMode](../../ineed-core-data-base/src/main/java/com/ineed/core/data/entity/TenantScopeMode.java)。

| 模式 | 数据语义 | 写入时重点 |
| --- | --- | --- |
| `TENANT_ONLY` | 当前租户数据 | 不能保存成全局租户行 |
| `GLOBAL_ONLY` | 全局数据 | 行必须属于全局租户，要求具备全局维护身份 |
| `GLOBAL_WITH_TENANT` | 全局数据与当前租户数据可一起读取 | 普通租户能读取全局行不代表能修改它 |
| `TENANT_SELF` | 租户主体自身 | 创建/修改规则不同于普通租户业务实体，查看具体实现 |

标准 ID 读取使用 `findEntityById / findAllEntityById / existsEntityById`。JDBC、JPA、MyBatis-Flex、MongoDB 的这些 Core 能力在返回实体前通过 `EntityTenantSupport.isReadable` 过滤租户范围；同步 `FindByIdManager` 转换 VO 前再做相同检查。单条不可见时 Manager 返回 `DATA_NOT_FOUND`，批量结果排除不可见行，存在性检查返回 `false`。非租户实体不受此过滤；`TENANT_SELF` 沿用租户主体可跨目标读取的语义，API 授权仍由业务约束。

这是 Core 能力入口的返回值隔离，不是数据库级行安全。底层原生 `findById / findAllById / existsById / selectOneById`、自定义 SQL、派生方法及 join 不自动获得这些检查；业务应使用标准能力或显式加入读取范围。R2DBC 的标准仓储基类在查询条件中限定范围。任何数据栈都不能把这些租户规则当作逻辑删除、字段脱敏或租户内数据范围授权。

## 写入链路

[EntityTenantSupport](../../ineed-core-data-base/src/main/java/com/ineed/core/data/entity/EntityTenantSupport.java)分别处理初始化、创建和修改校验。`init` 在缺少实体租户时按模式和目标租户补值；多租户下缺少必要目标会报错。

`checkModifyAccess(Object)` 先检查操作身份与实体模式，再检查当前目标租户。其带显式操作租户参数的重载只做对应写权限判断，不能当作包含全部目标租户检查的通用替代。

对于非全局行，修改时目标租户必须与实体租户相符；支持全局行的模式另有分支。即使操作身份可维护多个租户，也应在选定目标范围内操作。单租户模式下还会考虑配置租户的维护资格，具体见上述源码。

标准账户管理动作（重置密码、启停、密码过期标记、解锁时间）会先读取账户，再检查操作身份与目标租户，缺失或越界时不发出更新。响应式检查在订阅链内获取上下文。底层认证计数操作不属于该管理入口，认证模块需维护自己的可信调用边界。

新建和修改应沿 Core 标准生命周期执行；业务直接调用仓储时必须自行确认绕过了哪些校验。优先通过已有 Manager 能力组合，而不是在每个入口复制租户判断。

## 同步与响应式上下文

同步入口使用 [ContextHandler](../../ineed-core-modules/ineed-core-context/src/main/java/com/ineed/base/context/ContextHandler.java)。响应式入口使用 [ReactiveContextHandler](../../ineed-core-reactive-modules/ineed-core-context-reactive/src/main/java/com/ineed/base/context/ReactiveContextHandler.java)，上下文必须随订阅链传播。

不要在 Publisher 装配时提前读取当前用户，也不要把同步 ThreadLocal 当作任意异步线程都能读取的事实。后台任务需要显式建立并清理受信任上下文，不能沿用上次请求残留。WebFlux 请求语言保存在 `OperateInfo` 的 Reactor Context 中，使用 `ReactiveContextHandler.getLocale()` / `ReactiveI18nHandler` 读取；过滤器不向共享线程写入 `LocaleContextHolder`。

## 验证清单

准备租户 A/B 和全局数据，验证创建、查询、按 ID 读取、修改、状态、删除、关系绑定及自定义查询。至少包含：A 修改 B 失败、全局身份选 A 不能修改 B、普通身份不能修改全局行、支持全局读取时返回集合正确、缺失上下文不越界。

源码级例子：[EntityTenantSupportTest](../../ineed-core-data-base/src/test/java/com/ineed/core/data/entity/EntityTenantSupportTest.java)。它证明租户策略，不替代各数据栈真实 SQL 和 HTTP 授权测试。

ID 读取与账户写入的实际 JDBC 验证见 [ID 仓储集成测试](../../ineed-core-modules/ineed-core-data-jdbc/src/test/java/com/ineed/core/data/jdbc/repository/FindByIdJdbcIntegrationTest.java)和[账户隔离测试](../../ineed-core-modules/ineed-core-data-jdbc/src/test/java/com/ineed/core/data/jdbc/repository/AccountTenantJdbcIntegrationTest.java)，使用 H2；其他阻塞适配器另有默认方法契约测试，不代表已连接其生产数据库。
