☰
MySQL用户管理实战:从GRANT到角色权限,避开常见坑
2026/10/17 5:36:38 网站建设 项目流程

很多开发者最初接触MySQL用户管理,都是从一句grant all privileges开始的。等哪一天权限出问题,或者误删了核心表,才回头补课:原来用户不是“建个账号给个 root”这么简单。我接触过不少系统,应用账号直接用 root 连库,权限大到能删库,密码还明文写在配置文件里,这种情况一旦出事故,连排查的余地都没有。

MySQL用户管理真正要管的是两件事:一是账号本身,二是它能碰到哪些库、哪些表。这篇文章把这套模型从前到后拆开讲,包括建账号的完整语法、grant/revoke的用法、MySQL 8.0 和 5.7 的版本差异、密码过期与账号锁定,以及实际运维中那些“书上没写但真实存在”的坑。无论你是刚入行的后端开发,还是已经接触数据库一段时间的 DBA 新人,都应该能把这篇当做一个可随时翻阅的实操手册。

1. 用户管理到底在管什么:先搞懂权限模型

1.1 用户不是“一个人”,而是一对凭证

很多人把 MySQL 里的用户想象成“给某个人开的账号”。实际上,MySQL 里一个用户是由用户名 + 主机名(host)共同标识的。也就是说,'app'@'localhost'和'app'@'%'是两个完全不同的账号,可以有不同的密码、不同的权限,甚至在授权表里就是两行独立记录。

这个设计的理由很直接:MySQL 需要区分“从本机连过来的 app”和“从任意远程主机连过来的 app”。你在授权时如果只关注用户名而忽略 host,短期看起来“能用”,但后续排查权限问题时会非常头疼。比如某个开发人员反馈“我用 app 账号连不上”,你第一反应是查权限,结果权限都在;实际上他用的'app'@'%',而你授权给的是'app'@'localhost'。两个账号,互不相干。

所以做用户管理之前,请先把“用户 = 用户名@主机”这个观念刻在脑子里。后续所有CREATE USER、GRANT、SHOW GRANTS都离不开 host 这一维度。

1.2 权限从全局到列:五种权限范围

MySQL 的权限不是“有或者没有”的二元逻辑,而是分层的。按范围从小到大概括如下:

权限层级作用范围典型授权语法类比
列权限某张表的某几列GRANT SELECT (name, email) ON db.tbl TO ...抽屉钥匙
表权限某张表GRANT SELECT ON db.tbl TO ...房间钥匙
数据库权限某个库下的所有对象GRANT SELECT ON db.* TO ...单元门卡
全局权限所有库及系统管理操作GRANT ALL ON *.* TO ...大门总卡
例程权限存储过程/函数GRANT EXECUTE ON PROCEDURE db.proc TO ...特定设备操作权

这套层级关系非常重要,因为 MySQL 在判断“到底给不给这个用户执行某条 SQL 的权限”时,是自下而上叠加判断的。意思是:只要任何一个层级里有权限,这个操作就被放行。比如某个用户对整个库有ALL PRIVILEGES,你单独在某张表上REVOKE DELETE,是拦不住他删除这张表数据的,因为库级权限已经包含 DELETE 了。这类“权限叠加”逻辑,是新手最容易踩的坑,我在后文授权部分会专门展开。

1.3 版本差异:8.0 与 5.7 在用户管理上的关键变化

如果你还在用 5.7,或者正在从 5.7 迁移到 8.0,有几个用户管理层面的版本差异必须知道。

第一,8.0 不再支持GRANT后自动创建用户。5.7 里你可以直接执行GRANT ALL ON *.* TO 'xx'@'%' IDENTIFIED BY '密码',一条语句既建用户又授权。8.0 不行,8.0 要求先用CREATE USER建用户,再用GRANT授权。如果直接GRANT,会报ERROR 1410 (42000): You are not allowed to create a user with GRANT。

第二,默认认证插件不同。5.7 默认是mysql_native_password,8.0(从 8.0.4 开始)默认是caching_sha2_password。后者安全性更高,但一些老语言驱动、老版本客户端可能不支持,连接时会报Authentication plugin 'caching_sha2_password' cannot be loaded。如果遇到兼容性问题,可以在创建用户时显式指定IDENTIFIED WITH mysql_native_password BY '...',但生产环境我更建议优先升级客户端驱动,而不是降级认证插件。

