REVOKE执行成功不等于权限立即失效,因已连会话仍用登录时的权限快照,须断开重连才生效;撤销须严格匹配原始授权范围,且需排查列级权限、角色、代理及动态权限等残留来源。

REVOKE 执行成功 ≠ 权限立即失效,用户当前连接仍持有登录时加载的权限快照,必须断开重连才能应用新权限。
已建立连接不读新权限是 MySQL 的设计机制
MySQL 在用户认证完成、连接建立的那一刻,就把该用户在 mysql.db、mysql.tables_priv 等系统表中匹配到的所有权限“快照”固化进会话内存。后续所有 REVOKE 操作只修改系统表,不会触碰任何活跃连接的权限状态。
这意味着:即使你刚执行了 REVOKE SELECT ON appdb.* FROM 'appuser'@'%',那个已经连着的 JDBC 连接或 Navicat 会话仍能正常执行 SELECT * FROM appdb.users——它不是绕过检查,而是压根没去查新权限。
-
SHOW GRANTS在旧连接里返回的仍是初始化时的快照,不能用来验证REVOKE是否生效 -
FLUSH PRIVILEGES在 MySQL 8.0+ 对已存在连接完全无效,它只影响新连接的加载行为 - 连接池(如 HikariCP)里的空闲连接可能持续数分钟甚至更久,不主动驱逐就一直带着旧权限
撤销语句没真正匹配上原始授权
MySQL 的 REVOKE 不推导、不模糊匹配,必须和当初 GRANT 的对象范围、主机名、数据库名、大小写完全一致,否则“执行成功”但实际未撤掉任何权限。
常见错配场景:
- 当初授权是
GRANT SELECT ON `my-app`.* TO 'u'@'10.0.2.%',撤销时写成REVOKE SELECT ON myapp.*(库名带连字符且没加反引号)或'u'@'%'(主机名不一致) - 用户实际登录的是
'u'@'localhost',但你撤销的是'u'@'127.0.0.1'—— MySQL 内部把localhost视为 socket 连接,匹配规则不同 - 误用
REVOKE ALL PRIVILEGES ON *.*去清理单个库权限,结果漏掉了appdb.t1上单独授过的INSERT
验证方式:先运行 SHOW GRANTS FOR 'u'@'h',逐字比对每一行的 ON 后面部分,再照抄写进 REVOKE 语句。
残留权限藏在细粒度系统表或角色里
撤销 ON db_name.* 不会动 mysql.columns_priv 里的列级权限,也不会解除角色继承来的权限。用户可能仍能操作,只是来源变了。
必须手动排查:
- 查列级权限:
SELECT * FROM mysql.columns_priv WHERE User = 'u' AND Host = 'h' AND Db = 'db_name' - 查表级单独授权:
SELECT * FROM mysql.tables_priv WHERE User = 'u' AND Host = 'h' AND Db = 'db_name' - 查角色绑定:
SELECT * FROM mysql.role_edges WHERE TO_USER = 'u' AND TO_HOST = 'h' - 查代理权限:
SELECT * FROM mysql.proxies_priv WHERE User = 'u' AND Host = 'h'
这些地方只要有一条残留记录,对应权限就依然有效;清理需对应执行 REVOKE 或直接 DELETE(操作前务必备份)。
动态权限(如 SYSTEM_VARIABLES_ADMIN)没清干净
MySQL 8.0+ 的动态权限(如 SYSTEM_VARIABLES_ADMIN、SESSION_VARIABLES_ADMIN、CONNECTION_ADMIN)是独立开关,彼此不覆盖。只撤一个,其他仍可绕过。
典型绕过路径:
- 撤了
SYSTEM_VARIABLES_ADMIN,但没撤SESSION_VARIABLES_ADMIN→ 用户仍可用SET PERSIST修改全局变量 - 漏掉
CONNECTION_ADMIN→ 它隐式包含所有变量控制能力,且还能KILL线程、CREATE USER
安全做法是三者一起撤:REVOKE SYSTEM_VARIABLES_ADMIN, SESSION_VARIABLES_ADMIN, CONNECTION_ADMIN ON *.* FROM 'u'@'h',并确认 SHOW GRANTS 输出里彻底消失。
真正难的从来不是写出那条 REVOKE 语句,而是搞清权限到底从哪来、现在在哪生效、连接是否还活着——不查系统表、不杀连接、不换账号验证,就永远不知道权限是不是真被收走了。











