1. 为什么要重视MySQL用户管理
上周有个新同事入职,需要给他开一个测试库账号。我顺手打开执行窗口,给了一条CREATE USER,再补了一条GRANT,前后不超过一分钟。旁边的实习生看着有点懵,问我:“为什么创建用户之后还要单独授权?直接给 root 不就行了?”
这个问题其实问到点子上了。很多人用了几年 MySQL,日常业务全是 SELECT 和 UPDATE,对用户管理这套东西一直停留在“会用 root 跑一切”的阶段。平时开发环境无所谓,一旦涉及生产库、财务数据或者客户敏感信息,root 一把梭就是一个定时炸弹。MySQL 用户管理,说到底是权限边界的问题:谁能连、能从哪连、能看哪些库表、能执行什么操作,这些都必须清晰可控。
这篇文章我打算从实际操作出发,把 MySQL 用户管理的核心讲透。内容包括用户与权限模型、创建/授权/回收的完整 SQL、密码策略与安全加固、8.0 的角色管理,以及我这些年踩过的坑和排查思路。适合正在学习 MySQL 的人,也适合刚接手数据库维护、想把用户权限体系理清楚的开发者和运维。内容不追求覆盖每个冷门参数,但凡是工作中高频遇到的场景,我都会尽量写细。
1.1 用户管理到底管的是什么
MySQL 里的“用户”和我们平时说的登录账号有点不一样。一个 MySQL 用户由两部分组成:用户名和主机名,写成'用户名'@'主机名'。主机名表示这个账号允许从哪台机器发起连接。'root'@'localhost'只允许本机连,'app'@'192.168.1.%'允许来自 192.168.1 网段的机器连接。我第一次知道这个机制的时候,觉得多此一举,后来才意识到,主机限制本身就是一道重要的安全屏障。你在云服务器上开一个'root'@'%'账号,全世界任何一台机器都能拿这个账号试密码,暴力破解的压力会直线上升。
权限体系也是分层的。MySQL 有全局权限、数据库级权限、表级权限、列级权限和例程权限。实际工作中用到最多的是数据库级和表级权限:给一个应用分配某个库的增删改查权限,或者给只读账号只开 SELECT。全局权限(比如 SUPER、PROCESS、FILE 这类)通常只给 DBA 自己用,普通业务账号不应该触碰。
很多人以为GRANT ALL PRIVILEGES ON *.*只是“给了所有权限”,其实它还隐含了跨库风险。举个例子,你给某个账号授了ALL ON *.*,意味着它不仅能读写自己的库,还能看到这台 MySQL 实例上所有数据库的元数据,甚至可以尝试访问其他业务库。所以我的习惯是:默认永远不用ON *.*授权业务账号,除非这个账号就是 DBA 专用管理号。
1.2 权限表与权限校验的基本流程
MySQL 的权限信息存储在系统库mysql中,核心表包括user、db、tables_priv、columns_priv和procs_priv。每次客户端发起连接时,MySQL 会先查mysql.user表,确认用户名和主机是否匹配、密码是否正确。连接建立之后,每次执行 SQL 还要做权限校验,校验顺序大致是:全局权限 → 数据库权限 → 表权限 → 列权限 → 例程权限。
在 MySQL 8.0 中,权限校验采用的是“授权表缓存 + 内存哈希”的方式,所以日常GRANT、REVOKE之后不需要手动FLUSH PRIVILEGES,权限会立即生效。这一点和 5.7 及以前版本有些区别。老版本如果你直接INSERT/UPDATE了mysql.user表,那必须执行FLUSH PRIVILEGES重新加载授权表;但通过官方CREATE USER、GRANT语句操作时,服务器会自动处理。我见过不少教程还保留FLUSH PRIVILEGES的习惯写法,这是旧版本遗留,新版本里属于画蛇添足。
权限校验流程可以用一句话概括:连接时查身份,执行时查权限。理解了这个模型,后面所有管理操作都顺理成章。
2. 用户与权限的完整实操
2.1 创建用户与修改密码
创建用户的标准语句:
CREATE USER 'app_user'@'%' IDENTIFIED BY 'StrongP@ssw0rd123';这里%是通配符,表示允许任意主机连接。生产环境我的建议是尽量把%收窄成具体网段,比如:
CREATE USER 'app_user'@'192.168.10.%' IDENTIFIED BY 'StrongP@ssw0rd123';这样即使密码泄露,攻击者也必须来自内网网段,大大降低了风险面。
修改密码在 8.0 里用ALTER USER:
ALTER USER 'app_user'@'192.168.10.%' IDENTIFIED BY 'NewP@ssw0rd456';删除用户:
DROP USER 'app_user'@'192.168.10.%';这里有一个细节值得一提:MySQL 5.7 及之前版本中,DROP USER默认不会自动回收该用户在其他库中创建的视图、存储过程等对象的属主关系,8.0 在这方面有改进。但无论如何,删除用户前最好先用SHOW GRANTS确认一遍,顺便排查一下有没有依赖这个账号的定时任务或应用配置,免得删完第二天业务直接断连。
创建用户之后,还需要授权才能真正干活。单独执行CREATE USER而不GRANT,这个账号登录之后什么也做不了,连SELECT都会被拒。很多新手第一次遇到ERROR 1044 (42000): Access denied for user ... to database 'xx',就是因为只建了用户没授权。
2.2 授权与回收
授权语句的核心格式:
GRANT SELECT, INSERT, UPDATE, DELETE ON biz_db.* TO 'app_user'@'192.168.10.%';这条语句的意思是:允许app_user从192.168.10.%网段连接,对biz_db库下的所有表执行 SELECT、INSERT、UPDATE、DELETE 操作。biz_db.*中的*表示库下所有表,如果只想授权某一张表,可以写成biz_db.orders。
回收权限使用REVOKE:
REVOKE DELETE ON biz_db.* FROM 'app_user'@'192.168.10.%';查看某个用户的权限:
SHOW GRANTS FOR 'app_user'@'192.168.10.%';查看所有用户列表:
SELECT user, host, plugin FROM mysql.user;这里补充一个常见误区:GRANT语句中可以同时完成建用户和授权两步操作。比如:
GRANT SELECT ON biz_db.* TO 'readonly_user'@'%' IDENTIFIED BY 'ReadOnly123';这条语句在 5.7 中会先创建用户再授权,一条命令搞定。但是 MySQL 8.0 已经不支持IDENTIFIED BY写在GRANT里了,必须先用CREATE USER建用户,再GRANT授权。很多从 5.7 迁移到 8.0 的 DBA,第一次用旧习惯写这条语句,直接报语法错误。这就是差异,需要特别留意。
2.3 最小权限原则的实际落地
最小权限原则听上去很抽象,落地其实就三条:能只读就不要写,能单表就不要整库,能库内就不要跨库。
我举一个实际场景。公司有一个报表系统,需要读取订单库的数据做统计。最开始开发图省事,直接在连接配置里填了一个 DBA 账号。后来我接手,发现这个账号有全局ALL PRIVILEGES,瞬间就觉得后背发凉——报表系统的代码一旦被注入,攻击者可以直接删库。后来我新建了一个账号:
CREATE USER 'report_ro'@'10.0.0.%' IDENTIFIED BY 'Report@123'; GRANT SELECT ON order_db.* TO 'report_ro'@'10.0.0.%';这样报表系统只能读订单库,写不了任何数据,也没法看其他库。整个过程十分钟不到,但把风险敞口缩小了一大截。
如果是给外部合作方开临时账号,还可以加上更细的粒度和有效期控制。MySQL 8.0 支持账号锁定和解锁:
ALTER USER 'temp_guest'@'%' ACCOUNT LOCK; ALTER USER 'temp_guest'@'%' ACCOUNT UNLOCK;配合临时账号用完后DROP USER或锁定,可以避免“临时账号变成永久后门”。
3. 密码策略与安全加固
3.1 设置密码强度校验策略
MySQL 密码强度校验是一个独立组件,在 5.7 中叫validate_password插件,8.0 中改成了validate_password组件。安装方式上有差异,这里说 8.0 的做法:
INSTALL COMPONENT 'file://component_validate_password';安装后,查看相关参数:
SHOW VARIABLES LIKE 'validate_password%';常见的几个参数:
| 参数名 | 默认值 | 含义 |
|---|---|---|
| validate_password.length | 8 | 密码最小长度 |
| validate_password.mixed_case_count | 1 | 大写和小写字母至少各出现次数 |
| validate_password.number_count | 1 | 数字至少出现次数 |
| validate_password.special_char_count | 1 | 特殊字符至少出现次数 |
| validate_password.policy | MEDIUM | 密码策略等级,LOW/MEDIUM/STRONG |
在实际生产中,我一般会把validate_password.length设为 12 以上,同时开启 MEDIUM 策略。像123456、admin888这类密码,在开启校验之后根本创建不了,CREATE USER会直接抛ERROR 1819。
很多人会问,那我自己的测试环境密码很弱,怎么办?可以安装组件后临时把validate_password.policy调成 LOW,创建完用户再调回来。但我的建议是,测试环境也尽量用稍微复杂一点的密码,不要养成弱密码的习惯。
3.2 认证插件与连接兼容性
MySQL 5.7 默认的认证插件是mysql_native_password,8.0 默认变成了caching_sha2_password。这个变化是很多连接报错的总根源。现象是:MySQL 服务端是 8.0,客户端驱动或者老版本工具用的还是mysql_native_password,连接时直接报Authentication plugin 'caching_sha2_password' cannot be loaded。
解决方法有两种。
第一种,修改用户认证插件为旧版:
ALTER USER 'app_user'@'%' IDENTIFIED WITH mysql_native_password BY 'NewP@ssw0rd';这也是最直接的兼容方案,适合驱动不能升级的存量系统。
第二种,升级客户端驱动或连接工具。如果你用的是 Navicat 老版本、PHP 5.x 的 mysql 扩展、Python 的MySQLdb(注意不是pymysql),大概率会遇到兼容性问题。优先升级驱动才是长久之计,把线上生产用户的认证方式降级成老插件,会降低安全性,只能算临时措施。
这里我特别想强调:创建用户时也可以显式指定认证插件。比如你确定客户端支持新版插件,可以这样做:
CREATE USER 'app_user'@'%' IDENTIFIED WITH caching_sha2_password BY 'StrongP@ssw0rd123';关于 SSL 连接的问题,MySQL 8.0 默认启用require_secure_transport的配置其实没有,但服务器默认支持 SSL。如果客户端连接时报类似SSL connection error的错,通常是服务端证书配置不对,或者客户端 SSL 参数设置有问题。8.0 实例可以通过--ssl-ca、--ssl-cert、--ssl-key配置证书。对于要求强制 SSL 的场景,可以在创建用户时加上REQUIRE SSL:
CREATE USER 'secure_user'@'%' IDENTIFIED BY 'StrongP@ssw0rd123' REQUIRE SSL;3.3 密码过期策略
安全领域有一个共识:密码不能长期不变。MySQL 提供了密码过期机制,可以在用户级别设定,也可以设定全局默认。
全局设置,比如让所有新密码 90 天后过期:
SET GLOBAL default_password_lifetime = 90;用户级别设置:
ALTER USER 'app_user'@'%' PASSWORD EXPIRE INTERVAL 90 DAY;强制指定立即过期:
ALTER USER 'app_user'@'%' PASSWORD EXPIRE;用户密码过期后,登录时 MySQL 会提示必须修改密码,否则无法执行其他 SQL。这个机制在离职交接、临时账号场景下特别有用。比如一个外包工程师,项目结束后我直接执行:
ALTER USER 'outsource_01'@'%' PASSWORD EXPIRE;然后再把账号锁定。两步操作之后,他的账号即使没删除,也基本等于废了。
4. MySQL 8.0 角色管理的实用价值
4.1 为什么需要角色
角色(Role)是 MySQL 8.0 引入的重要功能。它的本质是“权限的集合”。没有角色之前,如果你想给五个同事开只读账号,得挨个执行五遍同样的GRANT SELECT。有了角色之后,只需要把权限授予角色一次,再把角色授予五个人,后续调整权限只需要改角色本身。
我举一个真实案例。公司有十几个数据分析师,都需要对数据仓库的dw_sales库做只读查询。以前的做法是每个分析师建一个账号,逐个授GRANT SELECT ON dw_sales.*。后来其中一个分析师转岗,要求回收权限,我一个个地REVOKE,折腾了大半天。角色上线后,我只需要创建一个只读角色:
CREATE ROLE 'sales_ro'; GRANT SELECT ON dw_sales.* TO 'sales_ro'; GRANT SELECT ON dw_dim.* TO 'sales_ro';然后给每个分析师绑定:
GRANT 'sales_ro' TO 'analyst_01'@'%'; GRANT 'sales_ro' TO 'analyst_02'@'%';转岗的那位,直接:
REVOKE 'sales_ro' FROM 'analyst_03'@'%';一条语句搞定,不用再关心他具体有多少个权限点。这就是角色的价值:把复杂的权限集合抽象成了业务语义清晰的单位。
4.2 默认角色与激活设置
角色授予用户之后,还需要注意“默认激活”的问题。MySQL 8.0 中,用户被授予角色后,角色不会自动生效,必须调用SET ROLE手动激活,或者在创建/修改用户时指定DEFAULT ROLE。
手动激活:
SET ROLE 'sales_ro';一次性激活当前用户的所有角色:
SET ROLE ALL;设置某个用户的默认角色:
ALTER USER 'analyst_01'@'%' DEFAULT ROLE 'sales_ro';如果不设置默认角色,用户每次登录后都需要手动执行SET ROLE,否则会报SELECT command denied的权限错误,哪怕你明明已经授权了。这是我踩过的一个坑。
另外要注意,角色和用户本质上都是“账号主体”,在mysql.user表里能看到角色记录,它们的account_locked属性默认是Y。也可以给角色设置密码,但一般不需要,因为我不会允许任何人直接用角色身份连接数据库,角色只做权限载体。
5. 常见问题与排查技巧实录
5.1 Access denied 的几种典型原因
ERROR 1045 (28000): Access denied for user 'xxx'@'xxx'是用户管理中最常见的报错。排查思路基本上按下面几个方向来。
第一,用户是否存在。在 MySQL 5.7 中,如果你创建了一个用户,但密码写错,登录报的就是 1045,提示信息不会区分“用户不存在”和“密码错误”。先执行:
SELECT user, host FROM mysql.user WHERE user = 'app_user';确认用户确实存在。
第二,Host 匹配问题。比如你用'app_user'@'localhost'登录本机,但实际连接来源被解析成了127.0.0.1而不是localhost,MySQL 就会匹配不到用户。这种情况在 Linux 下经常发生,特别是/etc/hosts配置异常时。排查方法是:
SHOW VARIABLES LIKE 'skip_name_resolve';如果这个参数是ON,MySQL 不做反向 DNS 解析,客户端 IP 和主机名的匹配会受影响。你以为是'app_user'@'localhost'能连,实际上连接来源显示为127.0.0.1,需要创建'app_user'@'127.0.0.1'账号。
第三,密码错误。这是最常见的,直接ALTER USER重置密码即可。
5.2 忘记了 root 密码怎么办
这里提供一个通用方案。以 MySQL 8.0 为例,先停止 MySQL 服务,然后使用跳过授权表的方式启动:
mysqld --skip-grant-tables --skip-networking注意--skip-networking一定不要省略,否则跳过授权表期间,任何人都能免密连接,这是极其危险的。加了这个参数后,只有本机能连。
然后另开终端连接:
mysql -u root进入后刷新授权表,让--skip-grant-tables模式下也能正常执行账号管理语句:
FLUSH PRIVILEGES;然后重置 root 密码:
ALTER USER 'root'@'localhost' IDENTIFIED BY 'NewRootP@ss';改完后退出,正常重启 MySQL 服务即可。注意在 5.7 中这个流程略有不同,5.7 需要先UPDATE mysql.user SET authentication_string=...,因为ALTER USER在 skip-grant-tables 模式下可能不可用。所以我一般是先执行FLUSH PRIVILEGES,再执行ALTER USER,这样兼容性最好。
5.3 远程连接与主机限制问题
项目里经常有这种需求:本地开发、服务器部署、数据库在另一台机器上,需要在本地连远程 MySQL。如果报Host 'xxx.xxx.xxx.xxx' is not allowed to connect to this MySQL server,说明账号的主机限制把来源 IP 挡住了。
解决办法就是调整账号的 host 范围。比如原来只有'app_user'@'localhost',现在想允许内网所有机器连接:
CREATE USER 'app_user'@'192.168.1.%' IDENTIFIED BY 'Password123'; GRANT SELECT, INSERT, UPDATE, DELETE ON biz_db.* TO 'app_user'@'192.168.1.%';或者更激进一点改成%,注意这是不推荐的。改完再去测试连接,别忘了同时检查防火墙和安全组是否有 3306 端口的放行规则。很多时候报错信息看似是 MySQL 拒绝连接,其实是被防火墙挡在了前面,根本还没到 MySQL 这层。
5.4 MySQL 5.7 与 8.0 用户管理差异速查
很多项目仍在用 5.7,新旧版本交替期最容易混淆。我整理了一份高频差异表:
| 对比项 | MySQL 5.7 | MySQL 8.0 |
|---|---|---|
| 默认认证插件 | mysql_native_password | caching_sha2_password |
| GRANT 中能否同时创建用户 | 可以 | 不可以 |
| 角色管理 | 不支持 | 支持 |
| 密码过期全局变量 | default_password_lifetime | 同左 |
| FLUSH PRIVILEGES 必要性 | 直接 UPDATE 授权表后需要 | 官方语句操作时不需要 |
印象最深的一次是帮朋友迁移数据库,5.7 的备份导入到 8.0,原来程序用的数据库账号连接报认证插件错误。我给那个账号执行了ALTER USER ... IDENTIFIED WITH mysql_native_password BY ...才恢复正常。所以如果你在生产环境中同时有 5.7 和 8.0,一定要提前检查客户端驱动的兼容性,免得上线当天手忙脚乱。
6. 我平时会坚持的几条用户管理习惯
最后分享几条我这些年总结出的习惯,算不上高深,但确实避开了很多麻烦。
第一,所有账号创建都通过 SQL 语句操作,不直接改mysql.user表。原因是操作语句会写入 binlog,后续审计和回放都能查到;直接改表的话,一不小心漏了FLUSH PRIVILEGES,改完还不生效,排查起来浪费时间。
第二,默认不给任何业务账号开放全局权限。别说ALL PRIVILEGES ON *.*,连GRANT OPTION都尽量不要给业务账号。没有授权选项的账号,即使被攻破,也不能给自己加权限,算是一道保险。
第三,用户授权用“角色 + 默认角色”两层结构。数据库数量少的直接建角色,数据库多的建议脚本批量处理。角色命名遵循业务语义,比如sales_ro、bi_rw,一看就明白是干嘛用的。
第四,带时间性质的临时账号到期就删,不做“先留着”这种决定。安全意识都是在这种细节里积累出来的。
第五,定期核对账号清单。我一般每个月执行一次:
SELECT user, host, account_locked, password_expired FROM mysql.user;把不认识的账号清理掉,把密码过期状态异常的账号改掉。这个操作十分钟就能完成,但能避免很多潜在的权限失控问题。
MySQL 用户管理本质上没有太多高深的理论,核心就是把“谁用什么身份从哪里访问哪些数据”这件事管清楚。多花一点时间在权限规划上,远比出事了再补救要划算。