MySQL访问控制实现原理
MySQL 访问控制实际上由两个功能模块共同组成,从第一篇的第二章架构组成中可以看
到,一个是负责 “看守 MySQL 大门”的用户管理模块,另一个就是负责监控来访者每一个动
作的访问控制模块。用户管理模块决定造访客人能否进门,而访问控制模块则决定每个客人
进门能拿什么不能拿什么。下面是一张 MySQL 中实现访问控制的简单流程图(见图 4-2):
1、 用户管理
我们先看看用户管理模块是如何工作的。在 MySQL 中,用户访问控制部分的实现比较简
单,所有授权用户都存放在一个系统表中: mysql.user,当然这个表不仅仅存放了授权用户
的基本信息,还存放有部分细化的权限信息。用户管理模块需要使用的信息很少,主要就是
Host,User,Password 这三项,都在 mysql.user 表中,如下:
sky@localhost : (none) 12:35:04> USE mysql;
Database changed
sky@localhost : mysql 12:35:08> DESC user;
+---------------+--------------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+---------------+--------------------+------+-----+---------+-------+
| Host | char(60) | NO | PRI | | |
| User | char(16) | NO | PRI | | |
| Password | char(41) | NO | | | |
... ...
+---------------+--------------------+------+-----+---------+-------+
一个用户要想访问 MySQL,至少需要提供上面列出的这三项数据,MySQL 才能判断是否
该让他 “进门”。这三项实际上由量部分组成:访问者来源的主机名(或者主机 IP 地址信息 )
和访问者的来访 “暗号”(登录用户名和登录密码),这两部分中的任何一个没有能够匹配上
都无法让看守大门的用户管理模块乖乖开门。其中 Host 信息存放的是 MySQL 允许所对应的
User 的信任主机,可以是某个具体的主机名(如: mytest)或域名(如:www.domain.com),
也可以是以“%”来充当通配符的某个域名集合(如:%.domain.com);也可以是一个具体的
IP 地址(如:1.2.3.4),同样也可以是存在通配符的域名集合(如:1.2.3.%);还可以用“%”
来代表任何主机,就是不对访问者的主机做任何限制。如以下设置:
root@localhost : mysql 01:18:12> SELECT host,user,password FROM user ORDER BY
user;
+--------------------+------+-------------------------------------------+
| host | user | password |
+--------------------+------+-------------------------------------------+
| % | abc | |
| *.jianzhaoyang.com | abc | |
| localhost | abc | *2470C0C06DEE42FD1618BB99005ADCA2EC9D1E19 |
| 1.2.3.4 | abc | *2470C0C06DEE42FD1618BB99005ADCA2EC9D1E19 |
| 1.2.3.* | def | *2470C0C06DEE42FD1618BB99005ADCA2EC9D1E19 |
| % | def | *2470C0C06DEE42FD1618BB99005ADCA2EC9D1E19 |
| localhost | def | *2470C0C06DEE42FD1618BB99005ADCA2EC9D1E19 |
... ...
+--------------------+------+-------------------------------------------+
但是这里有一个比较特殊的访问限制,如果要通过 localhost 访问的话,必须要有一条
专门针对 localhost 的授权信息,即使不对任何主机做限制也不行。如下例所示,存在 def@%
的用户设置,但是如果不使用-h 参数来访问,则登录会被拒绝,因为 mysql 在默认情况下
会连接 localhost:
sky@sky:~$ mysql -u def -p
Enter password:
ERROR 1045 (28000): Access denied for user 'def'@'localhost' (using
password: YES)
但是当通过-h 参数,明确指定了访问的主机地址之后就没问题了,如下:
sky@sky:~$ mysql -u def -p -h 127.0.0.1
Enter password:
Welcome to the MySQL monitor. Commands end with ; or g.
Your MySQL connection id is 17
Server version: 5.0.51a-log Source distribution
Type 'help;' or 'h' for help. Type 'c' to clear the buffer.
def@127.0.0.1 : (none) 01:26:04>
如果我们有一条 localhost 的访问授权则可以不使用-h 参数来指定登录 host 而连接默
认的 localhost:
sky@sky:~$ mysql -u abc -p
Enter password:
Welcome to the MySQL monitor. Commands end with ; or g.
Your MySQL connection id is 18
Server version: 5.0.51a-log Source distribution
Type 'help;' or 'h' for help. Type 'c' to clear the buffer.
abc@localhost : (none) 01:27:19> exit
Bye
如果 MySQL 正在运行之中的时候,我们对系统做了权限调整,那调整之后的权限什么时
候会生效呢?
我们先了解何时 MySQL 存放于内存结构中的权限信息被更新:FLUSH PRIVILEGES 会强
行让 MySQL 更新 Load 到内存中的权限信息; GRANT、REVOKE 或者 CREATE USER 和 DROP USER
操作会直接更新内存中俄权限信息;重启 MySQL 会让 MySQL 完全从 grant tables 中读取权
限信息。
那内存结构中的权限信息更新之后对已经连接上的用户何时生效呢?
对于 Global Level 的权限信息的修改,仅仅只有更改之后新建连接才会用到,对于已
经连接上的 session 并不会受到影响。而对于 Database Level 的权限信息的修改,只有当
客户端请求执行了“USE database_name”命令之后,才会在重新校验中使用到新的权限信
息。所以有些时候如果在做了比较紧急的 Global 和 Database 这两个 Level 的权限变更之后 ,
可能需要通过“KILL”命令将已经连接在 MySQL 中的 session 杀掉强迫他们重新连接以使
用更新后的权限。对于 Table Level 和 Column Level 的权限,则会在下一次需要使用到该
权限的 Query 被请求的时候生效,也就是说,对于应用来讲,这两个 Level 的权限,更新之
后立刻就生效了,而不会需要执行“KILL”命令。
2、 访问控制
当客户端连接通过用户管理模块的验证,可连接上 MySQL Server 之后,就会发送各种
Query 和 Command 给 MySQL Server,以实现客户端应用的各种功能。当 MySQL 接收到客户
端的请求之后,访问控制模块是需要校验该用户是否满足提交的请求所需要的权限。权限校
验过程是从最大范围的权限往最小范围的权限开始依次校验所涉及到的每个对象的每个权
限。
在验证所有所需权限的时候, MySQL 首先会查找存储在内存结构中的权限数据,首先查
找 Global Level 权限,如果所需权限在 Global Level 都有定义(GRANT 或者 REVOKE),
则完成权限校验(通过或者拒绝),如果没有找到所有权限的定义,则会继续往后查找
Database Level 权限,进行 Global Level 未定义的所需权限的校验,如果仍然没有能够
找到所有所需权限的定义,MySQL 会继续往更小范围的权限定义域查找,也就是 Table
Level,最后则是 Column Level 或者 Routine Level。
下面我们就以客户端通过 abc@localhost 连接后请求如下 Query 我为例:
SELECT id,name FROM test.t4 where status = 'deleted';
在前面我们了解到 MySQL 的 grant tables 有 mysql.user,mysql.db,mysql.host,
mysql.table_priv 和 mysql.column_priv 这五个,我想出了 mysql.host 之外的四个都是非
常容易理解的,每一个表针对 MySQL 中的一种逻辑对象,存放某一特定 Level 的权限,唯独
mysql.host 稍有区别。我们现在就来看看 mysql.host 权限表到底在 MySQL 的访问控制中充
当了一个什么样的角色呢?
mysql.host 在 MySQL 访问控制模块中所实现的功能比较特殊,和其他几个 grant tables
不太一样。首先是 mysql.host 中的权限数据不是(也不能)通过 GRANT 或者 REVOKE 来授予
或者去除,必须通过手工通过 INSERT、UPDATE 和 DELETE 命令来修改其中的数据。其次是
其中的权限数据无法单独生效,必须通过和 mysql.db 权限表的数据一起才能生效。而且仅
当 mysql.db 中存在不完整(某些场景下的特殊设置)的时候,才会促使访问控制模块再结
合 mysql.host 中查找是否有相应的补充权限数据实现以达到权限校验的目的,就比如上图
中所示。在 mysql.db 中无法找到满足权限校验的所有条件的数据(db.User = 'abc' AND
db.host = 'localhost' AND db.Database_name = 'test'),则说明在 mysql.db 中无法完
成权限校验,所以也不会直接就校验 db.Select_priv 的值是否为'Y'。但是 mysql.db 中有
db.User = 'abc' AND db.Database_name = 'test' AND db.host = '' 这样一条权限信息
存在,大家可能注意到了这条权限信息中的 db.host 中是空值,注意是空值而不是'%'这个
通配符哦。当 MySQL 注意到有这样一条权限信息存在的时候,就该是 mysql.host 中所存放
的权限信息出场的时候了。这时候,MySQL 会检测 mysql.host 中是否存在满足如下条件的
权限信息:host.Host = 'localhost' AND host.Db = 'test'。如果存在,则开始进行
Select_priv 权限的校验。由于权限信息存在于 mysql.db 和 mysql.host 两者之中,而且是
两者信息合并才能满足要求,所以 Select_priv 的校验也需要两表都为'Y'才能满足要求,
通过校验。
我们已经清楚,MySQL 的权限是授予“username@hostname”的,也就是说,至少需要
用户名和主机名二者才能确定一个访问者的权限。又由于 hostname 可以是一个含有通配符
的域名,也可以是一个含有通配符的 IP 地址段。那么如果同一个用户有两条权限信息,一
条是针对特定域名的,另外一个是含有通配符的域名,而且前者属于后者包含。这时候 MySQL
如何来确定权限信息呢?实际上 MySQL 永远优先考虑更精确范围的权限。在 MySQL 内部会按
照 username 和 hostname 作一个排序,对于相同 username 的权限,其 host 信息越接近访问
者的来源 host,则排序位置越靠前,则越早被校验使用到。而且, MySQL 在权限校验过程中 ,
只要找到匹配的权限之后,就不会再继续往后查找是否还有匹配的权限信息,而直接完成校
验过程。
大家应该也看到了在 mysql.user 这个权限表中有 max_questions,max_updates,
max_connections,max_user_connections 这四列,前面三列是从 MySQL4.0.2 版本才开始
有的,其功能是对访问用户进行每小时所使用资源的限制,而最后的 max_user_connections
则是从 MySQL5.0.3 版本才开始有的,他和 max_connections 的区别是限制耽搁用户的连接
总次数,而不是每小时的连接次数。而要使这四项限制生效,需要在创建用户或者给用户授
权的时候加上以下四种子句:
max_questions : WITH MAX_QUERIES_PER_HOUR n;
max_updates : WITH MAX_UPDATES_PER_HOUR n;
max_connections : WITH MAX_CONNECTIONS_PER_HOUR n;
max_user_connections: MAX_USER_CONNECTIONS。
四个子句可以同时使用,如:
“ WITH MAX_QUERIES_PER_HOUR 5000 MAX_CONNECTIONS_PER_HOUR 10
MAX_USER_CONNECTIONS 10000”。