grant 之后真的需要执行 flush privileges 吗?如果没有执行这个 flush 命令的话,赋权语句真的不能生效吗?
先创建一个用户:
create user 'ua'@'%' identified by 'pa';
这条语句的逻辑是创建一个用户’ua’@’%’,密码是 pa。注意,在 MySQL 里面,用户名 (user)+ 地址 (host) 才表示一个用户,因此 ua@ip1 和 ua@ip2 代表的是两个不同的用户。
这条命令做了两个动作:
下图就是这个时刻用户 ua 在 user 表中的状态。
在 MySQL 中,用户权限是有不同的范围的。接下来,我就按照用户权限范围从大到小的顺序依次和你说明。
全局权限,作用于整个 MySQL 实例,这些权限信息保存在 mysql 库的 user 表里。给用户 ua 赋一个最高权限的话是这么写的:
grant all privileges on *.* to 'ua'@'%' with grant option;
这个 grant 命令做了两个动作:
在这个 grant 命令执行完成后,如果有新的客户端使用用户名 ua 登录成功,MySQL 会为新连接维护一个线程对象,然后从 acl_users 数组里查到这个用户的权限,并将权限值拷贝到这个线程对象中。之后在这个连接中执行的语句,所有关于全局权限的判断,都直接使用线程对象内部保存的权限位。
基于上面的分析我们可以知道:
除了全局权限,MySQL 也支持库级别的权限定义。如果要让用户 ua 拥有库 db1 的所有权限,可以执行下面这条命令:
grant all privileges on db1.* to 'ua'@'%' with grant option;
基于库的权限记录保存在 mysql.db 表中,在内存里则保存在数组 acl_dbs 中。这条 grant 命令做了如下两个动作:
下图就是这个时刻用户 ua 在 db 表中的状态。
每次需要判断一个用户对一个数据库读写权限的时候,都需要遍历一次 acl_dbs 数组,根据 user、host 和 db 找到匹配的对象,然后根据对象的权限位来判断。
也就是说,grant 修改 db 权限的时候,是同时对磁盘和内存生效的。
grant 操作对于已经存在的连接的影响,在**全局权限和基于 db 的权限效果是不同的。**接下来,我们做一个对照试验来分别看一下。
需要说明的是,图中 set global sync_binlog 这个操作是需要 super 权限的。
可以看到,虽然用户 ua 的 super 权限在 T3 时刻已经通过 revoke 语句回收了,但是在 T4 时刻执行 set global 的时候,权限验证还是通过了。这是因为 super 是全局权限,这个权限信息在线程对象中,而 revoke 操作影响不到这个线程对象。
而在 T5 时刻去掉 ua 对 db1 库的所有权限后,在 T6 时刻 session B 再操作 db1 库的表,就会报错“权限不足”。这是因为 acl_dbs 是一个全局数组,所有线程判断 db 权限都用这个数组,这样 revoke 操作马上就会影响到 session B。
这里在代码实现上有一个特别的逻辑,如果当前会话已经处于某一个 db 里面,之前 use 这个库的时候拿到的库权限会保存在会话变量中。
你可以看到在 T6 时刻,session C 和 session B 对表 t 的操作逻辑是一样的。但是 session B 报错,而 session C 可以执行成功。这是因为 session C 在 T2 时刻执行的 use db1,拿到了这个库的权限,在切换出 db1 库之前,session C 对这个库就一直有权限。
除了 db 级别的权限外,MySQL 支持更细粒度的表权限和列权限。其中,表权限定义存放在表 mysql.tables_priv 中,列权限定义存放在表 mysql.columns_priv 中。这两类权限,组合起来存放在内存的 hash 结构 column_priv_hash 中。
这两类权限的赋权命令如下:
create table db1.t1(id int, a int);
grant all privileges on db1.t1 to 'ua'@'%' with grant option;
GRANT SELECT(id), INSERT (id,a) ON mydb.mytbl TO 'ua'@'%' with grant option;
跟 db 权限类似,这两个权限每次 grant 的时候都会修改数据表,也会同步修改内存中的 hash 结构。因此,对这两类权限的操作,也会马上影响到已经存在的连接。
看到这里,你一定会问,看来 grant 语句都是即时生效的,那这么看应该就不需要执行 flush privileges 语句了呀。
答案也确实是这样的。
flush privileges 命令会清空 acl_users 数组,然后从 mysql.user 表中读取数据重新加载,重新构造一个 acl_users 数组。也就是说,以数据表中的数据为准,会将全局权限内存数组重新加载一遍。
同样地,对于 db 权限、表权限和列权限,MySQL 也做了这样的处理。
也就是说,如果内存的权限数据和磁盘数据表相同的话,不需要执行 flush privileges。而如果我们都是用 grant/revoke 语句来执行的话,内存和数据表本来就是保持同步更新的。
因此,正常情况下,grant 命令之后,没有必要跟着执行 flush privileges 命令。
那么,flush privileges 是在什么时候使用呢?显然,当数据表中的权限数据跟内存中的权限数据不一致的时候,flush privileges 语句可以用来重建内存数据,达到一致状态。
这种不一致往往是由不规范的操作导致的,比如直接用 DML 语句操作系统权限表。我们来看一下下面这个场景:
可以看到,T3 时刻虽然已经用 delete 语句删除了用户 ua,但是在 T4 时刻,仍然可以用 ua 连接成功。原因就是,这时候内存中 acl_users 数组中还有这个用户,因此系统判断时认为用户还正常存在。
在 T5 时刻执行过 flush 命令后,内存更新,T6 时刻再要用 ua 来登录的话,就会报错“无法访问”了。
直接操作系统表是不规范的操作,这个不一致状态也会导致一些更“诡异”的现象发生。比如,前面这个通过 delete 语句删除用户的例子,就会出现下面的情况:
可以看到,由于在 T3 时刻直接删除了数据表的记录,而内存的数据还存在。这就导致了:
grant 语句会同时修改数据表和内存,判断权限的时候使用的是内存数据。因此,规范地使用 grant 和 revoke 语句,是不需要随后加上 flush privileges 语句的。
flush privileges 语句本身会用数据表的数据重建一份内存权限数据,所以在权限数据可能存在不一致的情况下再使用。而这种不一致往往是由于直接用 DML 语句操作系统权限表导致的,所以我们尽量不要使用这类语句。