最稳妥的mysql多租户隔离方式是为每个租户配独立数据库和独立账号,需严格grant精确到db_name.*、禁用show databases、连接串强制指定database,并由专用服务统一建库建号且白名单校验库名。

直接给每个租户配独立数据库 + 独立账号,是 MySQL 多租户权限隔离里最稳妥、最易审计、也最容易出错的路——*只要 GRANT 写对、SHOW DATABASES 收紧、连接串写死 database,就能拦住 95% 的越权访问;但漏掉一个 . 或多授一个 SHOW DATABASES,整套就形同虚设。**
GRANT 必须精确到 database_name.*,不能省略末尾的 .*
这是最常踩的坑:写成 GRANT SELECT ON tenant_abc TO 'u1'@'%' 看似合理,其实权限不生效。MySQL 权限系统只认 tenant_abc.* 这种格式,tenant_abc 单独出现时会被当作“全局权限对象”,而你没授过全局权限,结果就是用户连 USE tenant_abc 都被拒绝。
- 正确写法:
GRANT SELECT, INSERT, UPDATE, DELETE ON `tenant_abc`.* TO 'tenant_abc_app'@'10.20.30.%' - 反引号
`必须加,尤其当库名含数字或短横线(如`tenant-123`) - host 尽量不用
'%',限制到子网更安全,比如'10.20.30.%' - 执行完不用
FLUSH PRIVILEGES—— 正常 GRANT 会自动刷新内存缓存
必须显式收回 SHOW DATABASES 权限
即使租户账号只能访问 tenant_x 库,它默认仍能在 SHOW DATABASES 里看到所有库名。这不是权限没设好,而是 MySQL 的设计:只要账号存在且有任意库权限,SHOW DATABASES 就列出全部库(除非用 ProxySQL 过滤或升级到 8.0+ 配 information_schema_restricted)。
- 补救命令:
REVOKE SHOW DATABASES ON *.* FROM 'tenant_x_app'@'10.20.30.%' - 别指望靠
REVOKE ALL ON *.*—— 它不包含SHOW DATABASES,这个权限得单独收 - MySQL 5.7 及更早版本无法真正隐藏库名,只能靠应用层不展示
SHOW DATABASES结果
连接时必须指定 database 参数,不能依赖 USE 切换
很多 ORM 或连接池(如 HikariCP)默认不设初始库,靠运行时执行 USE tenant_x。这在多租户下极危险:连接复用时状态残留,或者某次查询漏写库前缀,就可能读到同名表的其他租户数据。
- JDBC 连接串中强制加:
?useSSL=false&database=tenant_x - Druid 可配
connectionInitSqls=["USE tenant_x"],但需确保每次获取连接都重置 - HikariCP 没内置机制,得用
ConnectionCustomizer.onAcquire手动执行USE tenant_x - 绝对不要在 SQL 里拼库名:
"SELECT * FROM " + tenantDb + ".orders"是注入高危点
建库建号必须由专用服务统一执行,禁用租户自建库
一旦授予 CREATE DATABASE 权限,租户就能建任意库名,包括撞名系统库(如 mysql)、绕过前缀(如建 prod_users),甚至故意建同名库触发异常。真正的隔离,始于建库那一刻的控制。
- 建库语句必须带字符集:
CREATE DATABASE `tenant_x` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci - 租户标识(如
tenant_x)必须白名单校验:只允许字母、数字、下划线,长度 ≤32,禁用mysql、information_schema等敏感词 - 建库建号操作应由运维脚本或管理服务完成,账号用 root 或专用 DDL 账号,该账号不应有租户表级权限
- 建完立刻绑定账号 + 授权 + 收回
CREATE DATABASE和SHOW DATABASES
复杂点不在授权语法本身,而在于「权限边界」和「连接上下文」的双重对齐:账号能访问哪个库,和连接实际连到了哪个库,必须始终一致。任何一方脱钩,比如账号绑了 tenant_a 却连了 tenant_b 的连接串,或反过来,都会让隔离失效——而这种错往往只在压测或上线后才暴露。











