【MySQL 进阶系列】拒绝滥用root:从 mysql.user 到权限校验,带你彻底理解用户管理与授权机制!

🔥 本文专栏:MySQL

🌸作者主页:努力努力再努力wz


💪 今日博客励志语录所谓逆袭,不是突然变强,而是你终于把那些没人相信的坚持,熬成了证据。


思维导图

引入

在本篇博客之前,我们对于数据库表的基本操作,基本都是围绕 root 用户展开的。而我们知道,root 用户通常是 MySQL 中权限最高的超级用户,拥有对大多数数据库、数据表以及管理操作的权限。也正是因为其权限范围过大,如果在日常操作中长期使用 root 用户,一旦出现误操作,或者账号密码泄露,就可能对数据库造成较大的安全风险。

因此,在实际开发或生产环境中,通常不建议直接使用 root 用户登录数据库,而是应该根据具体的业务需求创建普通用户,并为普通用户分配必要的操作权限。对于普通用户来说,其权限是受到约束的:比如是否可以访问某个数据库,是否可以操作某张表,是否可以执行 SELECTINSERTUPDATEDELETE 等 SQL 操作,都需要由权限系统进行控制。

所以,学习 MySQL 的用户管理与权限控制,本质上就是为了让不同用户只能在被允许的范围内操作数据库,从而降低误操作和越权访问带来的风险。接下来,我们就围绕 MySQL 中用户的创建、登录身份的区分、权限的授予以及权限的回收展开分析。

MySQL 用户管理与权限控制:从授权表到权限校验

MySQL 授权表:用户身份与权限信息的底层存储

根据上文,我们已经知道,之所以建议使用普通用户登录数据库,是因为普通用户会受到相应的权限约束。而所谓权限约束,主要体现在两个方面:是否允许访问某个数据库或数据表,以及是否允许对这些数据库表执行 SELECTINSERTUPDATEDELETE 等操作

对于 MySQL 来说,除了业务数据本身之外,很多与数据库管理相关的信息,也会通过系统表的形式进行维护。例如,MySQL 内部存在一个名为 mysql 的系统数据库,它专门用于存储用户身份、权限授权、角色、时区、帮助信息等系统级元数据。其中,与用户管理和权限控制关系最密切的,便是 mysql 数据库下的一系列授权表。

在这些授权表中,最关键的表之一就是 user 表。user 表主要用于记录 MySQL 用户账号的身份信息以及全局权限信息。这里需要注意的是,MySQL 中的"一个用户"并不只是单纯由用户名决定,而是由 用户名 + 登录来源 共同确定的,也就是我们常见的:

sql 复制代码
'user_name'@'host'

例如:

sql 复制代码
'wz'@'localhost'
'wz'@'%'

虽然它们的用户名都是 wz,但由于登录来源不同,在 MySQL 看来,它们其实是两个不同的账户。

对于一个用户来说,其信息大体可以分为两部分:一部分是用户身份相关的信息,例如用户名、登录来源、认证插件、密码认证信息等;另一部分则是权限相关的信息,例如该用户是否拥有全局级别的查询、插入、更新、删除、创建用户等权限。

不过需要注意的是,MySQL 的权限信息并不是全部集中存储在 user 表中,而是采用了分级存储 的策略。也就是说,MySQL 会按照权限作用范围的不同,将权限信息存储到不同的授权表中。例如,user 表主要记录全局级别的权限,db 表记录数据库级别的权限,tables_priv 表记录数据表级别的权限,columns_priv 表记录列级别的权限。

看到这里,读者可能会产生一个疑问:既然全局权限已经可以作用于所有数据库和数据表,那么为什么还需要数据库级别、表级别、列级别这些更细粒度的权限表呢?这样不会造成重复或者冗余吗?

这个问题的关键在于:权限的作用范围不同。全局权限表示该权限可以作用于整个 MySQL 实例,而数据库级别、表级别、列级别的权限,则是为了实现更细粒度的权限控制。比如我们可以允许某个用户只访问某一个数据库,也可以只允许其查询某张表,甚至只允许其访问某张表中的部分字段。因此,MySQL 才会采用这种分级授权的设计。至于 MySQL 在执行 SQL 时具体如何进行权限认证和权限匹配,后文会继续展开分析。

