必须用独立数据库、独立账号、角色封装和连接串硬编码database四者缺一不可;grant必须写成grant select on tenant_a_prod.,反引号和.不可省略,host需限制子网,mysql 8.0须用create role统一授予权限并revoke show databases。

必须用独立数据库 + 独立账号 + 角色封装 + 连接串硬编码 database,四者缺一不可;靠应用层拼库名或共享账号,等于把租户数据摊在桌上任人翻。
GRANT 必须写成 GRANT SELECT ON `tenant_a_prod`.*,漏反引号或 .* 就等于没授权
MySQL 权限系统不认裸名 tenant_a_prod:它会被解析为全局对象,而你没授过全局权限,结果是连 USE tenant_a_prod 都被拒绝。常见错误写法包括 GRANT SELECT ON tenant_a_prod(缺 .*)或 GRANT SELECT ON tenant-a-prod.*(缺反引号,短横线导致语法报错)。
正确操作要点:
- 反引号
`必须包裹库名,尤其当库名含短横线、数字开头(如`tenant-001`)时 -
.*不可省略——漏掉就只拿到USAGE权限,连SHOW TABLES都被拒 - host 限制到子网更安全,例如
'tenant_a_app'@'10.20.30.%',而非'%'
用 CREATE ROLE 统一管理权限模板,避免每个租户重复写 GRANT
MySQL 8.0 的角色机制不是锦上添花,而是运维刚需。5.7 只能靠脚本批量 GRANT,稍有疏漏就授过头或漏收权限;8.0 可先定义角色,再批量赋权,逻辑清晰且可审计。
实操建议:
- 创建角色并绑定库级权限:
CREATE ROLE 'tenant_reader'; GRANT SELECT ON `tenant_%`.* TO 'tenant_reader'; - 将角色赋予具体账号:
GRANT 'tenant_reader' TO 'svc-order-app'@'10.20.30.%'; - 激活角色(用户登录后生效):
SET DEFAULT ROLE ALL TO 'svc-order-app'@'10.20.30.%'; - 禁止给角色授
PROCESS、SUPER、FILE等高危权限,哪怕只读场景也不行
连接串必须带 database=tenant_a_prod,禁用 USE 切库
很多微服务用 HikariCP 或 Druid,初始化时不设 database 参数,靠运行时执行 USE tenant_a_prod,这在多租户下极其危险:连接复用时,上一个请求残留的库上下文可能影响下一个租户查询,ORM 漏写表前缀就会跨库读取同名表。
可靠做法:
- JDBC URL 显式写死:
jdbc:mysql://db:3306/?useSSL=false&database=tenant_a_prod - 不要用
connectionInitSqls=["USE tenant_a_prod"]做兜底——它不解决连接复用问题 - HikariCP 必须配合
ConnectionCustomizer.onAcquire()执行USE tenant_a_prod并校验SELECT DATABASE()返回值是否匹配 - Druid 配置
initConnectionSqls后,仍需在获取连接时检查当前库名
必须显式 REVOKE SHOW DATABASES ON *.*,否则租户能看到所有库名
这不是漏洞,是 MySQL 设计行为:只要账号存在且有任意库权限,SHOW DATABASES 就列出全部库名。即使你只给了 `tenant_a_prod`.* 权限,它照样能看到 tenant_b_prod、tenant_c_staging —— 这是元信息泄露的第一跳。
补救命令只有一条,且必须执行:
REVOKE SHOW DATABASES ON *.* FROM 'svc-order-app'@'10.20.30.%';- 别指望
REVOKE ALL能收回这个权限——SHOW DATABASES是独立权限项 - 备份脚本若需遍历租户库,应从白名单配置读取库名,而非依赖
SHOW DATABASES - 禁止应用层把
SHOW DATABASES结果暴露给前端或日志
最易被忽略的是连接生命周期管理:连接池未清理旧连接、事务未及时提交导致会话残留、ORM 自动重连时复用了错误的 database 上下文——这些不会报错,但会让隔离策略在静默中失效。











