☰
MySQL用户与权限管理实战:从用户表到角色授权完整指南
2026/10/22 11:16:08 网站建设 项目流程

1. MySQL用户体系先搞清楚:user表到底存了什么

1.1 用户不是数据库名,也不是登录名

很多人刚开始接触MySQL用户管理时,容易把“用户”和“数据库”混为一谈。比如说,公司新来一个数据分析师,需要给他开一个只读账号看线上报表,不少人会直接执行一句GRANT SELECT ON report_db.* TO 'analyst'@'%';,然后发现居然能登录——但怎么也记不清这句语句到底创建了什么。

这里面的核心区分是:MySQL里的“用户”是一个连接身份,不是数据库本身,也不是操作系统的账号。它由三部分组成:用户名、来源主机、密码(以及认证插件等)。而数据库是存储数据的逻辑容器,两者是通过授权关系绑定在一起的。换句话说,用户是对“谁能以什么身份从哪台机器连上来”的描述,权限是对“连上来之后能碰哪些对象”的描述。

我见过不少运维事故,本质都是把用户当成了MySQL实例层面的全局对象,却忽略了“用户”和“权限”是两个可以独立管理的维度。MySQL 8.0之前,GRANT语句可以顺手把用户建出来;MySQL 8.0之后,官方明确建议先用CREATE USER创建账号,再用GRANT授权,两件事分开做。这样看起来啰嗦,但好处是责任边界清楚:建账号是建账号,授权限是授权限,后续排查问题时能少绕很多弯。

1.2 连接身份的三要素:用户名、主机、密码

每次成功连接MySQL,服务器都会在mysql.user表里匹配一条记录,匹配条件就是“用户名 + 来源主机 + 密码校验”。

其中最容易忽略的是“来源主机”。一位工程师曾经为了图省事,把所有账号都建成了'app'@'%',只允许从办公网连数据库的账号也配成了%,结果某天数据库出现大量慢查询,排查了半天才发现是有业务测试环境的机器也在用同一个账号连接——主机限制形同虚设。

%不是不能拿来做主机匹配,但要非常清楚地知道它意味着什么:任何IP都可以尝试用这个账号登录。安全一点的做法是精确到具体IP或者网段,比如'app'@'192.168.10.%',这样即使密码泄露,攻击者也要先进入指定网段才能利用。

密码也分版本。MySQL 5.7及之前用的是mysql_native_password这种比较老的认证插件,8.0默认换成了caching_sha2_password。很多老客户端驱动不支持新插件,就会出现“账号密码都对,但连不上”的诡异现象。实际运维中,如果遇到旧驱动兼容问题,可以临时为某个账号指定老插件,但一定要做技术债登记,后续推动客户端升级,而不是让所有账号都迁回旧插件。

1.3 一张user表看懂全局:常用字段拆解

mysql.user表是MySQL用户信息的核心存储,但直接用SELECT * FROM mysql.user\G;会输出几十个字段,新手容易看花眼。我一般只看这么几个关键字段:

字段名含义常见值
User用户名root / app / readonly 等
Host允许登录的来源主机localhost / % / 192.168.1.%
plugin认证插件caching_sha2_password / mysql_native_password
authentication_string密码的哈希值(不是明文)一长串随机字符
account_locked账号是否锁定N / Y
password_expired密码是否已过期N / Y
password_last_changed密码最近修改时间时间戳

这些字段中,Host和User合起来做主键,所以同一个用户名可以对应多条记录:'app'@'localhost'和'app'@'192.168.10.%'是不同的记录,可以配置不同的密码和权限。

我排查“为什么新加的账号怎么都登不上去”时,第一件事永远是先查mysql.user表,确认用户记录是否真的存在。有时候CREATE USER执行成功,但实际当前连接的节点是只读节点,数据并没有同步到写节点,客户端自然连不上。这类问题用方向看很常见,但一步步查下来也就十分钟。

2. 创建用户与改密的正确姿势

2.1 先CREATE USER再GRANT:顺序决定了排查成本

拿MySQL 8.0举例,最标准的创建用户流程是这个样子:

