聊到MySQL运维,用户管理是被提得最多、也最容易“看起来会了”的一块内容。建账号、给权限、改密码,看似几分钟的事,但在生产环境里一细究,全是细节:为什么localhost能连、换成%就连不上?为什么同一个用户换了密码,应用侧集体报错?为什么权限明明授了,客户端还是提示没有权限?这些坑我踩过不少,也帮身边同事排查过很多回。这篇文章把MySQL用户管理从原理到实操完整梳理一遍,适合刚接手数据库的开发者,也适合需要给团队制定账号规范的DBA新人。你能从中得到的,不只是几条SQL语句,还有一套遇到用户相关报错不会慌的排查思路。
1. 用户管理到底在管什么:先搞清楚权限模型
1.1 用户和权限的底层关系
MySQL的用户体系跟平时理解的“登录名”不太一样。一个完整的MySQL用户由两部分组成:用户名和登录主机。比如'zhang'@'localhost'和'zhang'@'%',在MySQL看来是两个完全独立的账号。这带来一个很隐蔽的影响:如果只按用户名去改密码、授权限,很可能操作的是其中一条记录,而应用实际用的是另一条记录。
我刚接触MySQL时就在这里栽过跟头。当时把一个账号的密码改了,应用却一直连不上,排查了半天发现mysql.user表里有两个zhang,一个限制本机一个允许任意主机,我改的是localhost那个,应用连的是远端那个。从那以后我养成了一个习惯:凡是涉及用户的操作,一律带上host完整写出,比如'zhang'@'192.168.1.%',不带host的操作除非我明确知道后果,否则不执行。
MySQL的校验流程分两步:先认证身份,再检查权限。客户端发起连接时,MySQL会根据来源IP在mysql.user里匹配对应的账号记录,匹配成功就验证密码;密码通过后,这条连接后续能执行什么操作,由这套权限记录决定。这里有个关键点:权限是在连接建立时一次性加载的,连接建立之后你再修改权限,这条已经存在的连接不会自动生效。很多人改了权限后说“没效果”,多半就是没重新连接。
1.2 授权表是怎么协同工作的
MySQL把权限信息散落在mysql系统库的几张表里,理解这几张表的关系,就能理解很多诡异权限问题。
最核心的是mysql.user表,它保存全局权限,比如SELECT ON *.*、CREATE ON *.*,作用范围是所有库。其次是mysql.db表,保存某几个库级别的权限,比如只对db1库有INSERT权限。再往下是mysql.tables_priv,管表级别权限;mysql.columns_priv管列级别权限;还有mysql.procs_priv管存储过程和函数权限。
权限检查的优先级是:全局权限优先于库权限,库权限优先于表权限,表权限优先于列权限。这句话听着简单,但实际很有用。举个例子:用户A对db1库没有任何权限,但全局有SELECT权限,那他还是能查db1的数据。相反,如果全局SELECT被撤销,只保留了db1库的SELECT,那他只能查db1。生产环境里经常遇到“为什么这个账号能查别的库”的疑问,基本都能用这张权限层次表解释。
这里还要说一个高频疑问:什么时候需要执行FLUSH PRIVILEGES?很多人写完GRANT语句后习惯性FLUSH一下,其实没必要。通过CREATE USER、GRANT、REVOKE、SET PASSWORD这类SQL语句修改授权时,MySQL会自动重载权限数据。只有当你直接操作了系统表,比如对mysql.user执行了INSERT、UPDATE、DELETE,才需要手动执行FLUSH PRIVILEGES让变更生效。直接改系统表这种操作我一般不建议做,除非是应急恢复,因为跨过授权层容易留下数据不一致的隐患。
2. 创建用户的完整姿势
2.1 基础创建语句与参数拆解
创建用户的标准语句是:
CREATE USER 'username'@'host' IDENTIFIED BY 'password';比如CREATE USER 'app'@'192.168.1.%' IDENTIFIED BY 'StrongPass@123';这条语句做的事情是:在mysql.user表插入一条记录,加密保存密码,默认没有任何权限。注意,新创建的用户登录后连SHOW DATABASES都只能看到系统库,这是符合预期的,因为权限还没有分配。
MySQL 8.0里默认的密码认证插件是caching_sha2_password。这个插件比老版的mysql_native_password安全得多,但也带来一个兼容性问题:一些老的客户端库和图形工具可能连不上。如果遇到这种报错,通常有两个解法:一是升级客户端驱动,这是最推荐的做法;二是在MySQL侧把该用户改成mysql_native_password,例如:
CREATE USER 'old_client'@'%' IDENTIFIED WITH mysql_native_password BY 'password';不到万不得已我不建议为了兼容老客户端降低认证强度。如果真要用,至少只给特定用户开,别把root也改了。
密码复杂度方面,MySQL 8.0可以启用validate_password组件,启用后系统会拒绝弱密码。配置方式大致是:
INSTALL COMPONENT 'file://component_validate_password';装上之后,密码最短长度、大小写字母数量、数字和特殊字符要求都可配置。我在测试环境见过不少账号密码直接是123456的,生产环境如果条件允许,这个组件建议必开。
2.2 主机限制:localhost和%的真实差别
localhost和%的差别是新手最容易迷糊的地方。'user'@'localhost'表示只有从本机发起的连接才能用这个账号登录,'user'@'%'表示来自任意主机的连接都可以。看起来只是范围差异,但权限模型里host参与匹配,还会影响账号是否生效。
更隐蔽的一个坑是:MySQL在匹配用户时,不只看用户名,还会比对host。如果系统里存在匿名用户,也就是mysql.user中User字段为空字符串的记录,当你在本机用某个普通账号连接时,反而可能被匿名用户“抢”走,导致登录后权限不对劲。排查方式很简单:
SELECT user, host FROM mysql.user WHERE user = '';如果存在匿名账户,建议直接删掉,或者确认是系统需要保留的再做限定。这个坑在MySQL刚安装完时比较容易出现,因为我遇到过一些人安装时选择了允许匿名登录的选项。
生产环境的host配置,我强烈建议不要图省事全写%。最稳的做法是写死应用服务器的IP或网段,比如'app'@'10.0.3.18'或者'app'@'10.0.3.%'。这样做的好处不只是安全收紧,更重要的是出了问题好定位:一看host就知道这个账号该从哪里连。如果你有一堆'%'账号,出了登录告警后根本分不清是哪个应用在使用。
2.3 实际项目中的用户规划建议
用户管理做得好不好,往往不在于SQL写得多溜,而在于账号规划是否合理。我在实际项目里一般按角色拆账号:
- 管理账号:负责DDL、权限管理、参数调整等,通常只允许DBA从跳板机登录,host限制很严。
- 应用账号:按业务拆分,只授该业务库的读写权限,不做全局授权。
- 只读账号:给报表、数据分析、后台查询用,只授SELECT,必要时加上SHOW VIEW。
- 备份账号:需要能够读取全部库、锁表、刷日志,所以权限相对大,但只用于备份任务。
- 监控账号:只需要PROCESS、REPLICATION CLIENT等少量权限,用于采集状态。
命名上建议带上用途和环境的标识,比如app_read_prod、app_write_dev、backup_core。这个名字不是为了好看,而是为了后续审计时能快速判断“这个账号是干嘛的”。我见过有人把所有账号都叫test,半年后根本不敢删任何一个,因为根本分不清哪个系统在用。账号规划是一种低成本高回报的管理手法,越早做越省心。
3. 权限授予、查看与回收
3.1 GRANT语句的完整解析
创建用户之后,下一步就是授权。最常用的授权语句:
GRANT SELECT, INSERT, UPDATE ON dbname.* TO 'app'@'192.168.1.%';这个语句表示把dbname库里所有表的SELECT、INSERT、UPDATE权限授予指定用户。权限范围由ON子句决定,常见的几种写法:
ON *.*:所有库所有表,属于全局权限,谨慎使用。ON dbname.*:整个库所有表,日常业务账号最常用。ON dbname.tablename:指定表,适合权限细分场景。ON dbname.tablename(col1, col2):指定列,比如只让某用户查看用户表的用户名,不让他看手机号。
GRANT ALL PRIVILEGES表示授予某个范围内的所有权限。给管理账号授权时很常见,但要额外注意WITH GRANT OPTION这个子句。它表示允许该用户把自己的权限再授予其他用户。这个能力等于开了口子,如果管理不善,权限会像雪球一样越滚越大。我的建议是业务账号一律不给,管理账号也要严格控制数量。
MySQL常用权限里,业务开发接触多的是DML类:SELECT、INSERT、UPDATE、DELETE;运维接触多的是DDL类和系统类:CREATE、DROP、ALTER、INDEX、RELOAD、PROCESS、REPLICATION SLAVE、REPLICATION CLIENT等。每个权限含义都不一样,给之前最好查一下文档,别一股脑给ALL。
3.2 不同场景下的权限配置模板
我给几个可以直接抄作业的模板:
只读账号:
CREATE USER 'readonly'@'10.0.3.%' IDENTIFIED BY 'ReadOnly@123'; GRANT SELECT, SHOW VIEW ON business_db.* TO 'readonly'@'10.0.3.%';如果只读账号还需要做导出,注意SELECT INTO OUTFILE通常还需要FILE权限,这个权限作用域是全局的,给之前要慎重,因为开了文件写权限存在安全隐患。
业务读写账号:
CREATE USER 'app'@'10.0.3.%' IDENTIFIED BY 'AppPass@123'; GRANT SELECT, INSERT, UPDATE, DELETE ON business_db.* TO 'app'@'10.0.3.%';这里故意不给DDL权限,目的很明确:应用代码不应该能改表结构。真需要做表结构变更,走DBA的管理账号操作,省得哪天误执行了一条DROP语句还得做全库恢复。
备份账号:
CREATE USER 'backup'@'127.0.0.1' IDENTIFIED BY 'Backup@123'; GRANT SELECT, RELOAD, LOCK TABLES, PROCESS, REPLICATION CLIENT ON *.* TO 'backup'@'127.0.0.1';备份账号权限给得偏大,但host限制到备份服务器,同时限制只能本机或指定备份机连接,能有效降低风险。
主从复制账号:
CREATE USER 'repl'@'10.0.3.%' IDENTIFIED BY 'Repl@123'; GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'repl'@'10.0.3.%';这里有一处容易踩坑:主从复制的权限必须授予在*.*上,不能写db.*。我见过有人把复制权限授权到某个库上,结果从库一直报权限错误,就是这个原因。
3.3 查看用户权限的方法与技巧
创建和授权只是前半段,日常管理中最常见的是“查这个用户到底有什么权限”。最直接的方法是:
SHOW GRANTS FOR 'app'@'10.0.3.%';输出结果会按授权语句的形式列出所有权限。如果不带FOR,就是查看当前登录用户的权限。这个方法简单明了,但只能看整体授权语句,看不到某张表的独立授权细节。想看更底层的信息,可以查mysql库里的授权表:
SELECT * FROM mysql.tables_priv WHERE User = 'app'\G SELECT * FROM mysql.columns_priv WHERE User = 'app'\G另外还有几个information_schema视图可以用,比如USER_PRIVILEGES、SCHEMA_PRIVILEGES、TABLE_PRIVILEGES,它们把底层授权表读出来组成了更易懂的视图。日常排查我习惯先用SHOW GRANTS,不够再查底层表,很少直接去翻mysql.user里的权限位,因为那些字段布局对新手不太友好。
4. 日常维护:改密、锁定、删除一条龙
4.1 修改密码的三种方式
改密码是用户管理里操作频率最高的事,常见的写法有三种。
MySQL 8.0推荐用ALTER USER:
ALTER USER 'app'@'10.0.3.%' IDENTIFIED BY 'NewPass@123';老版本常用SET PASSWORD:
SET PASSWORD FOR 'app'@'10.0.3.%' = 'NewPass@123';还有一种命令行方式:
mysqladmin -u root -p password 'NewPass@123'第一种和第二种效果类似,命令行方式我基本不用,原因很简单:命令会留在shell历史记录里,密码等于明文暴露。只要是在业务环境,能不用就别用。
改密码时有个很容易忽略的流程问题:改了数据库账号密码,应用连接串里的密码也得同步改。这个看起来像是废话,但现实里因为改库没改应用导致线上告警的事故,我至少经历过三次。其中一次是某开发同学半夜改了老库密码,一早所有业务页面报“数据库连接失败”,原因就是配置中心里的连接串还是旧密码。所以改密码这件事,不是执行一条ALTER USER就结束的,要提前跟应用负责人沟通,确定好切换窗口。
4.2 锁定与解锁用户
有些场景不需要删除用户,只需要临时禁用它,比如员工离职交接期、账号被异常登录暂时冻结。这时候用锁定功能最合适:
ALTER USER 'app'@'10.0.3.%' ACCOUNT LOCK; ALTER USER 'app'@'10.0.3.%' ACCOUNT UNLOCK;锁定之后,该账号依然存在于mysql.user表里,所有授权都还在,只是无法登录。解锁之后恢复原状,比删除再重建要省事得多,也不用担心重建时漏掉权限配置。MySQL 5.7.6之后的版本都支持这个语法,所以基本不用担心兼容性。
还有一种场景是强制密码过期。比如你怀疑某个账号的密码可能泄露了,又不想立刻锁定业务,可以设置密码过期,让该用户下次登录时必须改密码:
ALTER USER 'app'@'10.0.3.%' PASSWORD EXPIRE;这个操作不会踢掉已经存在的连接,但对新连接会强制要求更新密码,适合做安全策略的缓冲处理。要注意的是,如果应用代码没有处理密码过期逻辑,这个操作也可能导致应用报错,同样需要提前沟通。
4.3 删除用户
删除用户用DROP USER:
DROP USER 'app'@'10.0.3.%';这里必须强调host的重要性。DROP USER 'app';这种写法在MySQL里默认操作的是'app'@'%',如果你实际想删的账号是'app'@'localhost',那这句SQL等于没删到。我见过有人漏掉host后重复执行同样的删除语句,折腾很久才发现系统里有两个同名host的记录。
可能有人问,为什么不直接DELETE FROM mysql.user WHERE User='app'?DROP USER不光是删记录,还会同步清理其他授权表中的关联数据,比如tables_priv、columns_priv里的记录,干净得多。直接删系统表则容易留下孤儿授权记录,而且必须手动FLUSH PRIVILEGES,麻烦不说,还危险。日常一律用DROP USER。
删除用户前建议先确认下线时间,最好用SHOW PROCESSLIST看一下有没有正在运行的会话:
SHOW PROCESSLIST;如果发现该用户还有活跃连接,先通知业务方停掉相关任务,或者手动KILL对应连接,再删用户。直接DROP一个正在使用的账号,应用会在下一次获取连接时收到Access denied错误,影响就会被放大。
4.4 用户管理的安全习惯
用户管理看着是技术活,其实是安全习惯的体现。我把平时坚持的几条经验列一下:
一是最小权限原则要落实到每个账号。给权限之前问一句“这个账号真的需要所有库的访问权吗?真的需要DROP权限吗?”回答不上来的,就先按最小集授,不够再加。权限缩减永远比扩张麻烦。
二是定期审计账号。我一般每个季度扫一次mysql.user表:
SELECT user, host, account_locked, password_lifetime FROM mysql.user;重点看有没有长期不用的账号、host是%的高危账号、已经锁定但还占坑的账号。审计的意义不是清空,而是让每个账号都有主,没有主就处理掉。
三是禁止root远程登录。root只保留localhost访问,远程操作通过普通管理账号加sudo方式登录服务器后再本地连接,最大程度减少root凭证暴露面。
四是可以考虑启用登录失败延迟插件。MySQL 8.0有connection_control插件,可以配置连续失败N次后延迟响应,能有效降低暴力破解风险。这个配置在文档里有现成示例,属于低成本高收益的安全加固。
5. 常见问题与排查实录
5.1 远程登录失败的排查思路
我接到最多的求助就是“远程连不上MySQL,报Access denied”。先说最常见的报错:Access denied for user 'xxx'@'x.x.x.x' (using password: YES)
排查顺序我一般这样走。
第一,确认账号和密码本身没问题。先在MySQL本机用同一套账号密码试着登录一下,如果能登录,问题大概率出在host匹配或网络层;如果本机也登不上,那先重置密码。
第二,确认host匹配。很多远程登录失败不是密码错,而是MySQL匹配不到对应的host记录。可以用:
SHOW GRANTS FOR 'xxx'@'x.x.x.x';如果报“There is no such grant defined”,说明这条账号不存在,需要去看看实际连接IP和mysql.user的host是否对得上。
第三,确认MySQL监听地址。默认配置下MySQL只监听127.0.0.1,外部主机根本连不到。检查my.cnf里的bind-address,如果是127.0.0.1,需要改成0.0.0.0或指定网卡IP,改完重启MySQL。
第四,确认防火墙和安全组放行3306端口。这一步和MySQL本身无关,但经常成为最后一根稻草。在服务器上用telnet测端口:
telnet 服务器IP 3306如果IP和端口都通,再继续查账号问题;如果不通,先去处理网络策略。
第五,检查是否有匿名账号干扰匹配。用前面提到的方法查一下mysql.user里User为空的记录。匿名账号在某些老版本安装里会默认创建,可能让正常的远程用户登录时匹配到错误的权限记录。
5.2 权限修改后不生效
“我明明授了权限,为什么还是报没有权限?”这个问题的排查思路比想象中简单。
先确认是不是没重连。前面说过,MySQL在连接建立时加载权限,已经存在的连接不会动态更新。所以修改权限后,把应用连接池刷新一下,或者等旧连接自然断开再重建,通常会解决。很多开发者改完权限后拿已有工具窗口反复试,当然没效果,新开一个连接试试就好。
再确认授权是否落在了正确的host上。比如应用连接时显示'app'@'10.0.3.8',你授权给的是'app'@'10.0.3.%',按匹配规则,IP形式的host更精确,应该能匹配上。但如果应用侧解析后的主机名变了,或者走了代理,实际匹配的host记录可能跟你授权的不一样。用SHOW GRANTS查一下连接时对应的账号:
SHOW GRANTS FOR 'app'@'10.0.3.8';查不到就说明host不匹配,需要补授权。
还有一种情况是直接改了系统表但没FLUSH PRIVILEGES。在应急恢复时直接对mysql.user执行UPDATE是可行的,但如果忘记FLUSH,MySQL的内存权限数据还是旧的。正常操作流程是:改完系统表 -> FLUSH PRIVILEGES -> 验证登录。
5.3 忘记root密码的应急方案
忘记root密码这种事情,遇到了会慌,但处理起来有固定套路。以MySQL 8.0为例,基本思路是跳过授权表启动实例,重置密码后恢复正常启动。
第一步,停止MySQL服务。
第二步,以跳过授权表且禁用网络的方式启动:
mysqld_safe --skip-grant-tables --skip-networking &这里必须强调--skip-networking。只跳过授权表而不禁用网络,相当于让整个MySQL对任意来源开放无认证访问,这在生产环境里是巨大风险。一定要加这个参数,只允许本机处理。
第三步,登录并加载权限数据:
mysql -u root进入MySQL后先执行:
FLUSH PRIVILEGES;为什么要先FLUSH?因为在skip-grant-tables模式下,MySQL的权限校验被临时关闭,不先加载权限表,后面直接ALTER USER可能会报权限不足的错误,或者行为不符合预期。
第四步,重置密码:
ALTER USER 'root'@'localhost' IDENTIFIED BY 'NewStrongPass@123';执行成功后退出,停止MySQL,重新按正常模式启动。启动后用新密码登录确认即可。
整个操作里最危险的一步就是忘记加--skip-networking,这一点我在处理故障时反复提醒自己。还有一点,操作前如果能备份mysql目录下的数据文件,尽量备份一下,虽然这是应急方案,但数据安全永远是第一位的。
最后再分享一个实操习惯:每次创建用户、授权、改密之后,我都会顺手执行一次SHOW GRANTS确认结果,同时在记录本里更新账号清单。这个习惯刚开始觉得多余,后来靠它避免了好几次误操作。用户管理看起来是细碎小事,但在生产环境里,一个host写错、一个权限给大,引发的故障规模往往是几何级放大的。平时把基础打牢,线上才会少折腾。