既然 mysql.user 表中存储了用户相关的信息,那么从底层角度来看,创建用户、删除用户、修改用户密码,本质上都可以理解为对这些系统授权表进行相应的增删改操作。比如创建用户,本质上就是在授权表中新增一条用户记录;删除用户,本质上就是删除对应的用户记录;修改密码,本质上就是更新对应用户的认证信息。

但是在实际使用中,并不推荐直接通过 INSERTUPDATEDELETE 等方式去手动操作 mysql.user

原因很简单:user 表中的字段数量较多,除了用户名和登录来源之外,还涉及认证插件、密码认证字符串、账号状态、SSL 限制、资源限制以及大量权限字段。如果我们直接手动插入一条记录,不仅操作繁琐,还需要手动维护大量字段,出错概率较高。

其次除了字段数量较多、操作繁琐之外,更关键的问题在于,用户密码并不是简单地以明文形式存储在 mysql.user 表中。

正常情况下,当我们执行 CREATE USER ... IDENTIFIED BY ... 这类语句时,MySQL 会将用户输入的原始密码交给对应的认证插件处理,然后再将处理后的认证字符串存储到 mysql.user 表中。也就是说,mysql.user 表中保存的并不是用户输入的明文密码,而是经过认证插件处理后的认证信息。

如果我们绕过 CREATE USER 这类专门语句,直接通过 INSERTmysql.user 表中的认证字段写入原始明文密码,那么 MySQL 并不会在后续登录认证时,再把这个值当作原始密码重新交给认证插件处理。相反,MySQL 会认为我们写入的内容已经是认证插件所要求的最终认证字符串。这样一来,MySQL 后续认证时会把这个明文密码误当成已经处理完成的认证字符串来使用,导致服务端计算出的校验结果无法与客户端发送的认证响应值匹配,最终出现用户无法正常登录的问题。

进一步来说,MySQL 的登录认证过程也不是简单的"客户端发送明文密码,服务端拿来和表中密码进行比较"。在连接认证阶段,服务端通常会先向客户端发送一个随机挑战值,我们也可以将其理解为一个临时随机数。客户端收到这个随机挑战值之后,会根据当前账户使用的认证插件,对用户输入的原始密码进行哈希或其他形式的处理,并结合服务端发送的随机挑战值计算出一个认证响应值,然后将这个认证响应值发送给服务端。

服务端收到认证响应值之后,并不会将其还原成用户的明文密码,而是会取出自己在 mysql.user 表中保存的认证字符串,并结合刚才发送给客户端的随机挑战值,按照认证插件规定的算法计算出一个用于校验的结果,然后再将这个结果与客户端发送过来的认证响应值进行匹配。如果二者能够匹配,就说明客户端确实是基于正确密码计算出了认证响应值,因此认证通过;如果无法匹配,则说明客户端提供的密码不正确,认证失败。

因此,如果我们直接向 mysql.user 表中写入明文密码,就很容易导致认证字符串格式不符合插件要求,最终使后续登录认证失败。所以,CREATE USERALTER USERGRANTREVOKE 等语句,本质上可以理解为 MySQL 对底层授权表操作的一层安全封装。它们会帮助我们按照认证插件和权限系统的规则,正确维护用户身份信息与权限信息,而不是让我们直接面对复杂且容易出错的系统授权表结构。

因此,MySQL 为用户管理和权限控制提供了专门的 SQL 语句,例如:

sql 复制代码
CREATE USER
DROP USER
ALTER USER
GRANT
REVOKE

这些语句本质上仍然是在修改底层的授权信息,但它们会帮助我们屏蔽复杂的底层表结构,并保证用户信息和权限信息能够按照 MySQL 规定的格式正确维护。所以在实际开发和运维过程中,我们应该优先使用这些专门的用户管理语句,而不是直接修改 mysql.user 等系统授权表。


CREATE USER注册用户:理解 'user'@'host' 账户模型

接下来我们开始注册一个 MySQL 用户。注册用户时,最基本的信息包括用户名以及密码。根据上文我们已经知道,虽然我们在语句中输入的是明文密码,但 MySQL 并不会直接将明文密码原样存储到 mysql.user 表中,而是会根据当前账户所使用的认证插件,对明文密码进行相应处理,然后将处理后的认证字符串保存到系统授权表中。

其次需要注意的是,MySQL 中的"用户"并不是单纯由用户名本身决定的,而是由 用户名和主机来源共同组成的二元组,也就是:

sql 复制代码
'user'@'host'

其中,user 表示用户名,host 表示该用户允许从哪个客户端主机来源连接到 MySQL 服务器。因此,即使两个账户的用户名部分相同,只要它们的 host 部分不同,在 MySQL 看来,它们依然是两个不同的账户。例如:

sql 复制代码
'wz'@'localhost'
'wz'@'127.0.0.1'
'wz'@'%'

这三个账户的用户名都叫 wz,但由于主机来源不同,因此它们是三条不同的用户记录,可以分别注册成功,也可以分别授予不同的权限。

'user'@'host' 中的 host客户端主机来源的匹配条件。常见的写法包括:

sql 复制代码
'用户名'@'localhost'
'用户名'@'127.0.0.1'
'用户名'@'指定IP地址'
'用户名'@'%'

其中,localhost 通常表示本机登录。在 Linux/Unix 环境下,如果客户端没有显式指定使用 TCP/IP 连接,那么通过 localhost 连接 MySQL 时,通常会优先使用 Unix 域套接字进行通信,这是一种本机进程间通信机制,开销相对较小。

127.0.0.1 表示本机 IPv4 回环地址。虽然它同样是在本机访问 MySQL 服务,但它一般会走 TCP/IP 协议栈,因此和 localhost 并不完全等价。在 MySQL 的账户匹配中,'wz'@'localhost''wz'@'127.0.0.1' 也会被视为两个不同的账户。

如果 host 部分写成某个具体的远程 IP 地址,则表示该用户只允许从这个指定的远程主机连接到 MySQL 服务器。而如果写成 %,则表示这是一个主机来源通配符,可以匹配任意客户端主机来源。也就是说:

sql 复制代码
'wz'@'%'

表示允许名为 wz 的用户从任意主机来源尝试连接 MySQL。

不过需要注意的是,% 只是 MySQL 用户匹配规则中的主机来源通配符,并不意味着一定可以成功远程登录。实际能否连接成功,还需要同时满足 MySQL 服务监听地址正确、防火墙或云服务器安全组放行对应端口、账号密码认证通过、权限配置正确等条件。

因此,注册 MySQL 用户时,我们本质上注册的不是一个单独的用户名,而是一个具体的 'user'@'host' 账户。只有当客户端连接时提供的用户名和实际连接来源能够匹配到 mysql.user 表中的某条用户记录,并且密码认证通过后,MySQL 才会允许该连接建立。

sql 复制代码
CREATE USER 'wz'@'localhost' IDENTIFIED BY '123456';

CREATE USER 'wz'@'127.0.0.1' IDENTIFIED BY '123456';

CREATE USER 'wz'@'%' IDENTIFIED BY '123456';

需要注意的是,虽然我们在语法层面通常使用 'user'@'host' 来表示一个 MySQL 账户,但在底层授权表中,用户名和主机来源并不是作为一个整体字符串存储的,而是分别存储在 User 列和 Host 列中。也就是说,MySQL 在逻辑上通过 User + Host 共同确定一个账户身份,在物理存储上则通过两列分别保存这两个组成部分。

DROP USER:删除账户与授权信息清理

认识了用户的注册之后,接下来我们再来看用户的删除。所谓用户删除,从底层角度来看,并不是简单地只删除 mysql.user 表中的一条用户记录,而是删除该账户在 MySQL 授权体系中的相关信息。因为一个用户的权限信息并不一定全部存储在 user 表中,除了全局权限之外,还可能在数据库级权限表、表级权限表、列级权限表等授权表中保存对应的权限记录。

因此,MySQL 提供了专门的 DROP USER 语句来完成用户删除操作。其基本语法如下:

sql 复制代码
DROP USER '用户名'@'主机来源';

例如:

sql 复制代码
DROP USER 'wz'@'localhost';

这条语句删除的并不是所有名为 wz 的用户,而是删除 'wz'@'localhost' 这个具体账户。因为在 MySQL 中,用户身份是由用户名和主机来源共同组成的二元组决定的,所以 'wz'@'localhost''wz'@'127.0.0.1''wz'@'%' 在 MySQL 看来都是不同的账户。删除其中一个,并不会影响另外两个。

同时,DROP USER 的作用也不只是删除 mysql.user 表中的记录。它会移除指定账户本身,并清理该账户在相关授权表中的权限记录。也就是说,如果该账户曾经拥有数据库级、表级、列级等权限,使用 DROP USER 删除账户时,MySQL 会一并清理这些与账户相关的授权信息。

这里还可以顺便区分一下 DROP USERREVOKEREVOKE 的作用是回收用户已经拥有的某些权限,但用户账户本身仍然存在;而 DROP USER 则是删除用户账户本身,并清理该账户对应的权限记录。因此,如果只是想取消某个用户对某个数据库或表的访问权限,应该使用 REVOKE;如果这个账户已经不再需要,才应该使用 DROP USER 将其删除。

ALTER USER:修改账户认证信息与账户状态

接下来我们来看用户信息的修改。这里需要先明确一点:所谓"用户的修改",其实可以分成两个层面。

第一个层面是用户身份信息的修改 ,例如修改用户密码、修改认证插件、锁定或解锁账户等。这类操作主要使用 ALTER USER 语句完成。

第二个层面是用户权限信息的修改 ,例如给用户授予某个数据库或数据表的操作权限,或者回收用户已经拥有的权限。这类操作则主要通过 GRANTREVOKE 语句完成。其中,GRANT 用于授予权限,REVOKE 用于回收权限。

本节我们先关注用户身份信息的修改,也就是最常见的修改用户密码 。修改密码可以使用 ALTER USER 语句,其基本语法如下:

sql 复制代码
ALTER USER '用户名'@'主机来源' IDENTIFIED BY '新密码';

例如:

sql 复制代码
ALTER USER 'wz'@'localhost' IDENTIFIED BY '654321';

这里需要注意的是,修改密码时同样要指定完整的账户标识,也就是 'user'@'host'。因为在 MySQL 中,'wz'@'localhost''wz'@'%' 虽然用户名部分相同,但它们是两个不同的账户。因此,执行上面的语句时,只会修改 'wz'@'localhost' 这个账户的密码,并不会影响其他主机来源下的同名用户。

同时,IDENTIFIED BY '654321' 中的 '654321' 仍然是我们输入的原始明文密码。MySQL 在真正存储时,并不会直接将明文密码写入 mysql.user 表,而是会把它交给当前账户使用的认证插件处理,再将处理后的认证字符串保存到底层授权表中。这样既保证了密码不会以明文形式存储,也保证了后续登录认证时能够按照认证插件的规则正常完成校验。

这里顺便补充一下 ALTER USER 中的账户锁定操作。所谓账户锁定,并不是删除用户,也不是回收用户已经拥有的权限,而是将该账户标记为不可登录状态。

例如:

sql 复制代码
ALTER USER 'wz'@'localhost' ACCOUNT LOCK;

需要注意的是,执行之后,'wz'@'localhost' 这个账户本身仍然存在,账户锁定主要影响的是后续的新连接认证。也就是说,如果该账户在被锁定之前已经建立了连接,那么这个已有连接通常不会因为账户被锁定而被 MySQL 立即主动断开;但是当该账户后续再次尝试建立连接时,就会在认证阶段被拒绝,从而无法完成新的连接建立。

如果后续需要重新启用该账户,可以使用:

sql 复制代码
ALTER USER 'wz'@'localhost' ACCOUNT UNLOCK;

所以,ACCOUNT LOCK 更适合用于临时禁用某个账户的场景。它和 DROP USERREVOKE 的作用并不相同:DROP USER 是删除账户本身,并清理相关授权信息;REVOKE 是回收账户的某些权限,但账户仍然可以存在并尝试登录;而 ACCOUNT LOCK 则是在保留账户和权限信息的前提下,暂时禁止该账户建立新的连接。

GRANT授权:理解操作类型与作用范围

接下来我们来看权限的授予。根据上文可知,MySQL 的权限信息是按照作用范围分级存储的,例如 user 表主要记录全局级权限,而其他授权表则分别记录数据库级、表级、列级等更细粒度的权限。因此,在授予权限时,也需要明确权限的作用范围。

首先来看全局权限。所谓全局权限,指的是作用于整个 MySQL Server 实例范围内的权限。全局权限通常使用 *.* 表示授权范围,其中第一个 * 表示所有数据库,第二个 * 表示所有数据表。例如:

