MySQL多租户应使用CREATE USER+GRANT实现用户级隔离,每个租户独立账号并限定IP与库表权限,禁用共用账号、手动改系统表、动态切库及模糊授权,8.0+推荐结合角色复用权限模板。

MySQL 多租户下,用 CREATE USER + GRANT 隔离权限最直接有效
单实例多租户场景里,靠数据库名前缀或应用层过滤权限,不如原生用户级隔离来得干净。MySQL 本身不支持“租户”抽象,但通过严格绑定 user@host 和库表粒度的 GRANT,能实现强边界。
常见错误是给所有租户共用一个账号(比如 'app'@'%'),再靠应用拼接库名访问 tenant_a_orders、tenant_b_orders——这等于没隔离,一旦 SQL 注入或配置写错,数据就串了。
- 每个租户必须分配独立账号,格式为
'tenant_a'@'10.20.30.%'(限制 IP 段比'%'更安全) -
GRANT SELECT, INSERT, UPDATE ON tenant_a.* TO 'tenant_a'@'10.20.30.%'—— 明确限定库名,不带通配符*做跨库授权 - 禁止授予
USAGE以外的全局权限(如GRANT OPTION、PROCESS、SHOW DATABASES),否则租户可发现其他库存在
避免误用 mysql.db 表手动插入做权限控制
有人想绕过 GRANT 语句,直接往系统表 mysql.db 插数据来批量开权限——这是高危操作。MySQL 5.7+ 默认启用 sql_mode=STRICT_TRANS_TABLES,且权限缓存机制会让手动改表后不立即生效,还可能因表结构变更(如 8.0 的角色模型)导致权限异常。
典型现象:执行 INSERT INTO mysql.db 后,SHOW GRANTS FOR 'tenant_b'@'...' 查不到新权限,应用连上去却报 Access denied for user。
- 始终用
GRANT/REVOKE管理权限,它们会自动刷新内存缓存并校验语法 - 执行完记得
FLUSH PRIVILEGES(仅在直接改系统表后需要,正常GRANT不用) - MySQL 8.0+ 推荐配合角色(
CREATE ROLE)复用权限模板,比如先建role_tenant_ro,再GRANT role_tenant_ro TO 'tenant_c'@'...'
连接时强制指定 database 参数,防租户间库名混淆
很多 ORM 或连接池(如 HikariCP)默认不设初始库,连接建立后靠 USE tenant_x 切库。这在多租户下极危险:如果某次查询漏写库前缀,又没开启 sql_mode=ONLY_FULL_GROUP_BY 等严格模式,可能意外读到同名表的其他租户数据。
真实踩坑案例:Laravel 的 DB::table('users') 在未指定连接库时,查到了 tenant_a.users,但本该查 tenant_b.users。
- 应用连接串必须显式带
database=tenant_a(如 JDBC:jdbc:mysql://localhost:3306/tenant_a?...) - 禁止在代码里动态拼接
USE xxx或执行跨库查询(如SELECT * FROM tenant_b.logs) - 可在 MySQL 配置中加
init_connect='USE tenant_a'作为兜底,但不能替代连接串设置(init_connect 对 SUPER 用户不生效)
租户库名硬编码在权限里,别依赖应用层路由逻辑
有些方案把租户 ID 存进请求头,由中间件解析后决定连哪个库——这种“逻辑租户”看似灵活,实则把权限边界从数据库下移到了应用层。只要中间件出 bug、配置错、或攻击者伪造 header,隔离就崩了。
更麻烦的是,这类设计让 DBA 完全无法审计权限:你查 SHOW GRANTS,看到的是一堆 'app_gateway'@'%',根本不知道它实际能碰哪些库。
- 租户对应库名必须是固定字符串(如
tenant_001),且与账号名一致('tenant_001'@'...'),方便追踪 - 禁止使用模糊库名如
tenant_%授权(MySQL 不支持 LIKE 匹配库名) - 定期用脚本检查:
SELECT User, Host, Db FROM mysql.db WHERE Db LIKE 'tenant\_%';确保无遗漏或宽泛授权
最难绷的是权限回收——删租户时,人容易只删库,忘了 DROP USER 'tenant_x'@'...'。留着空账号,下次新建同名租户可能被旧权限干扰。这事没法自动化,得进 checklist。


