CREATE USER 'readonly_user'@'192.168.10.%' IDENTIFIED BY 'Str0ng!Passwd'; GRANT SELECT ON report_db.* TO 'readonly_user'@'192.168.10.%'; FLUSH PRIVILEGES;

有些老教程会告诉你GRANT可以顺带建用户,比如:

GRANT SELECT ON report_db.* TO 'old_style'@'%' IDENTIFIED BY 'Passw0rd';

这条语句在5.7里确实可以运行,但到了8.0,它要么直接报错,要么给出警告,因为官方已经把这个逻辑去掉了。为什么去掉?因为“顺带建用户”会让权限回收和账号清理的边界变得模糊——你想回收权限,结果发现账号还在;你想删账号,又担心还有权限没清干净。分开执行后,每一个动作都指向唯一目标,审计和排错都清晰得多。

干活时我还习惯在CREATE USER后面加上ACCOUNT LOCK和PASSWORD EXPIRE,先把账号建出来锁住,等密码通过安全渠道交给使用方,再解锁:

CREATE USER 'temp_import'@'127.0.0.1' IDENTIFIED BY 'Init@123456' ACCOUNT LOCK PASSWORD EXPIRE;

这样能避免“账号建好了还没交接就被别人抢先连上来”的尴尬。默认情况下,PASSWORD EXPIRE会要求使用方在首次登录后立即修改密码,适合临时交接场景。

2.2 主机限定:为什么我从不随便给“%”

'%'看起来很省事,但它带来两个问题:安全边界消失,以及DNS反查可能拖慢连接速度。MySQL在收到连接请求时,如果主机配置不是IP而是主机名,会做反向解析。虽然%不需要反查,但'app'@'%.example.com'这类配置会引入解析开销,而且一旦DNS出问题,连接就会超时。

我的习惯是按网络区域拆分:

  • 'app_server'@'192.168.10.0/255.255.255.0':后端服务专用账号,只允许内网网段连接;
  • 'dba'@'127.0.0.1'和'dba'@'192.168.1.%':管理账号,只允许跳板机网段连;
  • 'readonly_report'@'10.0.0.%':报表系统只读账号,限定报表服务器所在网段。

注意MySQL的主机匹配支持IP掩码写法,但实际运维中我更推荐直接写192.168.10.%这种形式,简单明了;网段比较大的时候才考虑掩码。

这里还有个细节:'root'@'localhost'和'root'@'127.0.0.1'虽然是同一个用户名,但在MySQL看来是两条记录。有些人只改了'root'@'localhost'的密码,然后拿127.0.0.1去连,发现鬼打墙——旧密码还能用。查一下user表才发现是两个条目,这种情况在新手期非常普遍。

2.3 改密码不是只有SET PASSWORD一条路

MySQL 8.0下改密码的常用方式有这几种:

-- 方式一:建议使用 ALTER USER 'app'@'192.168.10.%' IDENTIFIED BY 'New#Pass123'; -- 方式二:自己先算好哈希再设置 ALTER USER 'app'@'192.168.10.%' IDENTIFIED WITH caching_sha2_password BY 'New#Pass123';

SET PASSWORD FOR 'xxx'@'xxx' = '明文密码'这种写法在8.0之前的版本比较常见,但新版更推荐ALTER USER。原因不只是语法统一,更重要的是ALTER USER还能顺带处理密码过期、账号锁定、认证插件切换等状态,一个语句搞定多个属性。

改完密码之后有一个很多人不知道的细节:已经建立的连接不会断。MySQL不会因为密码修改就把在线会话踢掉,只有下一次新连接才会用新密码校验。所以线上“改密即断连”的预期本身就不成立,实际需要阻断现有会话时,要配合KILL CONNECTION或者KILL QUERY来做。

2.4 实测:改密之后旧连接为什么还活着

我有一次给生产库的某个业务账号轮换密码,凌晨两点改完,群里马上有人反馈“旧的定时任务还在跑”,我当时也愣了一下。后来查了官方文档和相关资料才确认:MySQL的密码认证发生在连接初始化阶段,已认证通过的会话在生命周期内不会再次校验密码。