sql 复制代码
GRANT SELECT ON *.* TO 'wz'@'localhost';

这条语句表示授予 'wz'@'localhost' 对当前 MySQL 实例中所有数据库、所有数据表的查询权限。需要注意的是,全局权限强调的是权限的作用范围,而不是默认拥有所有操作能力。具体能执行哪些操作,仍然取决于授予的是 SELECTINSERTUPDATEDELETE,还是 ALL PRIVILEGES 等具体权限类型。

全局权限说的是"范围大",不是天然"权限全"。


接下来,我们再从全局权限过渡到更细粒度的权限控制。

在前面介绍的全局权限中,*.* 表示权限作用于当前 MySQL 实例下的所有数据库以及所有数据表 。但是在实际使用中,我们通常并不希望一个普通用户能够在所有数据库范围内拥有操作能力。例如,某个用户只负责 wz_db 这个数据库,那么就没有必要让它访问其他数据库。

因此,我们可以将权限的作用范围从全局级别缩小到数据库级别。比如:

sql 复制代码
GRANT SELECT ON wz_db.* TO 'wz'@'localhost';

这里的 wz_db.* 表示权限只作用于 wz_db 数据库下的所有表。也就是说,该用户只能在 wz_db 这个数据库范围内拥有对应权限,而不会影响其他数据库。

不过这里需要注意,wz_db.* 限定的只是权限的作用范围 ,并不代表该用户一定能够对 wz_db 下的所有表执行所有操作。用户具体能够执行哪些操作,还要看 GRANT 后面授予的是哪一种权限。

比如:

sql 复制代码
GRANT SELECT ON wz_db.* TO 'wz'@'localhost';

表示该用户只能查询 wz_db 数据库下的表;而:

sql 复制代码
GRANT SELECT, INSERT, UPDATE, DELETE ON wz_db.* TO 'wz'@'localhost';

才表示该用户可以在 wz_db 数据库下执行基本的增删改查操作。

所以,理解 MySQL 权限授予时,需要始终拆成两个维度来看:

text 复制代码
GRANT 后面决定:允许执行什么操作
ON 后面决定:这些操作作用在哪些对象范围内

也就是说,ON *.* 表示全局范围,ON 数据库名.* 表示某个数据库范围,后续还可以继续细化到某一张具体的数据表,甚至某张表中的某些字段。权限控制正是通过这种"操作类型 + 作用范围"的组合,实现对不同用户的精细化约束。


顺理成章地,既然权限的作用范围可以从全局级别缩小到数据库级别,那么它也可以继续缩小到更细粒度的表级别。也就是说,我们不仅可以控制某个用户是否能够访问某个数据库,还可以进一步控制该用户是否能够操作这个数据库下的某一张具体数据表。

例如:

sql 复制代码
GRANT SELECT ON wz_db.student TO 'wz'@'localhost';

这条语句表示:授予 'wz'@'localhost'wz_db 数据库中 student 表的查询权限。其中,SELECT 表示允许执行的操作类型,wz_db.student 则表示该权限的作用范围被限定在 wz_db 数据库下的 student 表上。

需要注意的是,表级授权并不是对用户已有权限范围的"收缩",而是在指定表这个粒度上为用户增加一条权限规则。对于同一个用户来说,它可以同时拥有全局级、数据库级、表级甚至列级的多条授权记录,MySQL 在权限校验时会综合这些授权信息进行判断。因此,只有在该用户没有其他更大范围权限的前提下,表级授权才表现为"该用户只能在指定表上拥有对应操作能力"。如果该用户之前已经拥有全局级或数据库级权限,那么单独执行一条表级GRANT 并不会自动取消这些更大范围的权限。

为了更直观地理解这一点,我们来看一个例子。假设现在有一个用户:

sql 复制代码
'wz'@'localhost'

一开始,我们给这个用户授予了全局查询权限:

sql 复制代码
GRANT SELECT ON *.* TO 'wz'@'localhost';

这里的 *.* 表示所有数据库下的所有表,因此该用户此时可以查询当前 MySQL 实例中的任意数据库和任意数据表。

比如下面这些操作都是允许的:

sql 复制代码
SELECT * FROM wz_db.student;
SELECT * FROM wz_db.course;
SELECT * FROM test_db.user_info;

