CodeIgniter实现多租户的关键是tenant_id全程贯穿:在BaseController统一注入并校验,Model层所有查询强制where绑定,路由通过子域名或路径前缀动态映射,日志、缓存、URL等全链路携带tenant_id,杜绝跨租户数据泄露。

CodeIgniter 本身不提供开箱即用的多租户支持,但只要 tenant_id 能贯穿请求生命周期每一层——从路由、会话、模型查询到日志和缓存——就能稳定支撑 SaaS 类型的多租户应用。关键不是“用什么扩展”,而是“在哪漏掉了 tenant_id”。
tenant_id 怎么注入并全局可用
不能靠每次手动传参或读 session,必须在基类控制器里统一提取并挂载。CI3 和 CI4 的处理逻辑一致,只是方法名略有差异。
-
BaseController的__construct()中调用$this->tenant_id = $this->session->userdata('tenant_id')(CI3)或$this->tenant_id = service('session')->get('tenant_id')(CI4) - 务必校验该值存在且为整数/合法字符串,缺失时应重定向至入驻页或抛出
PageNotFoundException - 避免在模型中重复读 session —— 模型只接收
tenant_id参数,由控制器或服务层传递 - 若使用 HMVC(如 CIBootstrap3),需确保子模块的控制器也继承同一
BaseController,否则tenant_id会丢失
Model 层怎么防止跨租户数据泄露
这是最常出问题的一环:90% 的数据越权都源于 Model 查询漏了 WHERE tenant_id = ?。CI 的 Query Builder 不会自动帮你加这个条件。
- 所有业务模型(如
Product_model、Order_model)的方法,只要涉及读写数据,必须显式接受$tenant_id参数,或在构造时强制绑定 - 不要写
$this->db->where('tenant_id', $this->tenant_id)在每个方法里 —— 推荐封装一个scoped_where()方法,在父模型中统一调用 - 禁止拼接 SQL 字符串(如
"WHERE tenant_id = ".$tenant_id),必须用参数化方式:$this->db->where('tenant_id', $tenant_id) - CI4 用户可考虑用
withFilter()+ 全局作用域,但需注意它不适用于原生查询或子查询场景
路由怎么区分不同租户的访问入口
路径前缀(/m/acme/dashboard)和子域名(acme.example.com)都能用,但实现成本和运维复杂度不同。
- 子域名方案更干净:在
routes.php开头解析$_SERVER['HTTP_HOST'],提取子域名后查主库映射tenant_id,再写入 session 或 request attribute - 路径前缀需配置通配符路由:
$route['m/(:any)/(.*)'] = 'tenant/$2';,然后在tenant控制器里根据$1(slug)查租户,失败则 404 - 无论哪种,URL 生成函数(如
site_url())必须重写或包装,确保后续链接自动带上当前租户上下文,否则跳转会丢 tenant_id - 静态资源(CSS/JS)路径也要带租户标识,否则缓存可能混用 —— 可加 query 参数如
?t=acme,或用租户专属子目录
日志、缓存、加密这些周边怎么隔离租户
这些看似“辅助”的环节一旦忽略 tenant_id,就会导致行为错乱或安全漏洞,比如 A 租户的日志写进 B 租户的 Redis key 里。
- 日志文件名或数据库表名应包含
tenant_id,例如log_acme_20260512.php;CI4 的Logger支持自定义 handler,可覆盖write()方法插入租户前缀 - Redis 缓存 key 必须加前缀:
"acme:product_list:123",而不是"product_list:123";别依赖prefix配置项全局设置,它无法动态切换 - 敏感字段加密时,
encryption_key可复用,但密文存储字段建议加tenant_id校验(如 AES-GCM 的附加认证数据 AAD),防止密文被复制到其他租户记录中解密成功 - 子账号登录的 session cookie 名称要独立(如
ci_subsession_acme),避免同域下多个租户后台互相干扰
真正难的不是写十个 WHERE tenant_id = ?,而是在新增一个 API 接口、加一个定时任务、引入一个第三方 SDK 时,本能地意识到:“这里有没有 tenant_id 上下文?它会不会跨租户生效?”——这种意识比任何框架特性都重要。


