第三,8.0 正式引入角色(Role)机制。5.7 里想给一批用户相同权限,只能逐个GRANT;8.0 里可以先建角色,把权限包赋给角色,再把角色赋给用户,管理效率高很多。这部分我在第 3 章专门讲。

第四,8.0.19 之后支持登录失败跟踪。可以设置连续登录失败多少次后自动锁定账号,这是密码暴力破解场景下的重要防线,5.7 需要借助外部工具才实现得了。

下表可以大概对照:

能力点MySQL 5.7MySQL 8.0
GRANT 自动建用户支持不支持
默认认证插件mysql_native_passwordcaching_sha2_password
角色 Role不支持支持
登录失败自动锁定无8.0.19+ 支持
密码轮换策略手动实现支持密码历史、复用策略

2. 建户、改密、删号的完整实操

2.1CREATE USER的完整语法和常用参数

建用户的标准语法长这样:

CREATE USER 'zhang'@'localhost' IDENTIFIED BY 'StrongPass@123';

这条语句创建了一个仅允许从本机登录、密码为StrongPass@123的账号。IDENTIFIED BY后面的密码会以不可逆哈希形式存储,不会明文落库。

进阶用法是在一条语句里同时设置多种属性:

CREATE USER 'zhang'@'localhost' IDENTIFIED BY 'StrongPass@123' PASSWORD EXPIRE INTERVAL 90 DAY ACCOUNT UNLOCK FAILED_LOGIN_ATTEMPTS 4 PASSWORD_LOCK_TIME 1;

几个关键属性的含义:

  • PASSWORD EXPIRE INTERVAL 90 DAY:密码 90 天后过期,过期后必须修改密码才能继续使用。
  • ACCOUNT UNLOCK:明确账号初始为解锁状态。如果某些场景要求“建好后先人工激活”,可以写成ACCOUNT LOCK。
  • FAILED_LOGIN_ATTEMPTS 4 PASSWORD_LOCK_TIME 1:连续失败 4 次后锁定账号,1 天后自动解锁。这是 8.0.19 加入的防爆破能力,实测非常有用。

如果你需要手动指定认证插件,这样写:

CREATE USER 'zhang'@'localhost' IDENTIFIED WITH caching_sha2_password BY 'StrongPass@123';

2.2 改密码与修改账号属性

日常用得最多的是ALTER USER。改密码:

ALTER USER 'zhang'@'localhost' IDENTIFIED BY 'NewPass@456';

如果当前用户想改自己的密码,MySQL 8.0 还支持ALTER USER USER() IDENTIFIED BY 'NewPass@456',USER()函数会返回当前会话的用户名和 host,不需要自己手动拼。

需要兼容老客户端时,把认证插件一并改掉:

ALTER USER 'zhang'@'localhost' IDENTIFIED WITH mysql_native_password BY 'NewPass@456';

强制账号下次登录时修改密码:

ALTER USER 'zhang'@'localhost' PASSWORD EXPIRE;

锁定和解锁账号:

ALTER USER 'zhang'@'localhost' ACCOUNT LOCK; ALTER USER 'zhang'@'localhost' ACCOUNT UNLOCK;

这里有一个非常容易误解的点:修改密码不会影响当前已经建立的连接。也就是说,你ALTER USER改了密码之后,正在跑着的长连接依然能继续执行 SQL,直到它断开重新连接才需要用新密码。这条在实际运维中很重要。有一次我在线上改完应用账号密码,开了个新连接测试没问题,但老连接还在跑,业务方以为改密失败,排查半天。

2.3 删除用户与授权残留问题

删除用户用DROP USER:

DROP USER 'zhang'@'localhost';

也可以一次删多个:

DROP USER 'zhang'@'localhost', 'li'@'192.168.1.%';

有些文章会教你直接DELETE FROM mysql.user WHERE user = 'zhang',这个操作我能理解是为了“快速清数据”,但极不推荐。原因在于,MySQL 的用户权限分散在mysql.user、mysql.db、mysql.tables_priv、mysql.columns_priv多张表里。直接删mysql.user里的行,可能留下授权残留,影响后续同名账号的权限判断。DROP USER才是官方支持的完整清理方式,一条命令把用户和所有层级授权一起清掉。

2.4localhost、%与网段的真实差异