接下来,如果我们再执行一条表级授权语句:

sql 复制代码
GRANT SELECT ON wz_db.student TO 'wz'@'localhost';

这条语句的含义只是:额外授予该用户对 wz_db.student 表的查询权限。它并不会把用户原来拥有的全局查询权限"收缩"到 wz_db.student 这一张表上。

也就是说,执行完这条表级 GRANT 之后,该用户仍然可以查询其他数据库和其他表,因为它之前拥有的全局 SELECT ON *.* 权限仍然存在。

如果我们的目标是让该用户只能查询 wz_db.student 这一张表,那么就不能只执行表级授权,而是应该先回收原来的全局权限:

sql 复制代码
REVOKE SELECT ON *.* FROM 'wz'@'localhost';

然后再只授予表级权限:

sql 复制代码
GRANT SELECT ON wz_db.student TO 'wz'@'localhost';

这样,该用户最终才只拥有 wz_db.student 表上的查询权限。此时:

sql 复制代码
SELECT * FROM wz_db.student;

可以执行,而:

sql 复制代码
SELECT * FROM wz_db.course;
SELECT * FROM test_db.user_info;

就会因为没有对应权限而执行失败。

因此要注意:GRANT 是在原有权限基础上继续增加权限,而不是缩小权限范围。真正想缩小权限范围,需要先通过 REVOKE 回收更大范围的权限,再重新授予更小范围的权限。

从底层存储角度来看,不同粒度的权限信息也会被记录到不同的授权表中。全局级权限主要记录在 mysql.user 表中,数据库级权限记录在 mysql.db 表中,而表级权限则会记录在对应的表级授权表中。MySQL 正是通过这种分级存储和分级匹配的方式,实现了从全局、数据库、数据表到字段级别的细粒度权限控制。

REVOKE回收权限:精确移除指定粒度的授权记录

接下来我们来看权限的回收,也就是 REVOKE 语句。

所谓权限回收,可以理解为 GRANT 授权操作的反向操作。既然 GRANT 是在某个作用范围上,为用户授予某种操作能力,那么 REVOKE 就是在某个作用范围上,从用户身上移除某种已经授予过的操作能力。

因此,理解 REVOKE 时,同样需要拆成两个维度来看:

text 复制代码
REVOKE 后面决定:要回收哪一种操作权限
ON 后面决定:从哪个作用范围上回收该权限

例如:

sql 复制代码
REVOKE SELECT ON wz_db.student FROM 'wz'@'localhost';

这条语句的含义就是:从 'wz'@'localhost' 这个账户身上,回收其对 wz_db 数据库中 student 表的查询权限。

这里需要特别注意的是,MySQL 中一个用户可以同时拥有多个不同粒度的权限记录。例如,同一个用户既可以拥有全局级权限,也可以拥有数据库级权限、表级权限,甚至列级权限。MySQL 在判断一个操作是否允许执行时,并不是简单地按照某个粒度进行"覆盖",而是会综合匹配该用户在不同粒度上的授权记录。

例如,假设我们先授予用户一个全局查询权限:

sql 复制代码
GRANT SELECT ON *.* TO 'wz'@'localhost';

然后又单独授予它某张表的查询权限:

sql 复制代码
GRANT SELECT ON wz_db.student TO 'wz'@'localhost';

此时,这个用户身上实际上同时存在两条权限记录:一条是全局范围的 SELECT 权限,另一条是 wz_db.student 表上的 SELECT 权限。

如果后续我们执行:

sql 复制代码
REVOKE SELECT ON *.* FROM 'wz'@'localhost';

那么这条语句只会回收该用户在 *.* 这个全局范围上的 SELECT 权限,并不会自动回收它在 wz_db.student 表上单独拥有的 SELECT 权限。也就是说,全局查询权限被回收之后,该用户不再能够查询所有数据库下的所有表,但由于表级查询权限仍然存在,所以它仍然可以查询 wz_db.student 这张表。

因此,REVOKE 的回收是针对某个具体作用范围、某种具体操作权限进行的。回收较大范围的权限,并不会自动让更小范围上单独存在的权限失效。如果想彻底回收某个用户的相关权限,就需要结合 SHOW GRANTS 查看该用户当前拥有的权限记录,然后针对不同作用范围分别执行对应的 REVOKE 语句。

