应直接精准REVOKE冗余权限,而非“先全授再逐步回收”,因MySQL权限叠加且角色/细粒度权限易残留;须查显式授权、角色权限及mysql.db隐式权限,重点清理GRANT OPTION等高危项,并通过新连接验证真实权限。

直接删掉冗余权限,别想着“先保留再慢慢收”,越拖越难清——MySQL 的权限是叠加的,GRANT ALL ON *.* 一旦执行,后续 REVOKE 很可能撤不干净,尤其在老版本或混用角色时。
确认用户当前真实权限(不是你以为的)
很多人以为 SHOW GRANTS FOR 'app'@'10.20.30.%' 返回的就是全部权限,其实不是。MySQL 8.0+ 默认不显示角色继承的权限,也不反映 mysql.db 表里隐式授予的库级权限。
- 查显式授权:
SHOW GRANTS FOR 'app'@'10.20.30.%' - 查角色权限(如果用了角色):
SHOW GRANTS FOR 'app'@'10.20.30.%' USING 'web_reader' - 查隐式库级权限:
SELECT Db, Select_priv, Insert_priv FROM mysql.db WHERE User='app' AND Host='10.20.30.%' - 重点盯
GRANT OPTION、FILE、SUPER、SHUTDOWN这类高危权限项,普通应用账号绝不该有
用 REVOKE 精确回收,而不是 DROP USER 重建
重建用户看似彻底,但容易漏掉连接池缓存、应用配置里的旧密码、或依赖该用户名的存储过程定义者(报错 ERROR 1449)。优先用 REVOKE 收权,更可控。
- 先收全局权限:
REVOKE ALL PRIVILEGES, GRANT OPTION ON *.* FROM 'app'@'10.20.30.%' - 再收库级权限(如果之前授过
ON shop.*):REVOKE ALL ON shop.* FROM 'app'@'10.20.30.%' - 特别注意:
REVOKE SELECT ON information_schema.*在 MySQL 8.0.29+ 才真正生效;老版本需配合show_compatibility_56=OFF才能限制元数据访问 - 执行后必须
FLUSH PRIVILEGES,否则内存缓存不更新
改用角色(ROLE)封装权限,避免权限再次漂移
直接 GRANT 给用户,权限会随时间发散——开发加个新表、DBA 临时给个 ALTER,没人记得清理。角色才是防越权的长效机制。
- 建专用角色:
CREATE ROLE 'order_writer', 'product_reader' - 只授最小必要权限:
GRANT INSERT, UPDATE ON shop.orders TO 'order_writer',绝不用ON shop.* - 把角色给用户:
GRANT 'order_writer' TO 'app'@'10.20.30.%' - 必须启用默认角色:
SET DEFAULT ROLE 'order_writer' TO 'app'@'10.20.30.%',否则登录后权限不生效 - 后续权限变更只需改角色,用户侧完全无感
验证是否真收住了——连上去跑业务语句
别只测 SHOW DATABASES 或 SELECT 1。越权风险藏在具体操作里,比如误删整张表、导出敏感字段、跨库写入。
- 从真实客户端 IP 连:
mysql -u app -p -h10.20.30.5(不能只在 localhost 试) - 立刻执行典型业务语句:
DELETE FROM shop.orders WHERE id = 123(应被拒绝),SELECT phone FROM users(若没授该表或列,应报错) - 尝试越权操作:
DROP TABLE shop.products、SELECT * FROM mysql.user,确认返回ERROR 1142或ERROR 1044 - 检查错误日志:
tail -f /var/log/mysql/error.log,确认没有因权限不足导致的连接中断或静默失败
最常被忽略的一点:权限回收后,应用连接池可能还持有着旧会话的权限上下文,重启应用服务比等连接超时更可靠。另外,GRANT OPTION 一旦存在,用户就能自己创建子账号并授高权限,这种链式风险比单个账号越权更难追溯。


