处理办法很简单,先把这些会话找出来:

SELECT id, user, host, db, command, time, state FROM information_schema.processlist WHERE user = 'app'@'192.168.10.%';

确认哪些会话是历史遗留,然后逐条KILL:

KILL 12345;

这才是“改密立即生效”的完整闭环。否则你辛辛苦苦改了密码,老的连接还挂着,定时任务也还在用旧权限跑,安全目标根本没达到。这也是很多公司做“密码轮换”时只改密码、不杀会话,最后审计发现还有一堆残留连接的原因。

3. MySQL权限体系全拆解:五层模型的那点事

3.1 权限层级:全局、库、表、列、例程

MySQL的权限不是一张扁平的清单,而是分层的。理解分层是避免“为什么我给用户授了库权限,但存储过程还执行不了”这种问题的前提。

简单画一个层次关系:

  1. 全局权限:作用在整个MySQL实例上,存在mysql.user表里,比如SELECT、PROCESS、SUPER;
  2. 库权限:作用在某个数据库上,存在mysql.db表里,比如SELECT ON db1.*;
  3. 表权限:作用在具体表上,存在mysql.tables_priv表里,比如SELECT ON db1.t1;
  4. 列权限:作用在表的某些列上,存在mysql.columns_priv表里,比如只授SELECT (col1);
  5. 例程权限:作用在存储过程和函数上,存在mysql.procs_priv表里。

MySQL在判断用户能否执行某操作时,是按照“全局→库→表→列”的优先级逐级放宽还是收紧呢?其实取决于权限类型:全局权限是超集,一旦全局有SELECT,库级、表级怎么限制都没用;反过来,全局没有,就看库权限;库没有,看表权限;表没有,看列权限。

这就是为什么你给一个用户设了“不能查某张表”的限制,但如果这个用户在全局或者库级别有SELECT,限制根本不生效——权限判断逻辑默认是“有就放行”,而不是“细粒度优先”。

3.2 权限粒度怎么选:不是越细越好

权限粒度越细,控制的灵活性越高,但维护成本也呈指数上升。一个库可能有几十张表,如果每张表都单独授权,那么权限列表会非常长,新的开发同事离职、项目下线时,清理工作会变成噩梦。

我的经验法则:

  • 普通业务账号给到库级权限,比如SELECT, INSERT, UPDATE, DELETE ON app_db.*;
  • 跨库访问很少的账号也尽量库级,不要动不动就给全局;
  • 只有那种“只需要查两张核心表”的报表账号,才给它表级权限;
  • 列级权限能不用就不用,一方面性能上有额外开销,另一方面权限矩阵复杂到后面没人能看懂。

例如下面这个场景:只让报表账号查order_db里的order_main和order_item两张表,不给整个库权限。

CREATE USER 'report_job'@'10.0.0.%' IDENTIFIED BY 'R3port@123'; GRANT SELECT ON order_db.order_main TO 'report_job'@'10.0.0.%'; GRANT SELECT ON order_db.order_item TO 'report_job'@'10.0.0.%';

这样做的好处是,将来如果order_db里加了一张敏感表,报表账号天然没有权限,不会因为库级授权“顺手”就看到了。

3.3 GRANT授权的一个隐蔽坑:WITH GRANT OPTION

WITH GRANT OPTION是个非常容易被忽视的权限,它的意思是“允许该用户把他拥有的权限再授予其他用户”。

举例:

GRANT SELECT ON report_db.* TO 'owner'@'192.168.10.%' WITH GRANT OPTION;

等于是把report_db.*的SELECT权力放给了owner,而且owner还能把SELECT权限授给别人。这在一个多人协作的数据库环境中很容易失控:A授权给B,B授权给C,最后出现了一堆你不知道从哪冒出来的账号,审计一查全是“谁的权限来自谁都理不清”。

我在实际管理中很少给普通账号WITH GRANT OPTION,只有DBA账号才需要。查询权限的授予来源可以用:

SELECT * FROM mysql.tables_priv WHERE Grantor IS NOT NULL;