查看用户权限可以使用:

sql 复制代码
SHOW GRANTS FOR 'wz'@'localhost';

该语句会以 GRANT 语句的形式展示当前用户已经拥有的权限,方便我们判断后续应该回收哪些权限。

所以,GRANTREVOKE 可以这样理解:

text 复制代码
GRANT  :增加权限记录
REVOKE :移除权限记录

二者都必须明确操作类型和作用范围。不同粒度的权限可以同时存在,并且会在权限校验时被综合考虑,而不是简单地互相覆盖。

权限校验:从连接认证到 SQL 执行权限判断

最后,我们再来看 MySQL 的权限校验过程。

MySQL 本质上是一个多用户的客户端-服务器架构的网络服务进程。客户端需要先与 mysqld 服务端建立网络连接,然后进入 MySQL 协议的连接认证阶段。在这个阶段,服务端会根据客户端提供的用户名、客户端主机来源以及认证信息,判断该账户是否存在、是否允许从当前主机来源登录,以及密码认证是否能够通过。

只有认证通过之后,客户端和服务端之间才会形成一个可用的 MySQL 会话连接。而这个连接一旦建立成功,后续该连接中执行的所有 SQL 语句,都会在这个特定的用户身份下执行。

也就是说,连接认证阶段解决的是:

text 复制代码
你是谁?

而 SQL 执行阶段的权限校验解决的是:

text 复制代码
你有没有资格执行当前这条 SQL?

MySQL 在内部会为每一个客户端连接维护对应的连接上下文或者也可以理解为会话上下文对象即 THD (Thread Descriptor)。这个上下文中会保存当前连接对应的用户身份、当前默认数据库、当前执行的 SQL 语句以及其他执行状态信息。因此,当客户端发送 SQL 请求时,MySQL 就能够知道:这条 SQL 是由哪个用户发起的,以及它想要访问哪些数据库、哪些表、哪些字段。

cpp 复制代码
// THD 可以理解为 MySQL 为每个客户端连接维护的线程/会话上下文对象

struct Security_context
{
    std::string user;        // 当前认证用户,例如 "wz"
    std::string host;        // 客户端来源,例如 "localhost" / "127.0.0.1"
    std::string ip;          // 客户端 IP
};

struct Parsed_sql
{
    std::string operation;   // SQL 操作类型,例如 SELECT / INSERT / UPDATE / DELETE
    std::string db;          // 要访问的数据库
    std::string table;       // 要访问的数据表
    std::string column;      // 要访问的列,简化表示
};

struct THD
{
    int conn_fd;                 // 当前客户端连接对应的 socket fd

    Security_context security;   // 当前连接对应的用户身份上下文

    std::string current_db;      // 当前默认数据库
    std::string query_string;    // 当前正在执行的 SQL 语句

    Parsed_sql parsed_sql;       // SQL 解析后得到的访问对象信息
};

当 Server 层接收到客户端发送过来的 SQL 语句之后,会先对 SQL 进行解析,判断语法是否正确;随后在名称解析和执行准备过程中,确定该语句要访问的数据库、数据表以及字段信息。接下来,MySQL 还会进行一个非常关键的步骤:权限校验

权限校验的核心,就是根据当前连接对应的用户身份,判断该用户是否拥有执行当前 SQL 所需要的权限。例如,用户执行的是:

sql 复制代码
SELECT * FROM wz_db.student;

那么 MySQL 就需要判断当前用户是否拥有对 wz_db.student 表的 SELECT 权限。

而根据前面的分析可知,MySQL 的权限信息是按照不同粒度分级存储的。因此在进行权限校验时,MySQL 也会结合不同粒度的授权信息进行判断。比如,MySQL 会先查看该用户是否拥有全局级权限;如果全局级权限不足,再继续结合数据库级权限;如果仍然不足,则继续检查表级权限、列级权限等更细粒度的授权信息。最终,MySQL 会综合这些权限记录,判断当前用户是否可以执行这条 SQL 语句。

这里需要注意的是,权限校验并不是只查看某一张授权表,而是会根据当前 SQL 要访问的对象和执行的操作类型,综合不同粒度的授权表一起判断。比如,全局权限主要查看 mysql.user 表,数据库级权限主要查看 mysql.db 表,表级权限主要查看 mysql.tables_priv 表,列级权限主要查看 mysql.columns_priv 表。如果当前用户在全局级、数据库级、表级、列级等相关权限粒度中,都没有匹配到能够支持当前 SQL 操作的权限,那么 MySQL 才会判定该用户权限不足,并拒绝执行这条 SQL。

