You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

多租户SaaS架构数据防污染:家装初创企业技术问询

多租户SaaS系统数据隔离最佳实践 & 踩坑经验

我之前在一家SaaS公司处理过几乎一模一样的家装类多租户场景,太懂你这种担忧了——数据交叉污染绝对是多租户系统里最致命的坑之一,你的风险意识真的很必要,早规划早规避总比出问题再救火强!结合我的实战经验,给你分享几个核心的最佳实践:

一、从依赖会话到强制租户上下文注入

当前靠登录会话传递租户ID的方式,很容易在批量操作、异步任务或者第三方集成场景中遗漏租户标识。建议做一个全局的请求拦截器/过滤器:

  • 在每个请求入口提取租户ID(从会话、JWT Token或者请求头均可),注入到线程上下文(比如ThreadLocal)
  • 所有数据访问层(DAO/Repository)的查询、修改操作,自动从上下文获取租户ID并拼接条件,禁止手动传参或者遗漏

举个Java MVC的拦截器示例:

public class TenantInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 从会话获取租户ID,实际可换成从JWT解析
        String tenantId = request.getSession().getAttribute("tenantId").toString();
        TenantContext.setCurrentTenantId(tenantId);
        // 同时放入日志上下文,方便排查问题
        MDC.put("tenantId", tenantId);
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 必须清理,避免线程池复用导致的租户ID串用
        TenantContext.clear();
        MDC.remove("tenantId");
    }
}

然后在MyBatis或者JPA的查询中,自动拼接租户条件:

<!-- MyBatis示例 -->
<select id="findProjects" resultType="Project">
    SELECT * FROM projects WHERE tenant_id = #{@com.xxx.TenantContext@getCurrentTenantId()}
</select>

二、数据库层面加行级安全(RLS)兜底

应用层的防护难免有疏漏,数据库层面的行级隔离是最后一道防线:

  • PostgreSQL直接用原生的Row Level Security(RLS),给每个租户相关表添加隔离策略
  • MySQL可以通过视图或者触发器实现类似效果

PostgreSQL的RLS示例:

-- 开启表的行级安全
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
-- 创建隔离策略:仅允许当前租户访问自己的数据
CREATE POLICY tenant_isolation_policy ON projects
    FOR ALL USING (tenant_id = current_setting('app.current_tenant')::text);

然后在应用建立数据库连接后,设置当前租户:

SET app.current_tenant = 'xxx';

这样哪怕应用层代码漏了租户条件,数据库也会自动过滤掉不属于当前租户的数据。

三、代码审查+自动化测试双保险

  • 代码审查:把「查询/修改操作是否包含租户ID条件」作为必查项,任何遗漏都必须整改
  • 自动化测试:专门编写跨租户访问的测试用例,比如用租户A的身份尝试读取租户B的数据,验证是否返回空或者抛出权限异常

JUnit测试示例:

@Test
void shouldNotAccessOtherTenantData() {
    // 模拟登录租户A
    loginAsTenant("tenant_001");
    // 尝试查询租户B的项目
    List<Project> crossTenantProjects = projectService.listByTenantId("tenant_002");
    // 预期返回空列表或者抛出权限异常
    assertTrue(crossTenantProjects.isEmpty());
}

四、缓存与资源的租户隔离

如果用了Redis、本地缓存等,必须把租户ID作为缓存键的一部分,比如project:{tenantId}:{projectId},避免不同租户的缓存数据互相覆盖或者读取。

踩过的坑分享

我之前接手过一个项目,早期没做全局拦截,有个开发写批量查询时忘了加租户ID,上线后两个小型家装客户的项目数据混在了一起——不仅花了3天手动清理数据,还赔了客户的损失。从那之后,我们就把「租户上下文注入+RLS」当成了多租户系统的标配,再也没出过类似问题。

内容的提问来源于stack exchange,提问作者Sam Winston

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.27 06:49:21