应用账号应禁用系统表访问,仅授予业务库最小权限;使用grant usage作为安全起点,显式限制列级访问与存储过程执行,并处理连接池权限缓存问题。

直接禁用系统表访问:别让应用账号碰 mysql、performance_schema、information_schema
应用账号默认不该看到任何系统元数据——这不是“限制”,而是基线安全要求。MySQL 的 information_schema 虽然是只读视图,但泄露表结构、列名、索引甚至部分统计信息(如 TABLES.TABLE_ROWS)可能被用于探测或构造攻击;mysql 库则直接暴露用户、权限、角色等敏感配置;performance_schema 若开放 SELECT,可能被用来监控锁、查询执行计划甚至内存分配,属于典型越权温床。
实操建议:
- 新建应用用户后,**不执行任何 GRANT 之前,先显式收回全局权限**:
REVOKE ALL PRIVILEGES ON *.* FROM 'app_user'@'172.16.0.%';
避免因历史脚本残留或权限叠加导致意外继承 - 仅授予业务库所需表的权限,例如:
GRANT SELECT, INSERT, UPDATE ON app_db.orders TO 'app_user'@'172.16.0.%';
注意:这里没写mysql或performance_schema,就等于完全禁止访问 - 检查是否误授了系统库权限:
SELECT User, Host, Db FROM mysql.db WHERE Db IN ('mysql', 'performance_schema', 'information_schema');若返回结果,立刻DROP USER或REVOKE
为什么 GRANT USAGE ON *.* 是安全起点,而不是摆设
很多人以为 GRANT USAGE 没用,其实它在 MySQL 8.0+ 中是“零权限连接占位符”——允许用户通过认证建立连接,但禁止任何操作。这对应用账号初始化极关键:先建用户、设密码、绑 IP,再单独授权业务库,中间不会出现“已连上却权限过大”的空窗期。
常见错误现象:
- 用
GRANT SELECT ON app_db.* TO 'app'@'%'创建用户时,MySQL 8.0+ 会报错ERROR 1410 (42000): You are not allowed to create a user with GRANT—— 因为没提前CREATE USER - 直接
GRANT ALL ON *.*再慢慢收权,结果忘了REVOKE系统库,权限实际仍生效(MySQL 权限是叠加的,REVOKE必须精准匹配GRANT的范围)
正确流程:
CREATE USER 'app_user'@'172.16.0.%' IDENTIFIED BY 'strong_pwd' REQUIRE SSL;<br>GRANT USAGE ON *.* TO 'app_user'@'172.16.0.%';<br>GRANT SELECT, INSERT ON app_db.users TO 'app_user'@'172.16.0.%';<br>FLUSH PRIVILEGES;
应用层 WHERE 过滤失效时,数据库层如何兜底防跨租户读取
权限控制到库/表级,只能防“查整个表”,但防不了“查别人的数据”。比如多租户场景下,SELECT * FROM orders WHERE user_id = ? 如果应用拼错了参数或没绑定,就可能漏掉 WHERE,变成全表扫描。这时候,仅靠账号权限毫无意义。
实操建议:
- 对敏感业务表,**显式限定可访问的列**,避免密码、手机号等字段被整表 SELECT:
GRANT SELECT (id, order_no, status, created_at) ON app_db.orders TO 'app_user'@'172.16.0.%';
- 用存储过程封装带租户校验的逻辑,再只授
EXECUTE权限:CREATE PROCEDURE app_db.get_user_orders(IN uid INT) BEGIN SELECT * FROM orders WHERE user_id = uid; END;<br>GRANT EXECUTE ON PROCEDURE app_db.get_user_orders TO 'app_user'@'172.16.0.%';
- 禁用
SHOW DATABASES,防止应用通过它枚举出其他租户库名:REVOKE SHOW DATABASES ON *.* FROM 'app_user'@'172.16.0.%';
容易被忽略的权限缓存与连接池陷阱
改完权限后,老连接不会自动失效——MySQL 权限检查只在连接建立时做一次。如果应用用了连接池(如 HikariCP、Druid),即使你 REVOKE DROP ON *.*,池里已存在的连接仍能执行 DROP TABLE,直到它超时或被回收。
性能与兼容性影响:
-
FLUSH PRIVILEGES不强制刷新活跃连接权限,它只重载权限表到内存;真正生效需断开重连 - 某些 ORM(如 Django 早期版本)会在连接初始化时执行
SELECT DATABASE()或SHOW TABLES,若你禁了information_schema访问,这些语句会失败,需配合白名单或调整 ORM 配置 - MySQL 8.0+ 的角色(
ROLE)默认启用activate_all_roles_on_login,意味着SET DEFAULT ROLE后所有角色权限都会激活——必须关掉:SET PERSIST activate_all_roles_on_login = OFF;
最稳妥的做法是:权限变更后,主动触发连接池滚动更新(如重启服务、或调用 evict() 清空连接),而不是等超时。