text 复制代码
检查全局权限是否足够
        ↓ 不足
继续检查数据库级权限是否足够
        ↓ 不足
继续检查表级权限是否足够
        ↓ 不足
继续检查列级权限是否足够
        ↓ 仍然不足
最终判定权限不足,SQL 执行失败

例如,如果用户拥有全局级的 SELECT 权限:

sql 复制代码
GRANT SELECT ON *.* TO 'wz'@'localhost';

那么它就可以查询当前 MySQL 实例下所有数据库中的所有表。

如果用户只拥有数据库级权限:

sql 复制代码
GRANT SELECT ON wz_db.* TO 'wz'@'localhost';

那么它只能查询 wz_db 数据库下的表,而不能查询其他数据库中的表。

如果用户只拥有表级权限:

sql 复制代码
GRANT SELECT ON wz_db.student TO 'wz'@'localhost';

那么它只能查询 wz_db 数据库中的 student 表,而不能自动查询同一个数据库下的其他表。

所以,MySQL 的权限校验可以简单理解为:先确定当前连接对应的用户身份,再根据当前 SQL 要执行的操作类型和访问对象,到不同粒度的授权信息中进行匹配,最终判断是否放行。

为了观察这些权限信息,我们可以查看对应的授权表。例如,全局级权限主要存储在 mysql.user 表中,数据库级权限主要存储在 mysql.db 表中。我们可以使用类似下面的 SQL 查看某个用户在这些授权表中的权限字段:

sql 复制代码
SELECT user, host, Select_priv, Insert_priv, Update_priv, Delete_priv
FROM mysql.user
WHERE user = 'wz';

或者查看数据库级权限:

sql 复制代码
SELECT user, host, db, Select_priv, Insert_priv, Update_priv, Delete_priv
FROM mysql.db
WHERE user = 'wz';

在这些授权表中,很多权限字段会以 YN 的形式进行存储。其中,Y 表示该用户在当前授权表对应的作用范围内拥有该操作权限,N 则表示没有该操作权限。

不过,直接查看授权表更偏向于观察 MySQL 底层是如何存储权限信息的。在实际排查用户权限时,更常用的方式是使用:

sql 复制代码
SHOW GRANTS FOR 'wz'@'localhost';

该语句会以 GRANT 语句的形式展示当前用户已经拥有的权限,更方便我们直观判断这个用户到底被授予了哪些操作能力,以及这些权限分别作用在哪些范围内。

因此,MySQL 的权限管理可以总结为两个阶段:连接认证阶段负责确认"当前连接对应哪个用户",而 SQL 执行阶段的权限校验负责确认"这个用户是否有资格执行当前这条 SQL"。正是通过这两个阶段,MySQL 才能够在多用户场景下,对不同用户的访问范围和操作能力进行约束。

结语

那么这就是本篇文章的全部内容,下一期我会更新MySQL的访问,我会持续更新,希望你能够多多关注,如果本文有帮助到你的话,还请三连加关注,你的支持就是我创作的最大动力!感谢各位大佬对我的支持!

相关推荐
超梦dasgg1 小时前
智慧充电系统订单服务Java 实现方案
java·开发语言·微服务
薛定谔的悦1 小时前
储能充放电状态机执行逻辑详解
linux·数据库·能源·储能·bms
雪度娃娃1 小时前
基于TCP的网络词典
网络·c++·tcp/ip·c#
装杯让你飞起来啊1 小时前
Kotlin List / Array 与 for 循环
开发语言·kotlin·list
南滑散修1 小时前
红黑树-非黑即红
java·开发语言
HaiXCoder1 小时前
AndroidAutoSize 框架原理分析与核心问题
android
Elastic 中国社区官方博客2 小时前
Elasticsearch percolator 用于电商搜索治理:将模糊查询转换为可控的检索策略
大数据·数据库·人工智能·elasticsearch·搜索引擎·ai·全文检索
zxrhhm2 小时前
PostgreSQL 中的层级查询 Oracle CONNECT BY 替代方案
数据库·postgresql·oracle