授权和建号时最容易被忽略的,就是 host 该填什么。常见的 host 值有这几类:

  • 'localhost':只能通过本机 socket 连接,不通过网络。适合管理账号、备份账号。
  • '127.0.0.1':本机 TCP 回环连接。有些环境里用localhost走了 socket,如果想强制走 TCP,就写这个。
  • '192.168.1.%':指定网段,例如192.168.1.0/24内的机器都能连。
  • '%':任何主机都能连。最省事,但风险也最大。

生产环境中,应用账号尽量写业务所在的网段,例如'app'@'10.10.20.%',管理账号尽量用'root'@'localhost'。%不是不能用,而是要谨慎用。尤其是密码一旦泄漏,%意味着任何一台机器都可能尝试连接,即使有防火墙,也等于把攻击面拉大了。

再补充一个关联知识点:MySQL 默认会做反向 DNS 解析,如果 DNS 环境慢,远程连接会明显卡顿。这时候可以考虑全局参数skip_name_resolve,但要注意它开启后,授权表里的 host 必须写 IP 或 localhost,不能再写域名。这个参数改起来要评估业务,不建议直接在生产上“拍脑袋”开。

3. 授权与回收:GRANT、REVOKE 和角色

3.1 授权语句的完整用法

GRANT的基本结构是“给谁、在哪一层、什么权限”。几个典型例子:

-- 给 app 账号授予 shop_db 库的增删改查权限 GRANT SELECT, INSERT, UPDATE, DELETE ON shop_db.* TO 'app'@'192.168.1.%'; -- 给报表账号只开放某张表的特定列 GRANT SELECT (name, email) ON shop_db.users TO 'report'@'localhost'; -- 给管理账号全局所有权限,并允许它再转授 GRANT ALL PRIVILEGES ON *.* TO 'admin'@'localhost' WITH GRANT OPTION;

关于权限字面量,日常常用的有SELECT、INSERT、UPDATE、DELETE、CREATE、DROP、ALTER、INDEX、REFERENCES、TRIGGER、PROCESS、SUPER、REPLICATION SLAVE、REPLICATION CLIENT等。不要一上来就给ALL PRIVILEGES,按需授予才是正确姿势。

WITH GRANT OPTION这个选项需要特别谨慎。它表示该用户可以把它拥有的权限再授予给别人。比如给admin加了WITH GRANT OPTION,他就能再GRANT给别人。如果给一个应用账号加了这条,等于给了它横向扩散权限的能力,万一账号被盗,攻击者可以直接给自己开后门。默认不给,这是基本原则。

3.2REVOKE回收与权限叠加的坑

回收权限的语法和授权对称:

REVOKE DELETE ON shop_db.* FROM 'app'@'192.168.1.%'; REVOKE ALL PRIVILEGES, GRANT OPTION FROM 'user'@'host';

但这里有个认知陷阱:你以为REVOKE DELETE之后,这个用户就不能删数据了,实际却不一定是这样。因为 MySQL 的权限判断是“并集”逻辑,只要全局层、库层、表层任意一层有 DELETE,就能执行删除。

举个例子:

GRANT ALL PRIVILEGES ON *.* TO 'app'@'192.168.1.%'; REVOKE DELETE ON shop_db.* FROM 'app'@'192.168.1.%';

这条REVOKE并不会让app账号失去在shop_db里执行 DELETE 的能力,因为全局层ALL PRIVILEGES还包含 DELETE。正确做法是先从全局层移除 DELETE,或者干脆一开始就不要给全局ALL PRIVILEGES。

所以,排权限问题时,遇到“为什么我 revoke 了还是能删”,不要只在当前层找问题,要按“全局 → 库 → 表 → 列”的顺序逐层排查。

3.3 角色机制:把权限包打包分发

MySQL 8.0 的角色(Role)本质上就是一个“权限包”。你可以把一堆权限授予角色,然后把角色授予一个或多个用户。团队里新同事入职时,不需要逐条GRANT,把角色挂上去就行。

-- 1. 创建只读角色 CREATE ROLE 'app_read'; -- 2. 给角色授予具体权限 GRANT SELECT ON shop_db.* TO 'app_read'; -- 3. 把角色授予某个用户 GRANT 'app_read' TO 'app'@'192.168.1.%'; -- 4. 设置默认角色,让用户登录后自动激活 SET DEFAULT ROLE 'app_read' TO 'app'@'192.168.1.%';

注意,把角色授予用户后,用户登录时不一定立刻拥有角色里的权限,要看激活状态。SET DEFAULT ROLE是让新连接默认激活;已经存在的连接不受影响。也可以手动激活:

