MySQL的权限系统在实现上比较简单,相关权限信息主要存储在几个被称为grant tables的系统表中,即:mysql.User、mysql.db、mysql.Host、mysql.table_priv和mysql.column_priv。由于权限信息数据量比较小,访问又非常频繁,MySQL在启动的时候,就会将所有的权限信息都加载到内存中,并保存在几个特定的结构里。所以每次手工修改了相关权限表之后,都须要执行“FLUSH PRIVILEGES”,通知MySQL重新加载MySQL的权限信息。当然,如果通过GRANT、REVOKE或DROP USER命令来修改相关权限,则不须要手工执行FLUSH PRIVILEGES命令,因为通过GRANT、REVOKE或DROP USER命令所做的权限调整在修改系统表的同时也会更新内存结构中的权限信息。从MySQL 5.0.2版本开始,MySQL还增加了CREATE USER命令,以此创建无任何特别权限(仅拥有初始USAGE权限)的用户。通过CREATE USER命令创建了新用户之后,新用户的信息也会自动更新到内存结构中。所以,建议读者尽量使用GRANT、REVOKE、CREATE USER及DROP USER命令来进行用户和权限的变更操作,尽量减少直接修改grant tables来实现用户和权限的变更。
要为某个用户授权,可以使用GRANT命令,要去除某个用户已有的权限则使用REVOKE命令。当然,除了这两种方式之外还有一种比较暴力的办法,那就是直接更新grant tables系统表。当给某个用户授权的时候,不仅须要指定用户名,还要指定来访主机。如果在授权的时候仅指定用户名,则MySQL会自动认为是对'username'@'%'授权。要去除某个用户的权限同样也须要指定来访主机。
可能有时还须要查看某个用户目前拥有的权限,这可以通过两个方式实现,首先是通过执行“SHOW GRANTS FOR 'username'@'hostname'”命令来获取之前该用户身上的所有授权。另一种方法是查询grant tables里面的权限信息。
MySQL中的权限分为5个级别,分别如下。
Global Level的权限控制又称为全局权限控制,所有权限信息都保存在mysql.user表中。Global Level的所有权限都是针对整个mysqld的,对数据库下的所有表及字段都有效。如果一个权限是以Global Level授予的,则会覆盖其他所有级别的相同权限设置。比如我们首先给abc用户授权可以UPDATE指定数据库(如test的t表),然后又在全局级别REVOKE掉了abc用户对数据库中所有表的UPDATE权限。这时候的abc用户则不再拥有test.t表的更新权限。Global Level主要有表1中列出的权限
要授予Global Level权限,只须在执行GRANT命令时,用“.”指定适用范围是Global即可,当有多个权限须要授予时,不必多次重复执行GRANT命令,只须要一次将这些权限名称通过逗号(“,”)分隔开即可,如示例代码1所示:
Database Level是在Global Level之下,其他3个Level之上的权限级别,其作用域即为指定整个数据库中的所有对象。与Global Level的权限相比,Database Level主要少了以下几个权限:CREATE USER、FILE、PROCESS、RELOAD、REPLICATION CLIENT、REPLICATION SLAVE、SHOW DATABASES、SHUTDOWN、SUPER和USAGE,没有增加任何权限。之前我们说过Global Level的权限会覆盖底下其他4层的相同权限,Database Level也一样,虽然它自己可能会被Global Level的权限设置覆盖,但同时他也能覆盖更下层的Table、Column和Routine的权限。
如果要授予Database Level的权限,可以有两种实现方式。
在授予权限的时候,如果有相同的权限须授予多个用户,也可以在授权语句中一次写上多个用户信息,通过逗号(,)分隔开,如示例代码4所示:
Database Level之下就是Table Level的权限了,Table Level的权限可以被Global Level和Database Level覆盖,同时它也能覆盖Column Level和Routine Level的权限。
Table Level的权限作用范围是授权语句中指定数据库的指定表。如可以通过示例代码5的语句给test数据库的t1表授权:
上面的授权语句在测试给t1表授予Table Level权限的同时,还测试了将权限授予含有通配符“%”的所有“.jianzhaoyang.com”主机。其中,USAGE权限是每个用户都有的最基本权限。
Table Level的权限由于其作用域仅限于某个特定的表,所以权限种类也比较少,仅有ALTER、CREATE、DELETE、DROP、INDEX、INSERT、SELECT和UPDATE这8种权限。
Column Level的权限作用范围就更小了,仅仅是某个表指定的某个(或某些)列。由于权限的覆盖原则,Column Level的权限同样可以被Global、Database和Table这三个级别权限中相同的级别覆盖,而且由于Column Level所针对的权限和Routine Level的权限作用域没有重合部分,所以不会有覆盖与被覆盖的关系。Column Level级别的权限仅有INSERT、SELECT和UPDATE这三种。Column Level的权限授权语句语法基本和Table Level差不多,只是须在权限名称后面将要授权的列名列表通过括号括起来,如示例代码6所示:
注意:若某用户向某个表插入(INSERT)数据,但他在该表中的某列上没有INSERT权限,则该列的数据将以默认值填充。这一点和很多其他的数据库有区别,是MySQL自己在SQL上面所做的扩展。
Routine Level的权限目前暂时只有EXECUTE和ALTER ROUTINE两种,针对的对象主要是procedure和function,当授予Routine Level权限时,须指定数据库和相关对象,如示例代码7所示:
除了上面几类权限之外,还有一个非常特殊的权限GRANT,拥有GRANT权限的用户可以将自身所拥有的任何权限全部授予其他用户,所以GRANT权限是一个非常特殊也非常重要的权限。GRANT权限的授予方式也和其他权限不太一样,通常是执行GRANT授权语句时在最后添加WITH GRANT OPTION子句达到授权目的的。
此外,还可以通过GRANT ALL语句把某个Level的所有可用权限授予某个用户,如示例代码8所示:
在以上5个Level的权限中,Table、Column和Routine三者在授权中所依赖(或者引用)的对象必须是已经存在的,不像Database Level,可以在当前不存在该数据库的时候就完成授权。
MySQL访问控制实际上由两个功能模块组成,一个是负责“看守MySQL大门”的用户管理模块,另一个就是负责监控来访者每一个动作的访问控制模块。用户管理模块决定造访客人能否进门,而访问控制模块则决定每个客人进门能拿什么,不能拿什么。图2是MySQL中实现访问控制的一幅简单流程图。
我们先看看用户管理模块是如何工作的。在MySQL中,用户访问控制部分的实现比较简单,所有授权用户都存放在一个系统表mysql.user中,当然这个表不仅仅存放了授权用户的基本信息,还存放了部分细化的权限信息。用户管理模块须要使用的信息很少,主要就是Host、User、Password这三项,都在mysql.user表中,如示例代码9所示:
一个用户要想访问MySQL,至少须要提供上面列出的这三项数据,这样MySQL才能判断是否该让他“进门”。这三项实际上由两部分组成:访问者来源的主机名(或者主机IP地址信息)和访问者的来访“暗号”(登录用户名和登录密码),这两部分中的任何一个没有匹配上都无法让“看守大门”的用户管理模块“开门”。其中Host信息存放的是对应User允许进行登录验证的主机,可以是某个具体的主机名(如:mytest)或域名(如:www.domain.com),可以是以“%”充当通配符的某个域名集合(如:%.domain.com);可以是一个具体的IP地址(如:1.2.3.4),也可以是存在通配符的域名集合(如:1.2.3.%);还可以用“%”来代表任何主机,就是不对访问者的主机做任何限制。如示例代码10所示:
但这里有一个比较特殊的访问限制,如果要通过localhost访问,必须有一条专门针对localhost的授权信息,即使不对任何主机做限制也不行。如下例所示,存在def@%的用户设置,如果不使用-h参数来访问,则登录会被拒绝,因为mysql在默认情况下会连接localhost,如示例代码11所示。
如果MySQL正在运行之中,我们对系统做了权限调整,那调整什么时候会生效呢?
先了解下MySQL存放于内存结构中的权限信息何时被更新:FLUSH PRIVILEGES会强行让MySQL更新加载到内存中的权限信息;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”命令。
当客户端连接通过用户管理模块的验证,连接上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为例,如示例代码12所示,处理流程如图3所示。
在前面了解到MySQL的grant tables有mysql.user、mysql.db、mysql.host、mysql.table_priv和mysql.column_priv这5个,我想除了mysql.host之外的4个都是非常容易理解的,每一个表针对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”才能满足要求,通过校验。
注意:这里有一个疑问,为什么Database Level层只要满足3个条件就能“校验通过”呢?
我们已经清楚,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这4列,前面3列是从MySQL 4.0.2版本才开始有的,其功能是对访问用户进行每小时所使用资源的限制,而最后的max_user_connections则是从MySQL 5.0.3版本才开始有的,它和max_connections的区别是限制耽搁用户的连接总次数,而不是每小时的连接次数。要使这4项限制生效,须在创建用户或给用户授权的时候加上以下4个子句: