一般安全问题

本节内容主要引自 MySQL 8.0 Reference Manual

MySQL 安全准则

在讨论安全问题时,有必要从全局的角度去考虑对服务器主机整体的保护,而非仅仅针对 MySQL 服务端。各种可能的攻击包括:窃取、修改、回放、拒绝服务攻击。

MySQL 使用基于 ACL 的安全防护,针对所有的连接、查询及其它可能的操作。也支持 MySQL 客户端与服务端使用 SSL 加密连接。

  • 永远不要授权任何人访问 mysql 数据库中的 user 表,除 MySQL 的 root 帐户。
  • 要了解 MySQL 访问权限系统的工作原理。使用 GRANTREVOKE 语句来控制访问,不要给予不必要的权限,不要为所有主机授权,而应有针对性地对某些主机授权。
    • 如果使用 mysql -u root 能够无需密码就成功连接到服务端,则任何人都可以成为超级用户。因此要为 root 设置密码。
    • 使用 SHOW GRANTS 语句可以检查哪些帐户有哪些访问权限,可以用 REVOKE 语句移除不必要的权限。
  • 不要在数据库中保存明文密码,而应该使用 SHA2() 或其它单向的哈希函数生成哈希值。
  • 不要从字典里选择密码。
  • 使用防火墙。
    • 尝试从互联网扫描服务器的端口,可以使用类似 nmap 的工具。MySQL 默认使用 3306 端口,非信任的主机应该无法访问该端口。或者用 telnet server_host 3306 测试,如果 telnet 被挂起或被拒绝,则说明端口是封死的;如果连接上了,并且看到一些垃圾字符,说明端口是开放的。则应该在防火墙中将其关闭。
  • 访问 MySQL 的应用程序必须不信任由用户直接输入的任何数据,而应该使用适当的防御编程技术来写入。
  • 不要在互联网上传输明文数据,而应使用 SSL 或 SSH 等加密协议。MySQL 支持内部 SSL 连接。另一种技术是使用 SSH 的端口转发来创建一个加密的通道来通信。
  • 建议使用 tcpdumpstrings 工具。多数情况下可以检查 MySQL 的数据流是否未加密:
$ tcpdump -l -i eth0 -w - src or dst port 3306 | strings

保持密码的安全

终端用户密码安全规范

MySQL 的用户应遵守以下准则来保证密码的安全:

如果要运行客户端程序来连接到 MySQL 服务端,应该规避有可能被别人看到密码的方法。最安全的方法是让客户端程序提示输入密码,或把密码保存在一个安全的配置文件中。

  • 使用 mysql_config_editor 工具,可以把用于身份验证的保密信息保存在一个加密的文件中,文件名为 .mylogin.cnf。该文件可以被 MySQL 客户端程序读取,以获取身份验证信息,用于连接 MySQL 服务端。
  • 在命令行使用 -p****** 选项很不安全,在某些系统中,选项中的密码会被 ps 等程序显示出来。虽然 MySQL 客户端程序会用一堆 0 来覆盖它,但仍是存在该密码可见的时间段。
  • 在命令行中使用 -p 选项,后面不加密码。这种情况,客户端程序就会以交互形式来询问密码,更加安全。
  • 把密码保存在一个配置文件 .my.cnf 中,并为其分配适当权限,然后在命令行使用 --default-file=/home/neo/.my.cnf 选项启动服务端。
[client]
password=your_pass
  • 把密码保存到 MYSQL_PWD 环境变量中,非常不安全,不建议使用。

mysql 客户端会把执行的语句记录到一个历史文件中,该文件默认文件名为 .mysql_history,位于家目录。在 CREATE USERALTER USER 语句中都可能有明文的密码,因此要严格限制对该文件的访问权限。

同样,命令行中执行过的命令也会保存到 bash 的历史文件中,如 ~/.bash_history,同样应该限制对该文件的访问权限。

管理员密码安全规范

MySQL 把用户帐户的密码保存在 mysql.user 表中,非管理员绝不能访问。

帐户密码会过期,用户必须重置。

可以使用 validate_password 插件来强制实施密码有效策略。

如果用户有权修改插件目录或 my.cnf 文件,他就有机会替换掉插件,风险极大。

日志文件也会保存密码,应控制访问权限。

巩固安全,抵御攻击

保证连接的安全

连接到 MySQL 服务端时必须使用密码,不能明文方式传递。所有其它信息都会以明文传递,很可能会被人看到。

  • 如果客户端与服务端之间的连接的网络不可信,可以使用压缩协议,增加流量解密的难度。
  • 也可以使用 MySQL 内部的 SSL。
  • 还可以使用 SSH 获取加密的连接。

安全规范

  • 所有 MySQL 帐号都要有密码。
  • 确保对数据目录有读写权限的用户,与运行服务的是同一个用户
  • 永远不要以 root 用户运行服务端。

    极度危险,因为有 FILE 权限的用户可以用 root 身份创建文件,如 ~root/.bashrc。可以创建一个名为 mysql 的 Linux 用户专门用于执行 mysqld。在配置文件中可以指定以该用户身份运行服务端。

[mysqld]
user=mysql
  • 不要为非管理员用户分配 FILE 权限。

    所有具有该权限的用户都有能力在文件系统的 任何位置 创建文件,使用的是 mysqld 服务的权限。

    为了让有 FILE 权限用户其操作更安全一些,由 SELECT ... INO OUTFILE 生成的文件不会覆盖现有的文件,并且任何人都可以修改。

    FILE 权限也可以用来读取系统中的文件,只要文件对于运行服务端的 Linux 用户是可读的。使用这个权限可以把任何文件导入数据库的表中。如果遭到滥用,完全可以把 /etc/passwd 导入数据库,很不安全。

    可以用 secure_file_priv 系统变量来限制哪些目录中的文件可读、可写。

  • 不要为非管理员用户分配 PROCESSSUPER 权限。mysqladmin processlistSHOW PROCESSLIST 语句的输出会显示当前执行的任何语句的内容,因此有该权限的用户通年地到其它用户执行的语句。

    mysqld 会一直为拥有该权限的用户保留一个额外的连接,以便 MySQL 的 root 用户可以随时进来查看服务端的活动。

    SUPER 权限可以用来中断客户端连接,用来修改服务端的运行,控制复制服务器等。

  • 应该禁止使用到表的符号链接。可以通过 --skip-symbolic-links 选项加以禁用。尤其以 root 身份运行 mysqld 时,因为任何对数据目录有写权限的用户,都可以删除系统中的任何文件。
  • 如果对 DNS 不信任,可以在授权表(grant table)中直接使用 IP 地址。尤其在使用通配符创建授权表条目时,一定要特别谨慎。
  • 如果要限制某一帐户的连接数,可以通过设置 max_user_connections 变量来实现。
  • 如果插件目录对服务端是可写的,用户就有机会用 SELECT ... INTO DUMPFILE 向插件目录中的文件写入可执行代码。可以把 plugin_dir 设为只读来避免,或者给 --secure-file-priv 指定一个可以用 SELECT 安全写入的目录。

客户端程序安全规范

访问 MySQL 的程序不应该任何由用户直接输入的数据,因为用户可能会在 Web 表格、URL 或其它程序中输入一些特殊字符或转义字符序列来欺骗系统。当用户输入一些诸如 ; DROP DATABASE mysql; 之类的命令时,要确保客户端程序能保证安全。

一个容易犯的错误是只保护字符数据。一定要记得,数字数据也需要保护。如果当用户输入 234 时,程序自动生成一个查询 SELECT * FROM table WHERE ID=234,则用户有可能会输入 234 OR 1=1 以促使程序生成查询 SELECT * FROM table WHERE ID=234 OR 1=1。结果就是,服务端会在表中遍历每一行,不仅每一行会暴露,还会让服务端负载极度增加。最简单的防护办法是在数字外使用单引号,SELECT * FROM table WHERE ID='234'。如果用户输入了额外的信息,也都会成为字符串的一部分而已。在数字环境下,MySQL 会自动把这样的字符串转换成数字,并删除后面的字符串。

规范

  • 启用严格的 SQL 模式,以告知服务端对于接收数据时要更加严格地审查。
  • 在网页问卷(form)中尽量使用单引号和双引号。
  • 修改动态 URL,向其中添加 %22 (”),%23(#),%27(’)
  • 修改动态 URL 中的数据类型,从数字改为字符型,以避免这一类的攻击。
  • 在数字字段尝试输入字符、空格和特殊字符,客户端应该先把它们清除,然后再传递给 MySQL,或者生成错误消息。把未经检查的数据直接发给 MySQL 是非常危险的!
  • 先检查数据的大小,再发给 MySQL
  • 用客户端程序连接到数据库时,不要使用管理员帐户。不要给客户端不必要的权限。

API

许多应用程序接口会为数据中的特殊字符提供转义功能,如果使用恰当,可以避免不小心输入非预期的语句:

  • MySQL C API :使用 mysql_real_escape_string_quote() API 调用
  • MySQL++ :为查询流使用 escapequote 修饰符
  • PHP :使用 mysqlipdo_mysql 扩展,不使用较早的 ext/mysql 扩展。
  • Perl DBI :使用占位符或 quote() 方法
  • Ruby DBI :使用占位符或 quote() 方法
  • Java JDBC :使用一个 preparedStatement 对象及占位符

MySQL 访问权限系统

MySQL 访问权限系统的主要功能是:对从特定主机连接过来的用户加以验证,将该用户与数据库权限关联到一起。其它的功能包括:允许匿名用户,为特定功能及管理行为授权。

MySQL 权限系统无法做到:

  • 无法显式拒绝指定用户的访问。无法显式匹配某个用户,然后拒绝其连接。
  • 无法让某个用户有权限在数据库中创建或删除表,同时却无法创建或删除数据库自身。
  • 用户的密码是应用于全局的。无法针对数据库、表、程序单独应用密码。

MySQL 访问权限系统的用户接口由 CREATE USERGRANTREVOKE 等语句组成。

服务端内部,会把权限信息保存在 mysql 数据库的授权表中。MySQL 服务端在启动时会将这些表的内容读入内存,并将访问控制决策基于授权表的内存副本。

MySQL 访问权限系统会确保所有用户只能进行权限范围内的操作。

用户连接到 MySQL 服务端时,其身份决定于连接的主机以及使用的用户名。连接以后,当用户发起请求时,系统会根据其身份授予其对应的权限。

MySQL 的访问控制涉及到两个阶段:

阶段一 :基于用户身份,服务端接受或拒绝连接。

阶段二 :假如连接成功,服务端会检查发来的每条语句,看是否该用户有权执行。

MySQL 提供的权限

授予 MySQL 帐户的权限决定了他可以进行哪些操作。

权限应用的范围

在不同的上下文中,进行不同级别的操作时,权限都会有所区别:

  • 有了管理员权限,用户可以管理 MySQL 服务端的运行。这些权限是全局的,不限于特定的数据库。
  • 数据库权限适用于某个数据库及其中的所有对象。这些权限可以授予特定的数据库,也可以全局授予,以应用于所有数据库。
  • 可以对数据库中特定对象单独授予权限,如对表、索引、视图、保存的程序等对象;也可以对一个数据库中某一类对象授权,如某数据库中的所有的表;还可以为所有数据库的某一类对象授权。

权限的保存位置

关于帐户权限的信息保存在系统数据库 mysql 中的 userdbtables_privcolums_privglobal_grants 这些表中。MySQL 服务端启动时,会把这些表的内容读取到内存中,并在权限发生修改时重新读取。访问控制的决策是基于授权表的这些内存副本的。

为帐户分配权限

为帐户分配权限时,应该只给他需要的。尤其在分配 FILE 及管理员权限时。

  • FILE 权限若被滥用,能把服务端可读取的本地任何文件导入数据库。
  • GRANT OPTION 权限的用户可以把他们的权限转给其他用户,两个拥有不同权限的用户,如果使用 GRANT OPTION 就可以合并他们的权限。
  • ALTER 权限可被用来重命名表,进而破坏权限系统。
  • SHUTDOWN 权限的滥用可导致服务端终止运行。
  • PROCESS 权限可被用来查看当前执行的语句的明文,包括语句中的密码。
  • SUPER 权限可被用来终止其它会话,或修改服务端运行的方式。
  • 如果被授予对 mysql 数据库的权限,用户就可以修改密码及其它访问权限信息。

动态权限

MySQL 支持静态权限与动态权限。

  • 静态权限是指服务端内建的权限,这些权限随时可以被授予给用户帐户,无法被注销。
  • 动态权限指在运行时定义的权限,既可被注册,也可注销。这会影响它们的可用性。未被注册的动态权限不可用于授权。

权限是静态还是动态的,会影响当时可为用户授权的具体内容。

动态权限的注册与注销

服务端会在内存中维护一组注册的权限,服务端关闭时,动态权限会注销。

一般来说,一个用来定义动态权限的服务端组件会在安装时注册动态权限。但卸载时,服务端组件却不会注销这些动态权限。

如果尝试注册一个已经注册了的动态权限,不会有警告和错误消息。

INSTALL COMPONENT 'my_component';
UNINSTALL COMPONENT 'my_component';
INSTALL COMPONENT 'my_component';

第一条语句会注册该组件定义的动态权限,第二条不会注销,第三条即使发现这些权限已经注册,也不会有警告和错误消息。

动态权限的保存

动态权限仅应用于全局。服务端会把当前分配的动态权限信息保存到 mysql.global_grants 系统表中的用户帐户中。:

  • 服务端启动过程中,会自动注册名为 global_grants 的权限,除非使用了 --skip-grant-tables 选项。
  • GRANTREVOKE 语句会修改 global_grants 表的内容。
  • global_grants 中的动态权限的分配是持久有效的,服务端关闭时不会被清除。

动态权限的分配及撤消

被授予的动态权限可以用 SHOW GRANTS 语句来查看,它们保存在数据库 information_schemauser_privileges 表中。

对于全局级别的 GRANTREVOKE 语句,如果语句中的权限不是静态的,就会检查是否动态的,如果是就授予。否则,发出错误消息。

而对于 GRANTREVOKE 来说,ALL [PRIVILIGES] 这种全局级别的语句包括所有静态全局权限,以及当前注册的动态权限:

  • 全局级别的 GRANT ALL 将授予所有静态的全局权限,以及所有当前注册的动态权限。
  • 全局级别的 REVOKE ALL 将撤消所有授予的静态全局权限,以及所有授予的动态权限。

FLUSH PRIVILEGES 语句用于重新加载权限,它会读取 global_grants 表,查找其中的动态权限分配,把找到的所有未注册的权限进行注册。

SUPER 迁移到动态权限

MySQL 8.0 中,许多操作在以前既需要 SUPER 权限,同时也需要在有限范围内使用一个动态权限。这样的每个操作现在只需要把相关的动态权限授予某个帐户即可,不再需要 SUPER 权限。这个改变提高了安全性,SUPER 权限将会在今后的版本中取消。

因此,从前使用 SUPER 权限的帐户,需要迁移到适当的动态权限,以便为 SUPER 权限的取消做好准备:

执行以下查询,找出被授予 SUPER 权限的帐户:

SELECT GRANTEE FROM INFORMATION_SCHEMA.USER_PRIVILEGES
WHERE PRIVILEGE_TYPE = 'SUPER';

针对找到的帐户,确定他是因为哪些操作才需要的 SUPER,然后把对应的动态权限授予给他,撤消 SUPER 权限:

GRANT BINLOG_ADMIN, SYSTEM_VARIABLES_ADMIN ON *.* TO 'u1'@'localhost';
REVOKE SUPER ON *.* FROM 'u1'@'localhost';

授权表

系统数据库 mysql 包含几个授权表,保存的是用户帐户及其权限的信息。

要想操作授权表中的内容,可以使用 CREATE USERGRANTREVOKE 这样的帐户管理语句来设置帐户,控制每个帐户的权限。

不建议使用 INSERTUPDATEDELETE 这样的语句来直接修改授权表,服务端会直接忽视这样的修改。服务端会检查针对授权表所做的更改,一理发现结构错误就会报错。此时需要用 mysql_upgrade 来为该表更新结构。

授权表常见内容

mysql 数据库中的表包含以下授权信息:

  • user :用户帐户,全局权限,其它
  • global_grants :分配给用户的动态全局权限
  • db :数据库级的权限
  • tables_priv :表级的权限
  • colums_priv :列级的权限
  • procs_priv :保存的流程及功能权限
  • proxies_priv :代理用户权限
  • default_roles :默认用户权限
  • role_edges :Edges for role subgraphs
  • password_history :密码的修改

MySQL 8.0 中,授权表使用 InnoDB 存储引擎,并是事务性的。在这之前,授权表使用 MyISAM 存储引擎,是非事务性的。这个改变带来了帐户管理语句其行为的改变,如 CREATE USERGRANT。之前,一条帐户管理语句中可以使用多个用户,结果可能是部分用户成功执行,部分失败。而现在,每条语句都是事务性的,要么所有用户执行成功,要么所有用户失败,回滚,语句不产生任何影响。

每个授权表都包含作用域字段和权限字段。

服务端以如下方式使用授权表

  • user 表的作用域字段决定了是拒绝还是允许传入的连接。对于允许的连接,user 表中所有授予的权限都是该用户的全局权限,对服务端所有数据库有效。
  • global_grants 表保存着当前为用户帐号所分配的动态权限。
  • db 表的作用域字段决定了哪些用户可以访问哪些主机上的哪些数据库。权限字段决定了允许的操作。
  • tables_privcolumns_priv 表与 db 表类似,但更加细化:它们作用于表和字段级别,而不是数据库级别。表级别的权限作用于该表及其所有字段,而字段级别的权限则仅作用于特定的字段。
  • procs_priv 表作用于保存的例程。该级别的权限仅作用于某个例程或函数。
  • proxies_priv 表决定了哪些用户可以作为他人的代理,以及用户是否可以为别人授予 PROXY 权限。
  • default_rolesrole_edges 表包含关于角色关系的信息。
  • password_history 表保存了之前使用过的密码,以限制重用。

如何指定帐户

MySQL 的帐户名由 用户名主机名 组成。

使用 CREATE USERGRANTSET PASSWORD 语句时,帐户名要遵循以下规则:

  • 帐户名的语法为 'user_name'@'host_name'
  • 如果帐户名只包含用户名,则等同于 'user_name'@'%'
  • 如果用户名不包含特殊字符,允许不加单引号
  • 引用用户名和主机时可以用单引号、双引号或反引号
  • 用户名和主机名必须分别括起来
  • 引用函数 CURRENT_USERCURRENT_USER() 等同于指定当前客户端用户名和主机名

帐户的保存

MySQL 在系统数据库 mysql 中的授权表中保存帐户名,用户名和主机名会分别用单独字段保存:

  • 每个帐户在 user 表中都有一条记录。User 字段保存用户名,Host 字段保存主机名。该表同时还体现了该帐户拥有的全局权限。
  • 其它的授权表体现了帐户对于数据库及其对象的权限。这些表也有 UserHost 字段,与 user 表相对应。
  • 用户名是大小写敏感的,主机名不是。

帐户和主机名的形式

用户名可以为非空,用于匹配传入连接请求的用户名;也可以为空,用于匹配任何用户名。如果帐户中的用户名为空则分区表为匿名用户,如 ''@'localhost'

主机名可以用多种形式,也可用通配符:

  • 主机名可以用主机名或 IP 地址表示。'localhost' 表示本地主机。'127.0.0.1' 表示 IPv4 环回地址,'::1' 表示 IPv6 环回地址。
  • %_ 是通配符,可以用来表示主机名和 IP 地址。'%''%.mysql.com''198.55.100.%'。如果通配符出现在 IP 地址中,则它只能匹配 IP 地址,而不能匹配字符,因此 '1.2.example.com' 不会得逞。
  • 当用 IPv4 地址表示主机时,可以指定掩码,用斜线分隔 IP 地址与掩码。
CREATE USER 'david'@'198.51.100.0/255.255.255.0';

该语句的作用是允许 david 可以从子网 198.51.100.0 中的任何主机连接进来。

角色名的指定

MySQL 中用角色来分区表 一组权限

角色的语法与帐户名类似,不同之处在于:

  • 角色名中的用户名不能为空。因此没有匿名角色这一说。
  • 主机部分可以省略,用 '%' 表示,但没有通配符属性,不能代表任何主机。
  • 主机不支持掩码
  • 帐户名在某些上下文环境中可以用 CURRENT_USER() 表示,但角色名不能

mysql.user 系统表中的每一行记录,可以既作为帐户,也可以作为角色。通配符只能用于在帐户中进行匹配,而无法应用于角色。

SET ROLE 'myrole'@'%';

该语句不可能匹配角色名为 myrole 的所有角色,它只能匹配一个角色,名字就叫 'myrole'@'%'

基于这个原因,指定角色名时通常只有用户名,而把主机名部分用 '%' 表示。

但是,主机名部分不用 '%',而使用真正的主机名,这样做其实更有用,尤其想要创建一个名字,既当作角色用,又当作帐户名用。

访问控制

第一阶段:连接的验证

用户想要连接到 MySQL 服务端时,服务端会基于以下条件来接受或拒绝连接:

  • 用户的 身份,提供正确密码以验证该身份
  • 该帐户 是否被锁

服务端先检查密码,再检查是否锁定。哪一步失败都会导致服务端拒绝访问。如成功则接受连接,进入第二阶段,等待请求。

身份验证

密码的检查是使用 user 表中的三个作用域字段 HostUser、’authentication_string’。

‘authentication_string’ 字段可以为空,意味着用户连接时不能指定密码。如果服务端用插件来验证客户端,则验证方法不一定使用 ‘authentication_string’ 字段做为密码,这种情况下,有可能使用外部密码来验证。

如果 ‘authentication_string’ 字段非空,则分区表加密过的密码。安全起见,MySQL 不会以明文保存密码的。同时,用户连接时输入的密码也是加密的,借助帐户验证插件的密码哈希方法。在连接的过程中,验证密码时使用的都是加过密的密码。

从 MySQL 的角度来看,加过密的密码才是真正的密码,因此不要把访问权给任何人。

锁定状态

锁定状态记录于 user 表中的 account_locked 字段。

接受连接的条件

服务端接受连接的条件:

  • user 表中找到了匹配的记录
  • 用户输入的密码与该条记录中的密码匹配
  • account_locked 字段的值为 N
服务端如何匹配用户身份

对于传入的连接,user 表中是有可能找到多个匹配记录的,此时,服务端必须确定要使用哪些记录:

  • 服务端每次把 user 表加载到内存时,都会 先排序
  • 客户端要连接时,服务端会按排序后的顺序 逐行匹配
  • 服务端只使用匹配到的 第一条 记录
服务端排序的规则

先按 Host 字段排序,最明确的指定排在前面,最不明确的指定排在后面:

  • 主机名和 IP 地址形式的指定是最明确的,它们一定排在前面
  • 通配符 '%' 代表任何主机,是最不明确的,一定排在后面。
  • 空字符串 '' 虽然也代表任何主机,要排在最后。

再按 User 字段排序,最明确的指定排在前面:

  • 非空用户名排在前面
  • 空的用户名排在最后

对于 HostUser 字段的值都很明确的,排序是不确定的。

查看当前用户帐户

鉴于以上所述,很有可能用户虽然成功连接进来,但其身份并不是其预想的那个,这会导致权限分配也并非预期。因此,有必要确认一下服务端使用的哪个身份验证的自己。使用 CURRENT_USER() 函数:

mysql> SELECT CURRENT_USER();
+----------------+
| CURRENT_USER() |
+----------------+
| @localhost     |
+----------------+

第二阶段:请求的验证

建立连接之后,服务端就进入第二阶段的访问控制。

针对用户在该连接发起的每个 请求,服务端会判断用户要进行什么 操作,然后会检查他是否有足够的 权限 来做。此时,授权表中的权限字段就派上用场了。这些权限可以来自 userdbtables_privcolumns_privprocs_priv 表中的任何一个。

检索全局权限

user 表授权的都是全局权限,作用于所有默认数据库。例如,假如 user 表授权用户 DELETE 权限,他就可以删除服务端主机中的任何数据库中的任何表中的任何记录。因此该权限只应该授予数据库管理员。对于其他用户来说,其在 user 表中的所有权限都应该设置为 N,然后只为其授予更加具体的权限。

检索数据库特定权限及主机权限

db 表用于授权特定数据库的权限,作用域字段的值可以有以下形式:

  • User 字段为空,则匹配匿名用户,非空则按实际匹配,不能使用通配符。
  • HostDb 字段可以使用通配符。
  • Host'%' 或空,代表任何主机。
  • Db'%' 或空,代表任何数据库。

服务端在读取 user 表时,同时也会把 db 表加载到内存并排序。为 db 排序时是基于 HostDbUser 作用域字段的,与用户表的排序方法相同,最明确的指定在先,最不明确的指定在最后,使用第一条匹配记录。

检索表、字段、例程的特定权限

tables_privcolumns_privprocs_priv 这几个表用于授予特定表、特定字段、特定例程的权限。作用域字段的值可接受以下形式:

  • Host 字段可用通配符
  • Host 字段若为 '%' 或空值,代表任何主机
  • DbTable_nameColumn_nameRoutine_name 字段不能使用通配符,不能为空

服务端基于 HostDbUser 字段为这三个表排序,但更简单,因为只有 Host 字段可用通配符。

服务端使用经排序的表来验证收到的每个请求。

服务端检索的顺序

对于需要 管理员权限 的请求,如 SHUTDOWNRELOAD 等,服务端会 只检查 user 的记录,因为它是用来指定管理员权限的唯一的表格。如果匹配的记录允许请求的操作,则服务端会允许该访问,否则拒绝。

对于与 数据库相关的请求,如 INSERTUPDATE 等,服务端 首先检索 user,检查用户的全局权限。如果匹配的记录允许请求的操作,就允许访问;如果权限不够,服务端会 进一步检索 db,看用户是否有该数据库的专属权限。

服务端检索 db 表,尝试匹配 HostDbUser 字段。HostUser 字段分别用于匹配当前连接的用户主机及 MySQL 用户名。Db 字段用于匹配用户希望访问的数据库。如果找不到匹配 HostUser 的记录,访问被拒绝。

通过 db 表的匹配记录确定了对指定数据库的权限之后,服务端会把这些权限添加到 user 表授予的全局权限中。如果验证的结果允许请求的操作,则允许访问,否则拒绝。否则,在检索完 user 表,以及 tables_privcolumns_priv 表中的权限以后,服务端会把检索到的权限统统添加到用户的权限中,并基于该结果来决定允许还是拒绝。

如果一开始就发现 user 表中的权限不满足当前请求的操作符,为什么还是要把这些权限加到数据库、表、权限字段中呢?是因为一个请求可能需要一个或多个类型的权限,有可能 user 表中有一个权限,db 表中有另一个权限,两个加在一起才满足请求的操作权限。

对于保存的例程操作,服务端使用 procs_priv 表,而不会使用 tables_privcolumns_priv

小结

用户权限究竟是如何计算的,可以用布尔值的形式来描述一下:

global privileges
OR (database privileges AND host privileges)
OR table privileges
OR column privileges
OR routine privileges

权限的修改何时生效

mysqld 启动时,会把所有 授权表 中的内容加载到内存中,此时起,这些表格对于访问控制是有效的。

如果用 GRANTREVOKESET PASSWORDRENAME USER 等帐户管理语句,对授权表进行了间接的修改,服务端发现这些改动时,会立即 重新加载 这些改动。

如果用 INSERTUPDATEDELETE 等语句直接地修改了授权表,这些改动必须重启服务或手动重新加载才能生效。

要想手动加载授权表,进行一次 权限冲洗 的操作,可以使用 FLUSH PRIVILEGES 语句,或 mysqladmin flush-privilegesmysqladmin reload 命令来实现。

权限冲洗会对当前已连接的客户端产生如下影响:

  • 表和字段权限的改变会从客户端下一个请求生效
  • 数据库特定权限的修改会在客户端下次执行 USE db_name 语句时生效
  • 对于已连接的客户端来说,全局权限和密码不受影响,这些改变只能在下次连接生效。

注:客户端程序有可能会缓存数据库的名字,因此要想让对数据库特定权限的修改生效,可以先修改当前数据库,再切换回来。

如果服务端启动时使用了 --skip-grant-tables 选项,它就不会读取授权表,也不会进行访问控制。这样的话,任何连接上来可以做任何事,非常不安全。

连接排错

如果连接 MySQL 服务端时遇到问题,可参照如下解决办法:

确认服务是运行着的

如果服务没有运行,当然连接不上。

错误消息通常为:

$ mysql
ERROR 2003: Can't connect to MySQL server on 'host_name' (111)
$ mysql
ERROR 2002: Can't connect to local MySQL server through socket
'/tmp/mysql.sock' (111)
确认端口正确

如果用户使用的端口并不是服务端所侦听的,也会无法连接。

使用 --port 来指定端口,--socket 来指定命名通道或套接字文件。

服务端是否配置为忽略网络连接

启动时使用 --skip-networking,服务端会不接受任何 TCP/IP 连接;

启动时使用 --bind-address=127.0.0.1,则只会在环回接口侦听本地发起的连接,不会接受外部的任何连接。

检查防火墙设置

确认防火墙没有拦截对 MySQL 的访问。MySQL 的通信端口默认为 3306.

授权表是否配置正确

授权表必须正确地配置,才能保证服务端用它来进行访问控制。

在某些发行版中,安装进程会初始化 MySQL 的数据目录,包括含有授权表的 mysql 数据库。如果安装过程没有做这一步,就需要用户手动进行。要想确认是否需要初始化授权表,可以查看数据目录中的 mysql 目录,确保在 mysql 目录中有一个名为 user.MYD 的文件,如果没有,就初始化数据目录,然后再启动服务端。

版本更新

在现有版本 MySQL 进行过一次更新之后,授权表有时会因为某些新功能而发生结构上的变化,因此必须运行 mysql_upgrade 脚本来更新。

检查客户端默认环境

客户端程序会使用配置文件或环境变量来做为连接的参数。如果你发现明明在命令行中没有指定任何参数,却在连接时发送了错误的默认参数,应该检查一下配置文件和环境变量。

可以使用 --no-defaults 选项来绕过配置文件进行连接测试。

访问被拒绝

连接时,在返回的 Access denied 错误消息中,说明了你尝试以哪个身份去连接的,从哪个主机去连接的,以及是否使用了密码。可以根据这些信息来分析具体原因。

密码不正确:

Access denied for user 'root'@'localhost' (using password: YES)

没输入密码:

using password: NO

指定的数据库无法访问

使用 mysql -u database 启动时,如果被拒绝访问,有可能是

服务端无法解析客户端主机名
$ mysqladmin -u root -pxxxx -h some_hostname ver
Access denied for user 'root'@'' (using password: YES)

如果错误信息如上,返回的主机部分是空的,或者是 IP 地址,表明服务端无法把客户端的 IP 地址解析成主机名。

Access denied for user ''@'unknown'

这个更惨,帐户名为空,表明 user 表中没有这个帐户;主机名为 unknown 表明无法解析客户端主机名。

这些错误都是由于 DNS 解析错误导致的,因此可以有如下解决办法:

  • mysqladmin flush-hosts 重置 DNS 缓存
  • 检查 DNS 服务器的设置
  • 在 MySQL 授权表中使用 IP 地址,而不用主机名
  • /etc/hosts 加入客户端主机名条目
  • --skip-name-resolve 选项启动 mysqld
  • --skip-host-cache 选项启动 mysqld
  • 如果客户端与服务端在同一主机,连接到 localhost
用户不存在

如果返回的错误消息为:

Host ... is not allowed to connect to this MySQL server

意味着在 user 表中没有匹配客户端主机的记录。

解决:新建一个帐户名与该主机名的组合。

还有一个可能的原因是,客户端与服务端 MySQL 编译时所用的 glibc 版本不同。

解决:更新操作系统或 glibc,或下载 MySQL 的 SRPM 重新编译。

没有足够权限

如果连接以后,使用 SELECT ... INTO OUTFILELOAD DATA INFILE 语句时返回 Access denied 消息,表明该用户没有 FILE 权限。

使用调试模式

如果始终无法定位错误,可以启用调试选项,如 --debug=d,general,guery。这样,连接时,系统会打印出主机和用户信息,以及键入的每个命令的信息。

….