接手过一套"历史遗留"MySQL环境的人,大概都懂那种感觉:打开数据库一看,业务连接用的是root,线上库的账号权限五花八门,离职同事的账号还稳稳挂在mysql.user里,密码是多少没人记得,也没人敢动。MySQL用户管理这个事,表面上就是建号、授权、改密、删除几个动作,但真正到了生产环境,牵扯出来的问题一个比一个麻烦。这篇文章我把这几年在用户管理上踩过的坑、总结出的套路,以及背后的权限模型逻辑一次讲清楚,适合刚入门MySQL的开发者,也适合正在被账号体系折腾的运维和DBA。
1. 用户权限体系:一张user表背后的完整权限模型
很多人以为MySQL的权限管理就是mysql.user表里的一条记录,其实这只是入口。理解完整的权限模型,才是后面所有操作的根基。
1.1 权限不是平铺的,而是逐层收紧的五级模型
MySQL的授权数据分散在多张系统表中,按粒度从粗到细依次是:
- mysql.user:全局权限,作用于所有数据库
- mysql.db:数据库级权限,作用于指定库
- mysql.tables_priv:表级权限,作用于指定表
- mysql.columns_priv:列级权限,作用于指定表的指定列
- mysql.routines_priv:存储过程/函数级权限
一个连接进来时,MySQL会从全局权限开始查,再看库级、表级、列级,逐层往下匹配。权限的生效方式是"叠加"而不是"覆盖":只要任何一层给了权限,这个权限就生效。比如你在mysql.db里给了某个库的SELECT,在mysql.tables_priv里没给某张表的任何权限,这张表依然可以查——因为库级权限已经覆盖到了。
但有个例外要注意:管理类权限(SUPER、RELOAD、PROCESS、SHUTDOWN等)只存在于全局层,不能在db表或者tables_priv里授权。所以如果你看到某个用户需要PROCESS权限(比如监控数据库线程状态),那只能在全局层给,没有折中方案。这也是为什么监控类账号往往需要SELECT ON *.*或者PROCESS ON *.*这种看起来"挺宽"的授权。
1.2 host字段的匹配规则:localhost、%与网段到底怎么选
MySQL的账号是由user + host两部分组成的,同一用户名在不同host下可以拥有完全不同的权限。比如:
CREATE USER 'app'@'localhost' IDENTIFIED BY 'pass_strong_1'; CREATE USER 'app'@'192.168.1.%' IDENTIFIED BY 'pass_strong_2';这是两个完全独立的账号,互不影响。host的取值从小到大依次是:localhost、具体IP(如192.168.1.10)、网段(如192.168.1.%)、任意主机(%)。
匹配优先级不是"范围越小越优先"那么简单,MySQL会先精确匹配,没精确匹配时按host的"具体程度"排序。实际排障时,你只需要记住一个场景:如果同时存在'app'@'localhost'和'app'@'%',本机通过socket登录会命中'app'@'localhost',远程连接会命中'app'@'%'。你改了远程账号的密码,本机登录不受影响,反之一半的排障时间都容易卡在这里。
还有个大坑:如果只创建了'app'@'%',没有任何localhost账号,那么在本机上执行mysql -uapp -p连接反而可能失败——因为默认的socket连接匹配的是localhost或空host,匹配不到就会去找匿名用户。有些MySQL安装包自带匿名用户(User字段为空字符串),一旦匹配到匿名用户,登录后的身份就不是你了,权限也被限制得莫名其妙。建议一安装完就清掉匿名用户:
DELETE FROM mysql.user WHERE User=''; FLUSH PRIVILEGES;1.3 认证插件演进:5.7老账号连接8.0报2059的根因
MySQL 8.0把默认认证插件从mysql_native_password换成了caching_sha2_password,这是很多人升级后遇到的第一个坎。
连接时报错:
ERROR 2059 (HY000): Authentication plugin 'caching_sha2_password' cannot be loaded原因是老客户端(比如旧版Navicat、老版本PHP的mysqlnd扩展、某些旧语言驱动)只实现了mysql_native_password的握手流程,没有caching_sha2_password的实现,服务端发起该插件认证时客户端直接傻眼。
解决办法有三条路:
- 升级你的客户端驱动,这是治本的办法;
- 单个老账号降级认证插件:
ALTER USER 'app'@'%' IDENTIFIED WITH mysql_native_password BY '新密码';- 全局改回老插件(不推荐,但迁移期确实有人这么干):
# my.cnf [mysqld] default_authentication_plugin=mysql_native_password修改后需要重启MySQL。新创建的账号会用回老插件,但已经创建的账号不受影响。
我自己在升级到8.0时是这么处理的:先把所有存量账号批量改成mysql_native_password,保证业务方有充足时间升级驱动,随后在客户端版本全部达标后,再用一个月时间逐个把账号改成caching_sha2_password。整个切换过程隔了差不多两个发布周期,稳得很。生产环境不要追求"一步到位"的切换,兼容性过渡比安全理想主义更重要。
2. 建号授权的标准动作:从CREATE USER到最小权限落地
这一节说流程,但不说死流程,重点是让你明白每一步为什么这么做。
2.1 8.0与5.7在创建用户上的语法分水岭
MySQL 5.7及以前,一句GRANT可以同时完成建号和授权:
-- 5.7写法 GRANT SELECT ON mydb.* TO 'app'@'%' IDENTIFIED BY 'password123';这句SQL在8.0里直接报语法错误。8.0把"创建账号"和"授予权限"彻底分开了,必须分两步:
CREATE USER 'app'@'%' IDENTIFIED BY 'password123'; GRANT SELECT ON mydb.* TO 'app'@'%';我第一次从5.7升到8.0时,一堆老脚本全挂在这上面。升级前如果有一批自动化建号脚本,建议先检查有没有依赖GRANT自动建号的逻辑,否则会看到满屏ERROR 1064 (42000)。
创建用户时还有个小细节值得注意:密码里的特殊字符。单引号、双引号、反斜杠在密码串里必须正确处理。一个比较稳的做法是尽量避开这些字符,只用大小写字母、数字和@#%^*+=_这类相对安全的符号。如果非要存带单引号的密码,SQL里要写成:
CREATE USER 'app'@'%' IDENTIFIED BY 'it'\''s_pass';这种写法在脚本里特别容易出问题,调试起来也难受,能避免就避免。
2.2 不同角色的授权组合:只读、读写、DDL、备份
最小权限原则不是一句口号,落实到具体账号上,要按用途拆分。我常用的角色授权模板如下:
| 账号用途 | 授权命令 | 说明 |
|---|---|---|
| 只读分析账号 | GRANT SELECT ON report_db.* TO 'report'@'192.168.10.%' | 只能查询,不能写入 |
| 应用运行时账号 | GRANT SELECT, INSERT, UPDATE, DELETE ON app_db.* TO 'app'@'%' | 应用正常读写,不给DDL权限(8.0中默认自动创建临时表权限不定,必要时加CREATE TEMPORARY TABLES) |
| 变更发布账号 | GRANT SELECT, INSERT, UPDATE, DELETE, ALTER, CREATE, INDEX, DROP, REFERENCES ON app_db.* TO 'deploy'@'%' | 用于执行数据库变更脚本 |
| 备份账号 | GRANT SELECT, SHOW VIEW, RELOAD, LOCK TABLES, REPLICATION CLIENT ON *.* TO 'backup'@'%' | 逻辑备份所需,物理备份还可能需要BACKUP_ADMIN(8.0) |
| 监控账号 | GRANT PROCESS, REPLICATION CLIENT ON *.* TO 'monitor'@'%' | 查看连接状态、主从状态 |
| DBA管理账号 | GRANT ALL PRIVILEGES ON *.* TO 'dba'@'%' WITH GRANT OPTION | 仅供DBA使用,不共享给业务方 |
注意几点:
- 业务账号不要给
DDL权限。应用上线后唯一能改表结构的入口应该走变更发布账号,这样DBA可以审计。 - 备份账号需要
RELOAD是因为mysqldump默认加锁需要它,缺了会报Access denied; you need the RELOAD privilege。 - 只读账号如果涉及导出数据到本地(比如SELECT INTO OUTFILE),还要额外授权
FILE,但生产环境尽量不要开。
2.3 FLUSH PRIVILEGES到底什么时候要用
这是网上吵得最多的问题之一,其实原理很简单。
MySQL启动时把权限表读入内存,之后所有权限判断直接看内存。CREATE USER、GRANT、ALTER USER这类账号管理语句在操作时已经同步修改了内存中的权限缓存,所以不需要再执行FLUSH PRIVILEGES。
但如果你直接操作了系统表,比如:
UPDATE mysql.user SET Host='192.168.1.%' WHERE User='app'; DELETE FROM mysql.user WHERE User='old_user' AND Host='localhost';这些语句只改了磁盘上的数据,内存里的权限缓存没有变,必须执行FLUSH PRIVILEGES才能让它们生效。
所以结论很简单:用官方账号管理语句,不用flush;手改grant表,必须flush。生产环境里我从来不直接UPDATE mysql.user,宁可多敲几句ALTER USER,省得哪次忘了flush还把权限弄得神不知鬼不觉。
2.4 用SHOW GRANTS验证账号授权是否精准
授权完成后不要急着交付,先验证:
SHOW GRANTS FOR 'app'@'%';输出会列出账号下的所有全局、库级、表级权限。重点检查:有没有权限明显超出用途(比如只读账号带了UPDATE)、有没有多账号共享密码、GRANT OPTION有没有漏关。
更严格的验证是实际用这个账号登录,执行一条有权限的SQL和一条无权限的SQL,确认反馈符合预期。这一步能拦截大多数"授权表看起来对,实际连不上"的问题。
注意:MySQL自带
mysql.sys、mysql.session等系统保留账号,千万别去删除或者修改它们的host,否则可能导致系统组件异常。动了这些账号的密码,有时候比丢root还麻烦。
3. 用户生命周期管理:改密、锁定、回收与定期审计
账号不是建完就完事了,从改密到封禁再到定期复查,每一步都有讲究。
3.1 改密码的几个姿势和它们之间的差异
改密码的方法很多,我用下来最推荐的是ALTER USER:
ALTER USER 'app'@'%' IDENTIFIED BY 'New_Pass_456!';这条在5.7和8.0都可用,改完立刻生效,不需要flush。注意8.0的语法不要把PASSWORD()函数包在里面,8.0已经移除了这个函数,写了会报错。
等价的老式写法是:
SET PASSWORD FOR 'app'@'%' = 'New_Pass_456!';命令行下还有个mysqladmin的方式,适合脚本里批量改:
mysqladmin -u root -p'旧密码' password '新密码'但这种方式在shell的历史记录里会留下明文密码,生产环境谨慎使用。更安全的方式是交互式登录后用SQL改。
还有一个在5.7时代很常见但8.0已经废掉的写法:SET PASSWORD = PASSWORD('xxx')。如果你在网上搜到这种老语法,到8.0环境直接不能用了,别奇怪。
经验之谈:给业务方改密码前,先看有没有当前活跃连接。可以用
SHOW PROCESSLIST查一下该账号的会话,如果有正在跑的长事务,直接改密码虽然不会影响已经建立的连接,但会导致业务方的连接池里的旧连接在下次校验时全部失败(取决于连接池配置)。最好在低峰期操作,或者配合业务方一起切换。
3.2 账号锁定、解锁、删除与重命名
部分临时关闭某个账号,不需要删,锁起来就行:
ALTER USER 'temp_user'@'%' ACCOUNT LOCK;恢复时:
ALTER USER 'temp_user'@'%' ACCOUNT UNLOCK;删除账号则用:
DROP USER 'temp_user'@'%';删除前建议先确认没有活跃连接:
SELECT id, user, host, command, time, state FROM information_schema.processlist WHERE user = 'temp_user';如果确认不再需要账号,直接DROP USER。注意DROP USER会同时清理该账号在mysql.db、mysql.tables_priv里的授权记录,手改表清理反倒可能漏。
账号重命名这块也有坑。RENAME USER old TO new只能改用户名,host部分保持不变。想同时改host必须写全两个部分:
RENAME USER 'old'@'192.168.1.%' TO 'new'@'10.0.0.%';凡是涉及host变更的,干脆删除重建比RENAME更清晰,尤其是权限结构复杂的账号。
3.3 密码策略组件与过期强制轮换
8.0的密码校验组件叫validate_password,它是一个可加载组件。安装方式:
INSTALL COMPONENT 'file://component_validate_password';安装后,新创建用户的密码会经过强度校验,太弱的直接拒绝。可以通过参数调整策略:
SET GLOBAL validate_password.policy = MEDIUM; SET GLOBAL validate_password.length = 12; SET GLOBAL validate_password.mixed_case_count = 1; SET GLOBAL validate_password.number_count = 1; SET GLOBAL validate_password.special_char_count = 1;参数名字在5.7里是validate_password_policy这种下划线风格,在8.0里改成了点分风格,老参数在8.0中已废弃。
密码过期机制也很实用。针对单个用户:
ALTER USER 'app'@'%' PASSWORD EXPIRE INTERVAL 90 DAY;强制立即过期:
ALTER USER 'app'@'%' PASSWORD EXPIRE;全局默认:
[mysqld] default_password_lifetime=90密码过期后,用户下次登录会被要求先改密码才能执行其他语句,这对业务账号来说容易造成断连,生产上要跟业务方提前约好。
3.4 一份可以直接抄的账号审计SQL模板
定期审计账号是DBA的好习惯。我最常用的一组SQL:
-- 列出所有非系统账号及其状态 SELECT user, host, account_locked, password_expired, plugin, IF(authentication_string = '', 'EMPTY_PASS', 'HAS_PASS') AS pwd_status FROM mysql.user WHERE user NOT IN ('mysql.sys', 'mysql.session', 'debian-sys-maint'); -- 查看某个账号的全局权限 SELECT * FROM mysql.user WHERE user = 'app' AND host = '%'\G; -- 查看某个账号的库级权限 SELECT * FROM mysql.db WHERE user = 'app'\G; -- 查看某个账号的表级权限 SELECT * FROM mysql.tables_priv WHERE user = 'app'\G;审计时我重点关注四类问题:
- 有没有
plugin为空或authentication_string为空的账号(空密码或者异常导入导致的); - 有没有权限明显大于岗位需求的管理类账号(比如开发人员的账号带
SUPER); - 有没有长期不用的老账号(结合processlist和登录日志判断);
- 有没有host是
%却挂着高权限的账号,这类风险极大。
建议把审计SQL存成脚本,每月跑一次,输出到表格里供复查。审计这件事,跑一次不难,难的是每次跑完都有人跟进结果的整改闭环。
4. 用户管理实战中最常见的四个坑
这一节把我在实战中遇到的典型问题和完整排查链路写出来,你在自己环境里大概率会撞上一两个。
4.1 远程授权成功却连不上:按这个顺序排查
现象:GRANT都给了,SHOW GRANTS也显示有权限,但业务方反馈连不上。诊断要按链路一层一层排除,从外到内:
| 步骤 | 检查项 | 命令/方法 |
|---|---|---|
| 1 | 网络是否通 | ping 数据库IP |
| 2 | MySQL端口是否可达 | telnet 数据库IP 3306(新的系统可能没有telnet,用nc -vz IP 3306) |
| 3 | 端口是否真的在监听 | ss -lntp | grep 3306 |
| 4 | 服务是否绑定了所有网卡 | 查看my.cnf中bind-address,如果是127.0.0.1则远程永远连不上,需改为0.0.0.0或具体网卡IP |
| 5 | 是否启用了skip-networking | my.cnf里如果配置了skip-networking,TCP连接全部被拒,只能本地socket |
| 6 | 账号host是否匹配 | SELECT user, host FROM mysql.user WHERE user='app'; |
| 7 | DNS反解是否拖慢连接 | 开启skip_name_resolve=ON时连接更快,但host字段必须写IP形式,写域名会匹配不上 |
有一年我一个线上库频繁出现"应用超时"的报警,查到最后发现是skip_name_resolve没有开,每次新连接MySQL都在做DNS反解,遇到一次DNS抖动直接连接超时。开了skip_name_resolve=ON之后,这类问题彻底消失。
4.2 caching_sha2_password引发的客户端兼容性事故
这类事故我在8.0迁移排障时见过太多。现象是:密码明明正确,客户端就是报Authentication plugin 'caching_sha2_password' cannot be loaded或者握手失败。
根因在前面1.3节已经说过,这里补充一个排障链路:先确认当前用的是哪个插件:
SELECT user, host, plugin FROM mysql.user WHERE user='app';如果看到caching_sha2_password,而你的客户端驱动版本比较老,解决方案按优先级排列:
- 升级客户端驱动到支持8.0的版本(Java的Connector/J 8.0+、PHP的mysqlnd 7.2.4+、Navicat 12+);
- 临时把该账号降级为
mysql_native_password:
ALTER USER 'app'@'%' IDENTIFIED WITH mysql_native_password BY '同一个密码';- 全局改回老插件(最差方案,不推荐)。
这里有个容易忽略的知识点:8.0里插件是服务端决定的,客户端只是配合。客户端不能自己说"我要用老插件",所以哪怕客户端支持两种插件,只要服务端账号指定了caching_sha2_password,客户端就必须支持它。降级插件方案治标不治本,该升级客户端还是得升级。
4.3 授权明明给了却不生效:权限验证的优先级
场景:用户在mysql.db表里授权了某个库的SELECT,但业务端查询时报权限拒绝。
第一个要查的是SELECT CURRENT_USER();。因为MySQL判断权限用的是当前会话匹配到的账号,而不是你印象里的那个账号。如果授权时匹配到的是'app'@'%',而实际连接命中了'app'@'192.168.1.%'(host更精确,优先匹配),两个账号权限不一样,就会出现"授权给了却不生效"的假象。先确认自己是以哪个身份登录的:
SELECT CURRENT_USER(); SHOW GRANTS FOR CURRENT_USER();第二个常见原因是授权写错了对象。GRANT SELECT ON mydb.*和GRANT SELECT ON mydb.t1不是一个概念,两者分别存在mysql.db和mysql.tables_priv,我在审计时见过授权到了mydb.t1.*这种完全无效的写法,MySQL不会报错,但也不会按你的预期生效。
第三个原因是授权后已建立的连接不会自动刷新权限。权限是在连接建立时快照的,连接建立之后的权限变更不会影响已存在的会话。所以改完权限一定要让相关业务重连。这也是生产上"通知业务方重连一下连接池"这个操作的理论来源。
4.4 忘记root密码的急救流程
这可能是每一个MySQL使用者最恐惧的时刻。完整流程如下:
- 停止MySQL服务:
systemctl stop mysqld- 以跳过权限验证的方式启动:
mysqld_safe --skip-grant-tables &- 免密登录:
mysql -u root- 清空root密码:
FLUSH PRIVILEGES; ALTER USER 'root'@'localhost' IDENTIFIED BY '新密码'; FLUSH PRIVILEGES;如果ALTER USER因为插件问题执行不了,用下面这条应急:
UPDATE mysql.user SET authentication_string='' WHERE User='root'; FLUSH PRIVILEGES;然后重启MySQL:
mysqladmin -u root shutdown systemctl start mysqld此时root密码为空,登录后再用ALTER USER设置正式密码。
这里有几个关键点:
--skip-grant-tables模式下,MySQL默认会跳过权限判断,这个状态绝对不能留到业务侧,操作完必须立刻恢复正常模式启动。这是救命流程,也是高危状态。- 在8.0里,单靠清空
authentication_string后重启,root依然可能无法正常ALTER,所以我的习惯是清空后马上FLUSH,再马上ALTER,少了一步都会绕回死胡同。 - 如果是主从环境或者多个MySQL实例,操作前确保没有正在写入的关键事务,避免在恢复root密码的过程中丢了binlog位置或触发从库误判。
最后再说点我的体会
MySQL用户管理做得顺不顺,往往决定了一个团队在数据库面前从容还是狼狈。我现在接手任何一套新环境的MySQL,第一周必做三件事:清点所有账号、收缩权限边界、确认密码策略和过期周期。这三件事做完,后面很多"离奇故障"都能提前消掉。一个小习惯分享给大家:把账号信息登记成一张表,列明账号名、host、用途、负责人、最近一次密码轮换时间和授权范围,每季度跟着审计脚本一起过一遍。这张表比任何人的记忆力都靠谱,出了事它就是排查的第一份依据。
数据库账号这东西,账面越干净,出问题的概率越低。给账号尽量少的权限,让每一颗权限的螺丝钉都有出处——这句话值得写进每一个团队的数据库规范里。