唯一可靠路径是每个租户独占一个database,且授权必须写成GRANT ... ON tenant_x.:反引号防语法错误,.绑定所有表权限;连接串须指定database=参数固化上下文,禁用USE切换,同时禁用SHOW DATABASES及全局权限。

MySQL 中不同租户之间要实现数据库权限硬隔离,唯一可靠路径是:每个租户独占一个 database,且授权语句必须写成 GRANT ... ON `tenant_x`.*——漏掉反引号或 .*,权限就等于没给。
GRANT 语句必须带反引号和 .* 才生效
MySQL 权限系统不认裸名 tenant_x,它会被当作全局对象解析;而 GRANT SELECT ON tenant_x(没写 .*)只授予库级 USAGE 权限,连 SHOW TABLES 都被拒绝。
-
`tenant_x`必须加反引号:租户库名含数字、短横线(如tenant-123)时,不加直接语法报错 -
.*绝对不能省:这是把权限绑定到“该库下所有表”的唯一写法,漏写即无表级权限 - 禁止用
ON *.*或ON tenant_x替代——前者越权,后者无效 - 执行后务必校验:
SHOW GRANTS FOR 'tenant_a_app'@'%'确认输出里有ON `tenant_a_prod`.*
连接串必须指定 database= 参数,禁用 USE 切库
USE tenant_x 是会话级命令,连接复用时极易残留旧上下文,导致后续请求查到其他租户数据。驱动层无法保证每次重连都重置 schema,所以必须靠连接参数固化上下文。
- JDBC URL 必须写成
jdbc:mysql://host:3306/?useSSL=false&database=tenant_a_prod(注意是&database=,不是?database=) - 旧版
mysql-connector-java的?database=在重连时大概率丢失,现象是偶发跨库查询 - Druid 配
connectionInitSqls=["USE tenant_a_prod"]不够,还得在获取连接后执行SELECT DATABASE()校验返回值 - HikariCP 没内置 init SQL,必须用
ConnectionCustomizer.onAcquire()主动执行USE并校验
租户账号必须禁用 SHOW DATABASES 和全局权限
MySQL 默认只要账号有任意库权限,SHOW DATABASES 就会列出全部库名——这不是漏洞,是设计行为。真正危险的是权限收得松,或应用层把库名当变量拼进 SQL。
- 建号后立刻执行:
REVOKE SHOW DATABASES ON *.* FROM 'tenant_a_app'@'%'(心理防线,非技术必需但建议做) - 再执行:
REVOKE ALL PRIVILEGES ON *.* FROM 'tenant_a_app'@'%',然后FLUSH PRIVILEGES - host 写死子网更安全,例如
'tenant_a_app'@'10.20.30.%',比'%'少一个攻击面 - 绝对不要给
PROCESS、FILE、SUPER这类高危权限,哪怕只读场景也不行
备份与 ORM 必须按租户绑定 database,禁用动态拼接
任何把租户库名当字符串拼进 SQL 或配置的行为,都是硬编码注入点。MySQL 不会校验这个字符串是否真属于当前用户,只要权限开了,就能绕过隔离。
- ORM(如 MyBatis、Hibernate)必须配多数据源,每个租户一个独立
DataSource,不能靠运行时动态切换database - 备份脚本遍历租户库时,必须从白名单配置读取库名列表,而非执行
SHOW DATABASES动态获取 - 禁止在代码里写:
"SELECT * FROM " + tenantDb + ".orders"——这等于把库名变成注入入口 - mysqldump 必须按租户分库导出:
mysqldump -u backup_user -p'xxx' tenant_a_prod > /backup/tenant_a/tenant_a_prod_$(date +%F).sql
最容易被忽略的点是:权限语句中反引号和 .* 缺一不可,以及连接串里 database= 参数在重连时的实际稳定性。这两处一旦出错,隔离就形同虚设,而且问题往往偶发、难复现、审计日志里还看不出异常。


