Grantor字段记录了这个权限是哪个账号授出去的,能帮你理清授权链。

3.4 FLUSH PRIVILEGES到底什么时候用

网上有大量的教程说“改完权限一定要执行FLUSH PRIVILEGES”,这个说法其实把因果关系搞反了。

FLUSH PRIVILEGES的作用是让MySQL重新读取权限表,仅此而已。用GRANT、REVOKE、CREATE USER、ALTER USER这些语句修改权限时,MySQL会立刻更新内存中的权限缓存,不需要执行FLUSH;用INSERT、UPDATE、DELETE直接修改系统表,比如手动往mysql.user里插记录,MySQL不会感知变化,才需要FLUSH。

我见过某运维同学因为懒得写规范的授权SQL,直接UPDATE mysql.user SET ...,然后不FLUSH,结果用户怎么都连不上,折腾了半小时才发现系统表改了但没生效。正确姿势是:允许用INSERT/UPDATE修表的人只有你一个,并且每次修改后必须FLUSH。但更好的习惯是,打消手动改表的念头——所有变更都用SQL标准语句来做,系统表只读。

4. 角色管理:8.0多账号维护的利器

4.1 为什么角色比你一个个授权强

MySQL从8.0开始正式引入了角色(Role)概念。角色不是账号,它是一种可以被授予权限的集合体,然后再把这个集合体赋给多个用户。

举个真实场景:某公司有三个后端服务,每个服务都需要访问三个库,分别是order_db(读写)、user_db(只读)、log_db(只读)。如果不用角色,你得给每个服务账号分别授三套权限:

GRANT SELECT, INSERT, UPDATE, DELETE ON order_db.* TO 'svc_order'@'%'; GRANT SELECT, INSERT, UPDATE, DELETE ON order_db.* TO 'svc_pay'@'%'; GRANT SELECT, INSERT, UPDATE, DELETE ON order_db.* TO 'svc_refund'@'%';

三个账号还勉强能接受,如果账号数量到二十个,权限变更就是一场灾难:某天需要给order_db增加一个EXECUTE权限,你得改二十遍,漏一个就出问题。

有了角色,做法完全不一样:

CREATE ROLE 'r_order_rw', 'r_user_ro', 'r_log_ro'; GRANT SELECT, INSERT, UPDATE, DELETE ON order_db.* TO 'r_order_rw'; GRANT SELECT ON user_db.* TO 'r_user_ro'; GRANT SELECT ON log_db.* TO 'r_log_ro'; GRANT 'r_order_rw' TO 'svc_order'@'%'; GRANT 'r_order_rw', 'r_user_ro', 'r_log_ro' TO 'svc_pay'@'%';

之后不管账号怎么缩放,权限变更只需要改角色的授权,所有继承该角色的账号自动同步。效率和安全边界都清晰很多。

4.2 角色默认不生效:activate它

角色授权给用户之后,有一个非常容易踩的坑:角色默认是“非激活”状态。也就是说,你把角色赋给账号,然后去用这个账号连接,发现权限并没有加上,需要执行:

SET DEFAULT ROLE 'r_order_rw' TO 'svc_pay'@'%';

或者让用户自己连接后执行:

SET ROLE 'r_order_rw';

为什么这么设计?官方给出的考量是,一个用户可以拥有多个角色,但每个角色代表不同的权限边界,默认全开可能造成权限扩散。比如同一个DBA账号,平时使用只读角色,只在需要变更时临时切到管理角色,这样能降低误操作风险。

实际工作中,我的习惯是给账号一次性把所有需要的角色都设为默认角色:

SET DEFAULT ROLE ALL TO 'svc_pay'@'%';

这样简化了“为什么角色给了却看不到权限”的困惑。如果你追求更严格的控制,也可以只默认激活部分角色,需要时再动态切换,但要做好团队培训和文档说明,否则大家会以为是系统Bug。

4.3 角色与直接授权的叠加规则

一个用户可能既被直接授予了库权限,又拥有一个带有库权限的角色。MySQL的权限合并原则是“取并集”。举个例子:

  • 用户svc_pay直接有SELECT ON order_db.*的权限;
  • 用户又被赋予角色r_order_rw,这个角色含SELECT, INSERT, UPDATE, DELETE ON order_db.*;
  • 最终svc_pay在order_db上的权限是SELECT、INSERT、UPDATE、DELETE的并集。

如果你试图“用角色来限制已授权用户的权限”,对不起,做不到。MySQL不是Linux的文件权限,不存在“角色覆盖直接授权”的机制。权限只有叠加,没有互相屏蔽。

这一点直接影响权限规划:你不能因为某个角色里不含DELETE,就期待给用户直接授予了DELETE之后还能被角色拦住。要限制一个用户,正确做法是把直接授权回收,只留角色授权。

REVOKE DELETE ON order_db.* FROM 'svc_pay'@'%';

然后角色里的DELETE,如果也想去掉,只能从角色本身回收:

REVOKE DELETE ON order_db.* FROM 'r_order_rw';

权限管理里最怕的就是“我以为角色的权限会限制用户的直接权限”,这个误解会导致越权访问,而且极难追踪。

5. 用户操作风险控制:权限回收、账号下线与只读防护

5.1 收权限:REVOKE的坑和替代方案

回收权限的标准语法是:

REVOKE INSERT, UPDATE, DELETE ON order_db.* FROM 'svc_refund'@'%';

这看起来很简单,但实际中容易忽略一个问题:REVOKE只能回收GRANT授出去的权限。如果权限是通过角色授出的,直接对用户执行REVOKE并不会生效,你需要先回收角色,或者从角色里回收权限:

-- 从用户回收角色 REVOKE 'r_order_rw' FROM 'svc_refund'@'%'; -- 或者从角色回收权限 REVOKE DELETE ON order_db.* FROM 'r_order_rw';

在执行回收之前,建议先看看这个用户实际的有效权限是什么:

SHOW GRANTS FOR 'svc_refund'@'%';

它会列出直接授权和角色授权,是判断回收范围的第一手依据。

还有一种常见的替代方案:把user表里面的权限字段直接置为N。但我不推荐,除非你很清楚自己在做什么——这种操作绕过了权限缓存,不FLUSH不生效,而且还容易误改其他字段,百害而无一利。

5.2 删除用户前,先查会话再动手

删除用户比创建用户危险得多。一个人说删就删,可能下一秒线上服务就报Access denied。我自己的标准流程是三步走:

  1. 先查这个用户的会话是否还在运行:
SELECT id, user, host, db, command, time FROM information_schema.processlist WHERE user LIKE 'svc_refund%';
  1. 确认会话后,通知相关方停掉任务,等会话自然结束,或者手动KILL。

  2. 执行删除:

DROP USER 'svc_refund'@'%';

别看这三步简单,真出故障的场景往往是跳过了第二步。账号已经DROP了,但老连接因为不断开,任务还在跑,业务方根本感知不到,直到连接被网络层切断才炸出问题。所以删除用户前务必把“在线会话”当成第一排查项。

如果是下线一个即将废弃的应用,更稳妥的做法是先把账号锁定:

ALTER USER 'svc_refund'@'%' ACCOUNT LOCK;

观察一段时间,确认没有业务依赖,再DROP。锁定比删除温和得多,随时可以解锁恢复,生产环境建议优先用这套。

5.3 只读账号是线上环境的必备品

很多人觉得“只读账号”只给报表和数据分析用。其实不然,我给自己的建议是:任何能连上生产库的人工操作账号,都应该有对应的只读版本。

比如DBA在排查慢查询时,需要看看线上数据,但完全不需要写权限。如果DBA日常连接用的是有写权限的账号,一次误操作就可能造成数据污染。所以可以这样做:

CREATE USER 'dba_ro'@'192.168.1.%' IDENTIFIED BY 'Ro@2024Pass'; GRANT SELECT, SHOW VIEW, PROCESS ON *.* TO 'dba_ro'@'192.168.1.%';

这里给PROCESS权限是允许查看所有会话,SHOW VIEW是允许查看视图定义,这两个都是排查问题必需的,但不是写权限。线上只读账号的权限范围通常还要再加上临时表权限CREATE TEMPORARY TABLES,因为有些报表查询需要建临时表,但很多同学没给,结果查询一跑就报错。

5.4 一个生产环境的权限白名单示例

分享一套我自己整理的账号规划,适合中小规模业务库:

账号类型命名规范权限范围适用场景
业务读写svc_应用名_rw指定库的 SELECT/INSERT/UPDATE/DELETE后端服务连接
业务只读svc_应用名_ro指定库的 SELECT报表、数据核对
DBA管理dba_admin全局 SELECT/PROCESS/SUPER/RELOAD 等DBA日常运维
DBA只读dba_ro全局 SELECT/SHOW VIEW/PROCESS排查慢查询、巡检
监控采集monitor_ro全局 SELECT 及 PROCESS监控系统抓取状态变量

这套规范的核心思想是:每个账号都有清晰的边界,命名能看出用途,权限能看出场景。等账号数量多了以后,这种规范带来的维护便利远远超过分类时花的那点时间。

6. 一次真实权限事故的排查全记录

6.1 事故表现:服务突然连不上MySQL

某天下午,值班同事收到告警:某内部系统的后端服务连续报错,日志里出现Access denied for user 'svc_shop'@'192.168.20.55' (using password: YES)。奇怪的是,这个账号前一天还能正常使用,中间没有做任何密码变更。

按一般思路,先怀疑密码问题,重置密码后重启服务,发现依然报错。又怀疑账号被锁,执行了ALTER USER ... ACCOUNT UNLOCK,还是无效。直到有人想起来了:前一天刚执行过一个角色清理任务,把某个不再使用的旧角色批量回收了。

6.2 排查链路:从报错信息到权限矩阵

我先查了mysql.user表:

SELECT User, Host, account_locked, password_expired FROM mysql.user WHERE User = 'svc_shop';

记录确实存在,没有锁定,密码也没有过期。再执行:

SHOW GRANTS FOR 'svc_shop'@'192.168.20.%';

输出显示这个账号的权限来自两个角色:r_shop_rw和r_shop_ro。而r_shop_ro正是昨天被回收的角色。问题来了:r_shop_ro被回收后,svc_shop的SELECT权限是否还在?按理说r_shop_rw可能包含SELECT,但我在梳理角色定义时发现,r_shop_rw只授权了INSERT, UPDATE, DELETE,把SELECT放在了r_shop_ro里。这是典型的拆分错误:同一个业务账号依赖两个角色,一个负责写、一个负责读,回收其中一个时没有同步检查依赖。

于是故障链路清晰了:昨天清理角色的同事,只看了角色名以为r_shop_ro没人用,实际上多个账号都在依赖它——定期从库里读取配置,读到一半权限悬空了。

6.3 根因复盘:角色拆分与回收的教训

问题修复本身很简单,重新把r_shop_ro角色授权回来即可。但复盘时总结了几条值得写下来的经验:

  1. 回收或删除任何角色之前,先查哪些用户拥有该角色:
SELECT * FROM mysql.role_edges WHERE TO_USER = 'svc_shop';

或者全量查一遍:

SELECT FROM_USER, TO_USER FROM mysql.role_edges;
  1. 权限拆分时,不要把“业务必须的基础权限”放在一个单一角色里。每个业务账号的基础读权限应该单独确认,不能因为“应该有另一个角色兜底”就觉得没问题。

  2. 权限回收和删除操作,要像“下线服务”一样走变更流程:先通知、再执行、后验证。验证的方式就是用一个该账号的典型查询跑一遍:

mysql -u svc_shop -p -h 127.0.0.1 -e "SELECT COUNT(*) FROM shop_db.t_config;"

一句话总结:**权限管理里的每一个REVOKE和DROP,都值得你多花一分钟确认它到底会影响谁。**很多线上问题不是语法写错,而是权限依赖关系没看清楚。能把这一步做踏实,日常用户管理的故障率能降一半以上。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询