资讯动态

OpenFrame OSS Lib 多租户隔离实战:TenantContext 与 TenantAwareMongoTemplate 实现原理

发布时间:2026/10/15 12:53:55 来源:尧图企业网站定制
【免费下载链接】openframe-oss-libShared libraries between all services项目地址https://gitcode.com/gh_mirrors/op/openframe-oss-lib点击查看免费下载OpenFrame OSS Libopenframe-oss-lib是一个共享服务库仓库其中的多租户隔离方案是它的亮点之一通过TenantContext基于 ThreadLocal 的租户上下文TenantAwareMongoTemplate自动注入 tenantId 的 Mongo 模板让每个租户的数据严格隔离业务代码几乎零感知。本文带你完整拆解这套隔离机制的工作原理。多租户隔离为什么难在 SaaS 或多租户系统中所有租户共用同一个数据库。如果每条查询都要手动拼接tenantId条件一旦漏掉一处就会发生跨租户数据泄漏——这是多租户系统最严重的安全事故之一。OpenFrame 的解法是两道防线防线组件职责请求层TenantContextTenantContextFilter解析并持有当前请求属于哪个租户数据层TenantAwareMongoTemplate对每条 MongoDB 查询自动注入tenantId过滤条件相关源码分布在三个模块openframe-authorization-service-core/ —— 租户上下文请求解析openframe-data-mongo-common/ —— 租户标记接口与租户 ID 提供器openframe-data-mongo-sync/ —— 租户感知模板与仓储第一道防线TenantContextThreadLocal 线程上下文核心实现只有 20 行位于 TenantContext.javapublic final class TenantContext { private static final ThreadLocalString CURRENT_TENANT new ThreadLocal(); public static void setTenantId(String tenantId) { CURRENT_TENANT.set(tenantId); } public static String getTenantId() { return CURRENT_TENANT.get(); } public static void clear() { CURRENT_TENANT.remove(); } }设计要点线程隔离ThreadLocal保证 Servlet 线程模型下每个请求线程持有独立的租户 ID互不串扰显式清理clear()必须由调用方在请求结束时执行防止线程复用导致租户信息粘到下一个请求零依赖静态工具类任何 Service 都能一行取到当前租户TenantContext.getTenantId()。TenantContextFilter租户身份从哪里来TenantContextFilter.java 是OncePerRequestFilter按优先级从三个来源解析租户 IDURL 路径最高优先级形如/{tenantId}/oauth2/authorize的 OAuth 端点取第一段路径作为租户 ID排除login、sso、public等保留词查询参数?tenanttenantId显式指定HTTP Session读取 Session 中的TENANT_ID保证多步 OAuth2 流程中租户上下文延续。解析成功后会同时写入TenantContext和 Session。两个关键的安全细节值得注意 ⚠️租户切换即作废如果 Session 中已存在的租户 ID 与本次请求不一致Filter 会直接session.invalidate()防止跨租户会话污染唯一的例外是 onboarding 临时租户向正式租户的迁移finally 中清理filterChain.doFilter()包在 try/finally 中请求结束必然执行TenantContext.clear()杜绝线程泄漏。 官方架构文档对该模块的完整说明见 docs/reference/architecture/authorization-service-core/tenant-context.md。第二道防线TenantAwareMongoTemplate查询自动加租户口径TenantAwareMongoTemplate.java 继承自 Spring Data 的MongoTemplate是整个数据层隔离的心脏。只隔离该隔离的实体TenantScoped 标记接口模板不会给所有查询都注入条件它只处理实现了 TenantScoped 标记接口的实体public interface TenantScoped { String getTenantId(); void setTenantId(String tenantId); }所有租户级文档Machine、Ticket、Notification等都实现该接口文档中固定携带tenantId字段。核心机制scope() 一行注入模板重写了find、findOne、count、exists、updateFirst、updateMulti、upsert、remove、findAndRemove等全部 CRUD 入口统一先经过 scope()protected Query scope(Query query, Class? entityClass) { if (entityClass ! null TenantScoped.class.isAssignableFrom(entityClass) !query.getQueryObject().containsKey(tenantId)) { query.addCriteria(Criteria.where(tenantId).is(tenantIdProvider.getTenantId())); } return query; }效果是你写repository.findByMachineId(machineId)模板会自动把查询变成{ machineId: ..., tenantId: 当前租户 }——业务代码完全不用关心租户。连 Spring Data 派生查询也没放过Spring Data MongoDB 4.x 的派生查询findByXxx走的是ExecutableFindOperation流式入口会绕过普通的find(Query, Class)方法。模板重写了 query(Class) 方法用 JDK 动态代理包装返回的ExecutableFind拦截matching(Query)调用注入tenantId——派生查询、as(Class)、inCollection(...)全链路都被覆盖。两个必须知道的使用约定聚合查询不自动注入aggregate()被刻意留空不重写聚合管道必须手动添加$match。模板为此提供了便捷方法mongoTemplate.tenantCriteria(); // 生成 tenantId 匹配条件用作第一个 $match mongoTemplate.tenantId(); // 用于复合键场景禁止裸用 MongoTemplate官方安全文档明确要求租户级数据一律走TenantAwareMongoTemplate详见 docs/development/security/README.md。自动装配一个开关切换整套隔离TenantIsolationAutoConfiguration.java 负责装配由配置项openframe.tenant-isolation.enabledtrue触发ConditionalOnProperty将TenantAwareMongoTemplate以Primary的mongoTemplateBean 注册到容器Spring Boot 默认的 MongoTemplate 自动退让——隔离模式下租户感知模板就是唯一的 MongoTemplateTenantIdProvider使用Lazy注入代理在首次查询时才解析真实实现解耦构造顺序。租户 ID 从哪来由TenantIdProvider接口抽象默认实现 DefaultTenantIdProvider 读取环境变量TENANT_ID默认值oss——这正体现了每租户一个 Pod的部署模型隔离不仅靠查询过滤还靠进程级部署。仓储层规范让违规直接编译期可见为了杜绝绕过模板的野路子项目建立了三层约定TenantAwareRepository租户侧仓储接口的组合注解Repository 排除标记共享服务可通过EnableMongoRepositories的排除过滤器整体跳过这些仓储TenantAwareRepositorySupport所有自定义仓储实现的基类强制构造器注入TenantAwareMongoTemplate并提供tenantCriteria()/tenantId()便捷方法ArchUnit 架构测试强制com.openframe.data.repository下所有*RepositoryImpl必须继承 Support 基类、且不允许声明裸MongoTemplate字段——违反规则测试直接失败。一个典型的租户级文档 仓储组合新增文档流程参考 docs/development/setup/local-development.mdData Document(collection my_collection) public class MyDocument implements TenantScoped { Id private String id; private String tenantId; // 业务字段... } public interface MyDocumentRepository extends TenantAwareRepositoryMyDocument, String { // 派生查询自动带租户口径 }租户内自增序列TenantSequence连自增 ID 也是按租户隔离的。TenantSequence 通过复合唯一索引{tenantId: 1, name: 1}实现每租户一套编号例如工单号ticket_number各租户从 1 开始互不干扰——查询时同样依赖模板自动注入的tenantId过滤无需复合键查询。总结这套隔离方案的设计精髓设计点做法价值上下文持有ThreadLocal 请求级 Filter线程隔离、无状态传递、显式清理身份解析路径 查询参数 Session 三级回退兼容 OAuth 端点、显式指定与多步流程会话安全租户切换即作废 Session阻断跨租户会话污染数据隔离模板层统一scope()注入业务代码零感知杜绝漏拼条件派生查询覆盖动态代理拦截matching()Spring Data 4.x 全入口无死角架构守护组合注解 基类 ArchUnit违规用法在 CI 直接暴露部署模型每租户一个 Pod TENANT_ID环境变量查询过滤之外的进程级双保险 延伸阅读整体架构docs/development/architecture/README.md其中Multi-tenancy: ThreadLocal TenantContext为关键设计决策安全最佳实践docs/development/security/README.md仓储层架构文档docs/reference/architecture/data-mongo-sync-repositories/data-mongo-sync-repositories.md对新手来说理解这套机制后做三件事就够了给租户级文档实现TenantScoped、让仓储接口继承TenantAwareRepository、聚合查询记得手动加tenantCriteria()——剩下的交给框架。赞分享【免费下载链接】openframe-oss-libShared libraries between all services项目地址https://gitcode.com/gh_mirrors/op/openframe-oss-lib点击查看免费下载相关推荐Cuba微框架入门如何用Ruby快速构建轻量级Web应用Cuba微框架入门如何用Ruby快速构建轻量级Web应用 Cuba是一个基于Ruby的轻量级Web微框架以其简洁的设计和高效的性能成为开发者构建小型Web应深度解析G9v3-39A5B5B激活参数实现39B模型性能的Mixture-of-Experts架构揭秘深度解析G9v3 39A5B5B激活参数实现39B模型性能的Mixture of Experts架构揭秘 G9v3 39A5B是AI9Stars团队开发的 Mnpx skills命令调试终极指南高效排查技能安装与使用故障npx skills命令调试终极指南高效排查技能安装与使用故障 当我们使用npx skills管理AI代理技能时经常会遇到各种令人困惑的问题技能安装失败、AI 技能CLI开发工具人工智能创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