SET ROLE 'app_read';

角色特别适合读写分离场景。比如“只读账号组”和“读写账号组”分别建两个角色,运维只需要维护角色权限,不需要挨个用户改。5.7 没有角色,迁移到 8.0 之后这算是管理体验提升最大的一块。

3.4 查看权限的不同姿势

排查权限问题时,最直接的是SHOW GRANTS:

SHOW GRANTS FOR 'app'@'192.168.1.%';

输出结果会把该用户从全局、库、表、列各层拿到的授权都列出来,但格式是授权语句,不是一张干净的表。如果想看得更细,可以查授权表:

  • mysql.user:全局权限。
  • mysql.db:数据库级权限。
  • mysql.tables_priv:表级权限。
  • mysql.columns_priv:列级权限。

还有一个实用操作:想一次性导出所有用户的授权语句,方便备份和迁移环境,可以直接拼查询:

SELECT CONCAT('SHOW GRANTS FOR ''', user, '''@''', host, ''';') FROM mysql.user WHERE user NOT IN ('mysql.infoschema', 'mysql.session', 'mysql.sys');

把这些 SQL 执行一遍,把所有输出收集存储,就相当于保存了一份用户授权快照。业界有专门的工具pt-show-grants干这个事,但在没有工具的环境里,这条手写 SQL 也能用。

4. 密码安全、过期锁定与误操作恢复

4.1 密码复杂度校验:validate_password组件

很多数据库事故跟“密码太弱”没有直接关系,但“密码太弱导致被爆破”是真实的入口之一。MySQL 提供了密码校验组件,可以强制密码长度、大小写、数字、特殊字符的组合。

8.0 里安装方式:

INSTALL COMPONENT 'file://component_validate_password';

5.7 里是插件:

INSTALL PLUGIN validate_password SONAME 'validate_password.so';

安装后查看相关参数:

SHOW VARIABLES LIKE 'validate_password%';

常见参数含义:

  • validate_password.length:密码最小长度。
  • validate_password.policy:策略等级,LOW / MEDIUM / STRONG。MEDIUM 需要包含数字、大小写、特殊字符。
  • validate_password.number_count:至少几位数字。
  • validate_password.mixed_case_count:至少几位大小写混合。

我建议在测试环境就把策略开到 MEDIUM 以上,不然上线后再强行改策略,业务侧一定会有抵触。策略一旦开启,创建用户或改密码时如果密码过弱,会直接报错,比如ERROR 1819 (HY000): Your password does not satisfy the current policy requirements。

4.2 密码过期、账号锁定与登录失败

密码过期是安全审计里经常被要求的项目。可以在全局设置默认密码生命周期:

SET PERSIST default_password_lifetime = 180;

SET PERSIST会同时修改运行值和配置文件,重启也不丢。单账号单独设置:

ALTER USER 'zhang'@'localhost' PASSWORD EXPIRE INTERVAL 90 DAY;

账号锁定的实际使用场景是:某个账号暂时停用,但不希望删掉,以免影响后续恢复。这时候用ACCOUNT LOCK比DROP USER温和:

ALTER USER 'zhang'@'localhost' ACCOUNT LOCK;

登录失败锁定的配置,8.0.19 之后可以写在CREATE USER里,也可以事后用ALTER USER补:

ALTER USER 'app'@'192.168.1.%' FAILED_LOGIN_ATTEMPTS 4 PASSWORD_LOCK_TIME 2;

意思是连续失败 4 次后锁定,2 天后自动解锁。如果希望管理员手工解锁,可以把PASSWORD_LOCK_TIME写成UNBOUNDED。

这里需要提醒一件事:MySQL 的密码哈希是不可逆的。如果某个用户忘记密码,DBA 能做的最多是重置一个新密码,而不是“找回”旧密码。所以,任何基于“我记得密码哈希可以反推”的念头都要放弃。

4.3 权限不生效?先排查连接的建立时间

运维中经常遇到这类反馈:“我给他开了权限,但他还是报没有权限。” “我 revoke 了他还是能操作。” 这背后的常见原因有三个。

第一,当前连接已经建立,权限快照没有刷新。很多情况下,权限变更对已存在的连接不会立刻生效,甚至某些全局权限必须重连才见效。排查第一步永远是让业务方断开重连试试。

第二,修改授权表后没有生效。如果哪一天你手工UPDATE mysql.user改了权限,那么需要FLUSH PRIVILEGES;来强制系统重新加载授权表。但请记住,正常使用CREATE USER、GRANT、REVOKE这些语句时,系统会自动重新加载授权表,不需要额外FLUSH PRIVILEGES。这也是个高频误解:很多人习惯 GRANT 完之后再 flush 一把,实际多此一举。

第三,host 匹配问题。前文已经强调过,'app'@'localhost'和'app'@'%'是两个账号。检查SHOW GRANTS时要连 host 一起看,不能只看用户名。

4.4 误删用户后如何找回来

先说结论:如果mysql.user表没有备份,用户密码哈希是找不回来的,能做的只是“重新建一个同名同权限的账号”。所以平时养成导出授权语句的习惯非常重要。

如果你确实误删了用户,可以按这个顺序处理:

  1. 检查是否有当前会话还连着。如果有,让它维持连接,同时用SHOW GRANTS把这个会话的当前权限打印出来,至少能拿到一部分授权信息。
  2. 检查 binlog 里是否有历史CREATE USER/GRANT语句。开启 binlog 的实例,可以用mysqlbinlog解析日志,找到建号和授权语句,复制出来重新执行。
  3. 查mysql.db、mysql.tables_priv等表里的残留授权记录。因为DROP USER会清理关联授权,但如果用户是手工从mysql.user里删掉的,其它授权表可能还有记录,这些记录能作为重建授权的依据。
  4. 如果没有备份也没有日志,只能靠业务侧提供的权限清单重建,然后让开发验证。

从成本角度看,定期把授权语句导出保存成脚本,是最便宜的保险。很多团队对表结构、数据做了备份,却漏掉了 MySQL 库里的用户授权数据,等到误删或者迁移环境时才发现很被动。

5. 生产环境建议与日常管理习惯

5.1 最小权限原则如何真正落地

最小权限原则不是一句口号,落地时可以参考这几条:

  • 应用账号按系统拆分,一个系统一个账号,不要一个账号通吃多个库。
  • 开发环境可以给ALL PRIVILEGES,但测试、预发、生产必须收敛。
  • 只授予实际会用到的语句权限,比如只做增删改查就只给SELECT, INSERT, UPDATE, DELETE,不要顺手给DROP、ALTER、CREATE。
  • 建号时指定可登录的网段,不写裸的%。
  • 需要远程管理时,优先走堡垒机、跳板机,而不是把管理账号直接暴露到公网。

5.2 应用账号密码泄漏后的应急顺序

如果确认应用账号密码泄露,别慌,按顺序处理:

  1. 立即ALTER USER修改密码,让旧密码失效。
  2. 检查账号 host,如果原来设置的是%,业务允许的情况下收紧到具体网段。
  3. 在数据库层面开启针对该账号的日志审计,观察是否有异常查询。
  4. 涉及资金、订单等核心数据的系统,建议直接销毁会话:KILL掉该账号的全部连接,等确认安全后再让业务重连。

修改密码时要注意业务侧连接串的更新节奏。如果业务用的是连接池,旧连接不会立刻销毁,所以先让业务更新配置,再择机清掉旧连接,否则会出现“密码改了,但连接池还在用旧连接”的中间状态。

5.3 我建议养成的三个习惯

第一个习惯:所有授权语句进版本控制。建号、授权、角色定义的 SQL 单独建一个文件,跟着代码仓库走。环境重建时,直接执行脚本,比人肉敲命令靠谱得多。

第二个习惯:用查询定期找出长时间未登录的账号。mysql.user表在 8.0 里没有“最后登录时间”字段,但可以通过performance_schema的账号事件或者日志辅助判断。没有现成数据的,也可以结合业务侧反馈,每季度整理一次账号清单,挨个确认还要不要保留。这个动作很笨,但能有效避免“僵尸账号”越积越多。

第三个习惯:碰到权限问题,先看SHOW GRANTS,再查授权表,最后才动权限。不要一上来就GRANT ALL去“试一下”,试完又忘了,最后留下一个超高权限账号。我见过太多生产事故,都是“先给个大权限应急,之后忘了收回”导致的。

最后说一个我自己的体会。MySQL 用户管理本身其实不难,语法就那么几条,真正难的是“克制”。能不能忍住不建 root 级应用账号,能不能忍住不把所有库塞进一个用户,能不能在业务喊着“连不上”的时候冷静查一下 host 是不是写错了。把这些小选择做对,数据库这扇门就稳了一大半。

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

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

立即咨询