多租户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
相关产品推荐
相关产品推荐

