“GRANT ALL ON .”会导致Schema隔离失效,因其赋予全局权限,使后续对单Schema的REVOKE在MySQL 5.7等版本可能被缓存绕过,且隐式授予SHOW DATABASES等高危权限,违反最小权限原则;正确做法是仅对目标Schema显式授权,禁用WITH GRANT OPTION,并确保库已存在、host精确匹配。

为什么“GRANT ALL ON *.*”会导致Schema隔离失效
因为MySQL权限是叠加生效的,一旦授予了全局权限,后续对单个Schema的REVOKE在部分版本(如5.7)可能被缓存绕过,导致tenant_a_app仍能访问tenant_b_prod。更危险的是,GRANT ALL ON *.*会隐式赋予SHOW DATABASES、PROCESS等管理权限,用户连上就能看到所有租户库名——这已违反最小权限原则。
正确做法是跳过全局授权,从库级开始:只对目标Schema显式GRANT,且不带WITH GRANT OPTION(避免租户自授权限)。
- 必须先确保
tenant_a_prod这个database已存在,否则GRANT SELECT ON `tenant_a_prod`.*会报错ERROR 1146 (42S02)(实际是库不存在,不是表) - host必须精确匹配,
'tenant_a_app'@'%'和'tenant_a_app'@'10.0.2.%'是两个账号,后者更安全 - 执行完
GRANT后无需FLUSH PRIVILEGES——只有直接改mysql.user表才需要
如何验证Schema级权限是否真正生效
别信SHOW GRANTS输出,要实测连接行为。用目标账号直连并执行三类命令:
-
mysql -utenant_a_app -p -e "SHOW DATABASES;"—— 正常应只显示tenant_a_prod和information_schema(后者总是可见,但内容受限) -
mysql -utenant_a_app -p -e "USE tenant_b_prod;"—— 应报错Access denied for user 'tenant_a_app'@'%' to database 'tenant_b_prod' -
mysql -utenant_a_app -p -e "SELECT * FROM mysql.user;"—— 应报错Table 'mysql.user' doesn't exist(因无mysql库权限)
关键点:CURRENT_USER()返回值必须是'tenant_a_app'@'xxx',如果返回'tenant_a_app'@'localhost',说明客户端IP被DNS反解扭曲,需关掉skip_name_resolve。
MySQL 5.7 和 8.0 在Schema隔离上的实操差异
8.0支持角色(CREATE ROLE),可先建tenant_reader角色,再把SELECT, SHOW VIEW批量赋予角色,最后GRANT tenant_reader TO 'tenant_a_app'@'%'。这样后期调整权限只需改角色,不用遍历每个租户账号。
5.7没角色,只能脚本批量执行GRANT,容易漏或重复。更要命的是:5.7的REVOKE不能撤回由GRANT ALL ON *.*隐式继承的权限,所以5.7环境下必须从零开始建账号,绝不能复用已有高权限账号。
- 8.0默认开启
sql_mode = STRICT_TRANS_TABLES,租户建表时若字段定义不严谨(比如VARCHAR(255)插超长字符串),会直接报错而非截断——这反而利于早期发现数据质量问题 - 5.7下
GRANT EXECUTE ON `tenant_a_prod`.*仅控制存储过程调用,8.0则扩展到函数、事件等,统一归入EXECUTE权限
应用层最容易绕过Schema隔离的三个坑
权限配置再严,应用代码写错照样白搭。最常见的是硬编码库名拼接SQL:
- ORM里写
db.table('tenant_b_prod.users')而不是绑定当前租户上下文,等于主动越权 - 连接串漏掉
database=tenant_a_prod参数,靠USE tenant_a_prod初始化,但连接池复用时可能残留上一个租户的USE状态 - 备份脚本用
SHOW DATABASES动态获取库名来遍历,结果把tenant_c_prod也拖进备份集——应从运维白名单配置读取,而非依赖数据库元信息
真正难的不是配权限语句,而是让每个开发理解:Schema隔离是数据库+应用双控的结果,缺一不可。运维给的GRANT只是底线,不是保险箱。


















