在MySQL中,grant语句是用来给用户赋权的。
1 | create user 'ua'@'%' identified by 'pa'; |
这条命令做了两个动作:
- 磁盘上,往mysql.user表里插入一行,由于没有指定权限,所以这行数据上所有表示权限的字段的值都是N;
- 内存里,往数组acl_users里插入一个acl_user对象,这个对象的access字段值为0。
MySQL中,用户权限是有不同的范围的。
全局权限
全局权限,作用于整个MySQL实例,这些权限信息保存在mysql库的user表里。内存里则保存在数组acl_users中。
1 | grant all privileges on *.* to 'ua'@'%' with grant option; |
这个grant命令做了两个动作:
- 磁盘上,将mysql.user表里,用户‘ua’@’%’这一行的所有表示权限的字段的值都修改为’Y’;
- 内存里,从数组acl_users中找到这个用户对应的对象,将access值(权限位)修改为二进制的”全1”。
在这个grant命令执行完成后,如果有新的客户端使用用户名ua登录成功,MySQL会为新连接维护一个线程对象,然后从acl_users数组里查到这个用户的权限,并将权限值拷贝到这个线程对象中。之后在这个连接中执行的语句,所有关于全局权限的判断,都直接使用线程对象内部保存的权限位。
- grant命令对于全局权限,同时更新了磁盘和内存。命令完成后即时生效,接下来新创建的链接会使用新的权限。
- 对于一个已经存在的连接,它的全局权限不受grant命令的影响。
一般在生产环境上要合理控制用户权限的范围。如果一个用户有所有权限,一般就不应该设置为所有ip地址都可以访问。
1 | revoke all privileges on *.* from 'ua'@'%'; |
这个revoke命令做了两个动作:
- 磁盘上,将mysql.user表里,用户‘ua’@’%’这一行的所有表示权限的字段的值都修改为’N’;
- 内存上,从数组acl_users中找到这个用户对应的对象,将access的值修改为0。
db权限
MySQL支持库级别的权限定义。基于库的权限记录保存在mysql.db表中,在内存里则保存在数组acl_dbs中。
1 | grant all privileges on db1.* to 'ua'@'%' with grant option; |
这条grant命令做了两个动作:
- 磁盘上,往mysql.db表中插入了一行记录,所有权限位字段设置为’Y’;
- 内存里,增加一个对象到数组acl_dbs中,这个对象的权限位为”全1”。
每次需要判断一个用户对一个数据库读写权限的时候,都需要遍历一次acl_dbs数组,根据user、host和db三个字段找到匹配的对象,然后根据对象的权限位来判断。
- grant修改db权限的时候,是同时对磁盘和内存生效的。
全局权限与db权限对比
grant操作对于已经存在的连接的影响,在全局权限和基于db的权限效果是不同的。
sessionA | sessionB | sessionC | |
---|---|---|---|
T1 | connect(root,root)create database db1;create user ‘ua’@’%’ identified by ‘pa’;grant super on *.* to ‘ua’@’%’;grant all privileges on db1.* to ‘ua’@’%’; | ||
T2 | connect(ua,pa)set global sync_binlog=1;(Query OK)create table db1.t(c int);(Query OK) | connect(ua,pa)use db1; | |
T3 | revoke super on *.* from ‘ua’@’%’; | ||
T4 | set global sync_binlog=1;(Query OK)alter table db1.t engine=innodb;(Query OK) | alter table t engine=innodb;(Query OK) | |
T5 | revoke all privileges on db1.* from ‘ua’@’%’; | ||
T6 | set global sync_binlog=1;(Query OK)alter table db1.t engine=innodb;(alter command denied) | alter table t engine=innodb;(Query OK) |
set global sync_binlog这个操作需要super权限。
用户ua的super权限在T3时刻已经通过revoke语句回收了,但是在T4时刻执行set global的时候,权限验证还是通过了。这是因为super是全局权限,这个权限信息在线程对象中,而revoke操作影响不到这个线程对象。
在T5时刻去掉ua对db1库的所有权限后,在T6时刻sessionB再操作db1库的表,就会报错“权限不足”。这是因为acl_dbs是一个全局数组,所有线程判断db权限都用这个数组,这样revoke操作马上就会影响到sessionB。
特别的逻辑,如果当前会话已经处于某一个db里面,之前use这个库的时候拿到的库权限会保存在会话变量中。在T6时刻,sessionC和sessionB对表t的操作逻辑是一样的。但是sessionB报错,而sessionC可以执行成功。这是因为sessionC在T2时刻执行的use db1,拿到了这个库的权限,在切换出db1库之前,sessionC对这个库就一直有权限。
表权限和列权限
MySQL支持更细粒度的表权限和列权限。其中,表权限定义存放在表mysql.tables_priv中,列权限定义存放在表mysql.columns_priv中。这两类权限,组合起来存放在内存的hash结构column_priv_hash中。
1 | create table db1.t1(id int, a int); |
跟db权限类似,这两个权限每次grant的时候都会修改数据表,也会同步修改内存中的hash结构。因此,对这两类权限的操作,也会马上影响到已经存在的连接。
grant语句都是即时生效的,会同时修改数据表和内存,判断权限的时候使用的是内存数据。规范的使用grant和revoke语句,不需要随后执行flush privileges语句。
flush privileges命令会清空acl_users数组,然后从mysql.user表中读取数据重新加载,重新构造一个acl_users数组。以数据表中的数据为准,会将全局权限内存数组重新加载一遍。
同样的,对于db权限、表权限、列权限,MySQL也做了这样的处理。
如果内存的权限数据和磁盘数据表相同的话,不需要执行flush privileges。而如果都是用grant/revoke语句来执行的话,内存和数据表本来就是保持同步更新的。
正常情况下,grant命令之后,没有必要紧跟着执行flush privileges命令。
权限 | 磁盘存储 | 内存存储 | 修改策略 | 作用范围 |
---|---|---|---|---|
全局权限 | 表mysql.user | 数组acl_users | 已存在的连接不生效,新建连接立即生效 | 当前线程 |
db权限 | 表mysql.db | 数组acl_dbs | 所有连接立即生效 | 全局 |
表权限 | 表mysql.tables_priv | 和列权限组合的hash结构column_priv_hash | 所有连接立即生效 | 全局 |
列权限 | 表mysql.columns_priv | 和表权限组合的hash结构column_priv_hash | 所有连接立即生效 | 全局 |
flush privileges使用场景
当数据表中的权限数据跟内存中的权限数据不一致的时候,flush privileges语句可以用来重建内存数据,达到一致状态。(这种不一致往往是由不规范的操作导致的,比如直接用DML语句操作系统权限表)
drop语句同时操作磁盘和内存,能保证全选数据一致。delete语句只删除磁盘数据。
clientA | clientB | |
---|---|---|
T1 | connect(root,root)create user ‘ua’@’%’ identified by ‘pa’; | |
T2 | connect(ua,pa)(connect ok)disconnect | |
T3 | delete from mysql.user where user=’ua’; | |
T4 | connect(ua,ua)(connect ok)disconnect | |
T5 | flush privileges; | |
T6 | connect(ua,pa)(Access Denied) |
T3时刻虽然已经用delete语句删除了用户ua,但是在T4时刻,仍然可以用ua连接成功。原因就是,这时内存中acl_users数组中还有这个用户,因此系统判断时认为用户还正常存在。
在T5时刻执行过flush命令后,内存更新,T6时刻再要用ua来登录的话,就会报错“无法访问”了。
直接操作系统表是不规范的操作,这个不一致状态也会导致一些更“诡异”的现象发生。
clientA | |
---|---|
T1 | connect(root,root)create user ‘ua’@’%’ identified by ‘pa’; |
T2 | |
T3 | delete from mysql.user where user=’ua’; |
T4 | grant super on *.* to ‘ua’@’%’ with grant option;error 1133(42000):Can’t find any matching row in the user table |
T5 | create user ‘ua’@’%’ identified by ‘pa’;error 1396(HY000):Operation create user failed for ‘ua’@’%’ |
由于在T3时刻直接删除了数据表的记录,而内存的数据还在,导致:
- T4时刻给用户ua赋权限失败,因为mysql.user表中找不到这行记录;
- T5时刻重新创建这个用户也不行,因为在做内存判断的时候,会认为这个用户还存在。