部分隔离型多租户SaaS架构的租户身份验证策略咨询
部分隔离型多租户SaaS架构的租户身份验证策略咨询
作为搞过几个多租户SaaS项目的开发者,我太懂你这种纠结了——既要兼顾前端开发的便捷性,又不能把安全和架构严谨性丢在一边,咱们一步步把这个问题捋清楚:
一、绝对不能只信任前端传的X-Tenant-ID
你担心的恶意篡改场景完全是成立的,这是硬伤级的安全漏洞。前端的任何请求头、参数都是可以轻易被篡改的:用浏览器DevTools改一下请求头,或者用Postman直接构造请求,就能把X-Tenant-ID: foo改成X-Tenant-ID: bar,如果后端完全信任这个值,直接就能越权访问其他店铺的所有数据,这绝对不能容忍。
二、后端必须自己从Host头解析租户身份(核心策略)
这是所有正经SaaS平台的标准做法,后端绝对不能把租户身份的控制权交出去。正确的流程是:
- 后端在请求进入业务逻辑之前(比如Laravel的中间件、Next.js API的入口),直接从请求的
Host头中提取子域名,比如从foo.example.com拆出foo,然后通过这个子域名查询租户表拿到对应的tenant_id。 - (可选但推荐)如果前端传了
X-Tenant-ID,可以做一个校验:如果后端解析出的tenant_id和前端传的不一致,直接返回403 Forbidden,快速拦截异常请求,但这只是额外的校验,绝对不能用前端传的值作为业务逻辑的判断依据。
Laravel中实现的示例(中间件)
你可以写一个全局中间件,在所有请求进入路由之前完成租户识别和绑定:
<?php namespace App\Http\Middleware; use Closure; use Illuminate\Http\Request; use App\Models\Tenant; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; class ResolveTenant { public function handle(Request $request, Closure $next) { // 从Host头解析子域名 $hostParts = explode('.', $request->getHost()); if (count($hostParts) < 3 || !$hostParts[0]) { abort(404, 'Invalid shop domain'); } $subdomain = $hostParts[0]; // 查询租户信息 $tenant = Tenant::where('subdomain', $subdomain)->firstOrFail(); // 可选:校验前端传的X-Tenant-ID是否一致 if ($request->header('X-Tenant-ID') && $request->header('X-Tenant-ID') !== $tenant->id) { throw new AccessDeniedHttpException('Invalid tenant identifier'); } // 将租户实例绑定到Laravel容器,后续全局可用 app()->instance('currentTenant', $tenant); // 给所有租户相关模型添加全局查询作用域,自动过滤当前租户数据 \Illuminate\Database\Eloquent\Model::addGlobalScope('tenant_isolation', function ($query) use ($tenant) { $tenantModels = [ \App\Models\Product::class, \App\Models\ShopSetting::class, \App\Models\Review::class, \App\Models\Order::class ]; if (in_array(get_class($query->getModel()), $tenantModels)) { $query->where('tenant_id', $tenant->id); } }); return $next($request); } }
把这个中间件注册到api和web中间件组,所有请求都会先经过租户解析,后续业务逻辑里直接用app('currentTenant')拿到租户信息,数据库查询也会自动带上tenant_id过滤,完全不需要依赖前端传的值。
三、共享用户+隔离店铺数据的正确实现
针对你这种「客户全局共享、店铺数据隔离」的场景,核心要做好两点:
1. 数据库层面的隔离
- 全局表:
users(所有客户、卖家的账号信息,无tenant_id)、tenants(存储子域名、店铺名称、卖家关联ID等) - 租户隔离表:
products、shop_settings、reviews、orders等,所有表必须加tenant_id字段,关联tenants.id - 关联表:如果有需要(比如用户收藏商品),可以加
user_product_favorites,字段包括user_id、product_id(通过product_id关联到products.tenant_id,避免冗余)
2. 权限逻辑的控制
- 客户访问店铺资源:比如查看商品、提交订单,后端解析租户ID后,直接查询该租户下的资源即可(不需要额外权限校验,因为商品是公开的);如果是查看自己的订单,就需要同时关联
orders.user_id = 当前登录用户ID和orders.tenant_id = 当前解析的租户ID,保证用户只能看自己在该店铺的订单。 - 卖家访问店铺资源:除了解析租户ID,还要额外校验当前登录用户是否是该租户的管理员(比如
tenants.user_id = 当前登录用户ID),只有校验通过才能访问卖家端的接口(比如修改商品、查看店铺设置)。
四、大厂的常见做法(以Shopify为例)
你提到的「客户跨店铺购物、店铺数据完全隔离」的场景,和Shopify的模式几乎一致。他们的核心逻辑就是:
- 所有店铺请求的身份识别完全依赖域名,后端直接从请求域名解析店铺ID,不会信任前端传的任何标识
- 客户账号是全局的,登录后可以在任意店铺购物,订单数据会关联对应的店铺ID和客户ID
- 所有店铺的资源查询都会强制带上店铺ID,从根源上杜绝越权访问
最后总结几个关键点
- 安全优先:后端必须完全掌控租户身份的解析,绝对不能依赖前端传的任何参数/请求头,这是底线。
- 架构严谨:租户上下文是后端的核心业务边界,必须在请求进入业务逻辑之前就解析完成,并绑定到全局上下文,后续所有逻辑都基于这个上下文执行。
- 共享用户的处理:全局用户表+租户隔离表的组合是标准方案,权限逻辑要同时结合「当前用户ID」和「当前租户ID」,保证用户只能访问自己在对应店铺下的资源,以及店铺的公开资源。
如果你还有具体的实现细节疑问(比如Laravel的查询作用域怎么写、Next.js前端怎么配合),可以再细化问我~




