做MySQL运维这些年,我有个越来越强烈的感受:用户管理这块内容,看起来每个教程里都是一小章,真到生产环境里却是翻车重灾区。这篇文章就把我在实际项目中反复用到、也反复踩过的MySQL用户管理经验完整梳理一遍,含金量集中在用户与权限模型、账号生命周期操作、授权粒度设计,以及忘记密码、连接失败这些高频故障的真实排查过程。无论你是刚入门的学习者,还是在生产环境里维护数据库的开发和运维,都应该能从里面找到能直接落地的东西。
1. 用户管理的核心难点:账号概念不是你以为的那回事
1.1 MySQL里的“用户”,实际上是用户加来源主机
大多数人对数据库用户的最初理解,就是“一个用户名对应一套密码”。但MySQL的设计比这个多一个维度:登录身份由 user 和 host 共同组成,也就是 '用户名'@'主机'。同一个用户名,来源主机不同,就是完全不同的两个账号,甚至可以有完全不同的密码和权限。
我举个例子。服务器上有这两个账号:
sql复制'app'@'localhost'
'app'@'%'
localhost 那个可能是我本机登录用的管理员账号,% 那个是业务服务远程连接用的账号。要修改其中一个的密码,必须带着完整的 host 指定,只写 app 去改,命令可能直接报错,或者改到你不期望的那个账号上。新手最常见的困惑就在这里:为什么我明明给某个用户设置了新密码,但用旧密码还能登录?十有八九是存在两个同名但 host 不同的账号,你改的并不是业务实际连库时命中的那个。
这种设计从MySQL早期就存在,目的是便于针对不同来源主机做差异化授权。比如运维段内网可以放宽权限,公网来源就必须收紧。搞清楚了这层逻辑,后面所有的授权和管理操作才不容易糊。
1.2 用户和权限其实是两套独立体系
很多教程把用户管理和权限管理揉在一起讲,导致不少人对两者的边界不清晰。MySQL里账号体系归账号体系,授权体系归授权体系,它们可以分开操作。
账号本身只负责“能不能连上”,它包含用户名、来源主机、认证插件、密码、是否锁定、密码过期策略这些信息。而“能对哪些库表执行哪些SQL”,属于授权体系,由MySQL的权限系统单独管理。
这样的拆分带来了一个实际好处:你可以预先创建好一批账号,先不让它们登录任何业务库,等人来申请并审批通过后再单独授权,也能在账号保持不变的情况下,随时调整它的权限边界,做权限回收时不需要动账号本身。理解这一点,对后面设计权限申请流程很有帮助。
MySQL的权限范围按照从大到小可以分成:全局权限、库级权限、表级权限、列级权限,以及存储过程和函数级别的权限。每个层级对应不同的系统授权表:
| 权限范围 | 控制数据表 | 常见授权语法示例 |
|---|---|---|
| 全局 | mysql.user | GRANT SELECT ON . TO 'u'@'h' |
| 数据库 | mysql.db | GRANT SELECT ON db1.* TO 'u'@'h' |
| 表 | mysql.tables_priv | GRANT SELECT ON db1.t1 TO 'u'@'h' |
| 列 | mysql.columns_priv | GRANT SELECT(col1) ON db1.t1 TO 'u'@'h' |
| 存储过程/函数 | mysql.procs_priv | GRANT EXECUTE ON PROCEDURE db1.p1 TO 'u'@'h' |
权限是按范围“叠加生效”的。查询一条数据时,MySQL会先把全局权限、库级权限、表级权限全部整合起来,再判断你是否有权执行相应的操作。这带来的含义是:如果你在全局层面对某个用户赋予了太多权限,即使之后想在某个库里限制它,也很难收得住,因为全局权限的判断优先级通常会覆盖更细粒度的限制。所以生产环境里我强烈建议,能用库级或表级权限表达的,就尽量不要给全局的 . 权限。
1.3 MySQL 8.0之后,又多了一个认证插件的关卡
如果你所在的环境还是MySQL 5.7时代过来的,一定对 mysql_native_password 这个默认插件很熟悉。但从MySQL 8.0开始,默认认证插件改成了 caching_sha2_password。
这个改动的背景是安全升级,caching_sha2_password 比老的 native 插件在密码传输和存储上更安全。但随之而来的坑是:一些老版本的客户端、图形工具、旧驱动并不认识这个新插件,连接时会直接报类似下面这样的错误:
text复制Authentication plugin 'caching_sha2_password' cannot be loaded
或者:
text复制Client does not support authentication protocol requested by server
我之前排查过一个使用老版本JDBC驱动的Java服务,升级数据库到8.0后突然连不上,就是认证插件不兼容导致的。处理思路很明确:
- 最优先的方案是升级客户端、驱动或图形工具版本,让它们支持
caching_sha2_password; - 如果业务环境实在无法升级,可以单独为业务账号指定老插件,用类似下面的语法:
sql复制CREATE USER 'app'@'%' IDENTIFIED WITH mysql_native_password BY 'your_password'; - 但不要因为某一个老客户端兼容不了,就把整个MySQL实例的默认认证插件改回老版本,那样等于让所有账号都承担不必要的安全风险。这个后面故障排查章节还会详细展开。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 账号生命周期操作清单:从创建到回收的标准动作
2.1 创建账号的标准姿势与参数细节
创建账号,官方标准语法是 CREATE USER。一个常规示例长这样:
sql复制CREATE USER 'report'@'192.168.%' IDENTIFIED BY 'Report@2024';
这条语句做的事情包括:在MySQL中创建用户、指定允许的来源网段为 192.168.%、设置认证插件为数据库默认值并写入密码。如果你希望这个账号用更高级的加密认证方式或者兼容老客户端,可以显式指定:
sql复制CREATE USER 'app'@'%' IDENTIFIED WITH caching_sha2_password BY 'App@2024';
很多MySQL初学教程会让你用 INSERT INTO mysql.user ... 这种粗暴方式创建账号,我在生产环境里极其不建议这么做。直接写系统权限表跳过了内部一致性检查,容易造成密码哈希格式错误、权限相关的辅助表未同步、甚至账号创建不完整等诡异问题。现代版本的管理动作,统一走 CREATE USER,权限管理统一走 GRANT / REVOKE,不要自己去动系统表。
还有一种常见场景:开发和测试环境希望刚创建的用户能立刻操作某张表,那么 CREATE USER 和 GRANT 可以一起写,比如:
sql复制CREATE USER 'dev_zhang'@'%' IDENTIFIED BY 'Dev@123';
GRANT SELECT, INSERT, UPDATE ON demo_db.* TO 'dev_zhang'@'%';
这里我多说一句,生产环境尽量别用 % 作为授信主机范围,应该用具体的应用服务器IP或网段。% 意味着只要能通过密码验证,任何来源主机都能登录。一旦密码泄露,攻击面会被放大很多。
2.2 修改密码、锁定解锁与账号重命名
修改密码时,很多DBA还在用老语法 SET PASSWORD FOR ...,但在MySQL 8.0中更推荐的写法是 ALTER USER。比如业务方报告某个应用账号密码要轮换,可以这样执行:
sql复制ALTER USER 'app'@'%' IDENTIFIED BY 'NewApp@2025';
注意修改完成后,使用该账号的已有连接不会被强制断开,新连接才需要用到新密码。这经常被忽略,运维以为改了密码就生效,结果业务方反馈“还能连上”,其实那只是长连接还没有断开。
账号锁定和解锁也是高频率操作,比如员工离职、业务下线阶段,往往不删除账号而是先锁定:
sql复制-- 锁定账号,禁止新连接
ALTER USER 'dev_zhang'@'%' ACCOUNT LOCK;
-- 解锁账号
ALTER USER 'dev_zhang'@'%' ACCOUNT UNLOCK;
账号重命名的场景相对较少,一般出现在“这个账号交给新团队接管”的时候。可以用:
sql复制RENAME USER 'old_account'@'%' TO 'new_account'@'%';
RENAME USER 会把原有权限一并迁移过去,算是一个比较方便的操作。但要注意,在业务代码和配置里连接串使用的账号名也要同步修改,否则改完等于直接断连。
最终的账号回收,用 DROP USER:
sql复制DROP USER 'dev_zhang'@'%';
DROP USER 会同时把该账号在 mysql.user 以及各级授权表中的相关记录一并清理,比手动 DELETE 后还要担心残留权限干净得多。
2.3 密码过期策略和账户状态检查
企业信息安全规范里通常要求数据库密码定期更换。MySQL提供了密码过期机制,可以强制账号在指定周期后修改密码。比如要求某个账号密码每90天过期一次:
sql复制ALTER USER 'app'@'%' PASSWORD EXPIRE INTERVAL 90 DAY;
密码过期后,这个账号依然可以连接,但会进入受限状态,只能执行修改密码之类的有限操作。在业务侧表现出来的症状就是“连上了但所有正常SQL都报错”,排查时很容易忽略密码过期这个点。登录后执行 ALTER USER USER() IDENTIFIED BY 'NewPassword'; 即可解除该状态。
如果企业没有强制密码过期需求,可以设置成永不过期:
sql复制ALTER USER 'app'@'%' PASSWORD EXPIRE NEVER;
日常巡检时,我常用的检查语句是把账号的基本状态全部看一下:
sql复制SELECT user, host, account_locked, password_expired, password_last_changed
FROM mysql.user;
这张表的输出能很快告诉我:哪些账号被锁了,哪些账号密码过期了,哪些账号已经很久没有改过密码。账号生命周期管理的核心就是在这些状态之间做好切换,而不是单纯地“创建了就不管、离职了也不回收”。
2.4 配套的密码强度要求
MySQL 8.0默认装有密码校验组件 validate_password,它会对新设置的密码做强度检查。在默认配置下,密码过短或者太简单会被直接拒绝:
sql复制CREATE USER 'weak'@'%' IDENTIFIED BY '123456';
-- ERROR 1819: Your password does not satisfy the current policy requirements
这个组件在生产中很有价值,因为很多开发图省事设置“123456”或“password”,这等同于把数据库大门敞开着。如果你是在内网测试环境,确实想让密码简单一点,可以临时调整策略级别:
sql复制SET GLOBAL validate_password.policy = LOW;
SET GLOBAL validate_password.length = 6;
但这里同样有个坑要注意,SET GLOBAL 的设置在MySQL重启后会失效,想永久调整要达到配置文件里修改。另外,密码强度策略从5.7到8.0在参数名上发生了变化,8.0中 validate_password 相关参数以 validate_password. 开头,老版本则直接用 validate_password_policy。
3. 授权与回收:从“一把梭”到最小权限的演进
3.1 最小权限原则的真正含义
用户管理做得好不好,比账号本身更重要的是权限设计。很多项目初期为了赶进度,开发会申请一个root账号,或者运维图省事直接给了全部权限,这种操作在测试环境可能感觉没问题,一旦上了生产,迟早出事。
我印象很深的一次故障是这样的:某个报表系统连接MySQL使用的账号被授予了 ALL PRIVILEGES ON *.*。后来有次前端接口出现SQL注入,攻击者利用查询功能执行了 DROP TABLE,整个核心业务表被清空。虽然最后通过备份恢复,但教训特别深刻——如果当时只给这个账号 SELECT 权限,注入导致的最坏结果也就是数据被读取,不会发生删除。
最小权限原则不是说把所有权限都砍掉,而是让每个账号只拥有“完成本职工作必须要用到的权限”。一个报表系统通常只需要 SELECT,如果还要导出数据,可能加上 PROCESS,但没必要给它 DROP、ALTER、CREATE 这类DDL权限。同样的道理,业务应用账号如果需要写入,给 INSERT、UPDATE、DELETE 就够了,不给 DDL 操作权限。
3.2 权限制定的细化思路与授权命令
授权前先想清楚这几个问题:这个账号要连哪个库?要执行哪些类型的SQL?来源主机大概是什么范围?是长期使用还是临时开通?想清楚后,授权语法就会非常明确。
比如一个数据报表平台,需要读取订单库和用户库,但不需要写任何数据:
sql复制GRANT SELECT ON orders_db.* TO 'report_bi'@'10.0.%';
GRANT SELECT ON user_db.* TO 'report_bi'@'10.0.%';
再比如某个后台管理服务,需要对配置表做增删改查,同时也需要读取部分日志表:
sql复制GRANT SELECT, INSERT, UPDATE, DELETE ON config_db.* TO 'admin_srv'@'192.168.%.%';
GRANT SELECT ON log_db.* TO 'admin_srv'@'192.168.%.%';
如果多个微服务都要连同一个数据库,需要注意不要给它们创建同一个账号。每个服务使用独立账号的好处是:权限隔离清晰、密码轮换互不影响、日志审计也能定位到具体是哪个服务在操作。
某些场景还需要更细的粒度。比如只允许某账号读取某张表的某些列,可以这样授权:
sql复制GRANT SELECT (id, user_name, created_at) ON user_db.user_info TO 'user_srv'@'%';
但我也提醒一下,列级权限对查询规划器有一定影响,而且应用如果执行 SELECT *,在只有列级权限的情况下会直接报权限不足。所以除非有强安全诉求,优先把粒度控制在库级或表级,更容易维护。
3.3 THE GRANT OPTION 与角色功能的风险点
授权时最容易忽略的隐患是 WITH GRANT OPTION。拥有该属性的账号,可以把自己已有的权限再授予其他账号。假设一个普通业务账号拿了 WITH GRANT OPTION,即便它只有某个库的 SELECT 权限,它也能把这部分权限分享给任意其它账号,权限边界一下就失控了。
我见过的安全事故中,就有开发为了方便,给自己的账号加了 WITH GRANT OPTION,结果同事离职后,旧账号仍保留了一堆通过开发账号“复制”出来的权限,导致清理工作异常艰难。生产环境里,普通业务账号一律不要带 WITH GRANT OPTION,只有少数专职DBA账号或管理账号才需要这个能力。
从MySQL 8.0开始,官方建议用“角色”来管理权限集合,角色相当于一组权限的打包。比如:
sql复制-- 创建角色
CREATE ROLE 'read_only_role';
-- 给角色赋权
GRANT SELECT ON orders_db.* TO 'read_only_role';
-- 把角色授予用户
GRANT 'read_only_role' TO 'report_bi'@'%';
这样做的价值在于:权限需求变更时只需要调整角色本身,所有被授予该角色的账号就会批量生效。如果某个员工离职,直接从他账号上撤回角色,省掉了逐条查看和删除权限的麻烦。角色在MySQL 8.0中已经是生产可用的成熟能力,我强烈建议权限数量多、账号规模大的环境尽早引入。
3.4 权限回收与验证:删权限别忘了看实际效果
权限回收使用 REVOKE。比如之前给某个临时分析账号开通了订单表的写权限,现在要把写权限收回:
sql复制REVOKE INSERT, UPDATE, DELETE ON orders_db.* FROM 'temp_analyst'@'%';
如果想把某个账号在某库上的所有权限一把回收,可以这样做:
sql复制REVOKE ALL PRIVILEGES ON orders_db.* FROM 'temp_analyst'@'%';
回收后要记得验证当前生效的权限,最常用的查询语句是:
sql复制SHOW GRANTS FOR 'temp_analyst'@'%';
之前我在文章开头就强调过,账号是账号,权限是权限。很多运维在撤权限时只执行了 REVOKE,却忘了这个账号本身还能登录MySQL。如果该账号不再需要连接,应该再执行 ALTER USER ... ACCOUNT LOCK 或者直接 DROP USER,才能真正堵住后门。
4. 高频故障排查实录:从连接失败到密码找回的完整链路
4.1 忘记root密码后的恢复流程
忘记root密码这事,干这行的几乎没有不遇到的。以前的老办法是启动时加 --skip-grant-tables,然后直接进去UPDATE系统表把密码改掉。但在MySQL 8.0中,这条路有一个关键细节变了,我单独拎出来说。
整体恢复流程分五步。
第一步,确认当前实例身份和可维护窗口。如果线上是主库且有从库在同步,直接重启会引发主从延迟或切换问题,尽量先做好降级预案,或者选择业务低峰期执行。
第二步,停止MySQL服务。以systemd管理的MySQL为例:
bash复制sudo systemctl stop mysqld
第三步,在配置文件中临时加入跳过授权验证的参数,然后启动服务:
ini复制[mysqld]
skip-grant-tables
这一步会让MySQL启动时不加载权限表,任何账号都可以跳过密码直接登录,所以操作时千万注意网络隔离,别让外部机器有机可乘。
第四步,使用root用户免密登录:
bash复制mysql -uroot
正常情况下会直接进入MySQL命令行。接下来有一个容易踩的坑:在 --skip-grant-tables 模式下,直接执行 ALTER USER 可能报错,提示当前模式不允许执行该语句。网上很多老教程会让你用UPDATE去改 mysql.user 表的 authentication_string,但在MySQL 8.0中,这样手动UPDATE密码字段很容易把哈希格式写坏,而且部分版本甚至没有 password 列,只有 authentication_string。正确的做法是先让权限表重新生效:
sql复制FLUSH PRIVILEGES;
然后再执行:
sql复制ALTER USER 'root'@'localhost' IDENTIFIED BY 'NewRoot@2024';
这时的 ALTER USER 就能正常工作了。
第五步,修改完成后把配置文件里的 skip-grant-tables 去掉,再正常重启MySQL:
bash复制sudo systemctl restart mysqld
mysql -uroot -p
这里还要提醒一点,MySQL 5.7初次安装时通常会在启动日志里生成一个临时密码,很多人没用过会忘记。后面重装时找不到初始密码,不要急着卸载重装,先搜索一下日志目录下的临时密码记录,比如 /var/log/mysqld.log 中包含 temporary password 的行,很多时候找回来比重置更省事。
4.2 经典的 error 2002 socket 连接失败问题
在类Unix系统上本地连接MySQL时,客户端默认会通过socket文件与服务端通信,这个文件的默认路径通常是 /var/lib/mysql/mysql.sock 或者 /tmp/mysql.sock。如果连接时报了下面这个错误:
text复制ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/tmp/mysql.sock' (2)
很多人第一反应是“MySQL服务挂了”,但真实原因并不总是这样。正确排查顺序应该是这样一条链路。
第一步,先确认MySQL进程是否在运行:
bash复制ps -ef | grep mysqld
如果进程不存在,去查看MySQL错误日志,通常路径是 /var/log/mysqld.log 或 /var/log/mysql/error.log。日志里常见的原因包括:磁盘空间写满、datadir 目录权限不正确、非正常关机导致的表空间损坏等。把根因解决后再启动服务。
第二步,如果进程明明在运行,但客户端就是走默认socket连接不上,那大概率是socket文件路径不一致。比如通过源码编译或Docker方式运行的MySQL,socket文件可能被配置在别的位置。这时候可以显式指定socket路径连接:
bash复制mysql -uroot -p --socket=/var/run/mysqld/mysqld.sock
能连上,就说明是路径不匹配。
第三步,更深入的验证是用TCP方式连接,排除socket问题:
bash复制mysql -uroot -p -h127.0.0.1 -P3306
如果TCP方式能连上,而socket方式连不上,问题就基本锁定在socket路径或权限上。此时检查MySQL配置文件:
ini复制[mysqld]
socket=/var/run/mysqld/mysqld.sock
再看客户端连接时默认使用的socket路径是不是同一个。在某些系统上,客户端还会受 /etc/my.cnf 或 ~/.my.cnf 中 [client] 段的 socket 选项影响。老手一般都习惯在连接命令里显式指定socket,或者通过软链接把两个路径指向同一个socket文件。
4.3 Navicat等客户端报认证插件错误
图形化客户端连接MySQL 8.0时,常见报错类似于:
text复制Authentication plugin 'caching_sha2_password' cannot be loaded
这个问题的原因在前面说过:用户使用了新的默认认证插件 caching_sha2_password,而客户端版本太老,不认识这个插件。我在多个项目里处理这种问题时,首选方案是让客户端升级版本。Navicat、SQLyog、DataGrip以及各种语言的数据库驱动,在新版本中基本都已经支持 caching_sha2_password。
如果因为某些原因客户端无法升级,可选的兼容方案是把当前用户的认证插件改回老版:
sql复制ALTER USER 'app'@'%' IDENTIFIED WITH mysql_native_password BY 'App@2024';
改成老插件后,旧客户端就可以连接了。但是你要清楚,这只是兼容性的权宜之计。mysql_native_password 虽然现在还能用,但它属于已经被官方标记为废弃方向的插件。生产环境里,最好是新建一个单独的账号给老客户端用,而不去动主要账号的认证插件,这样可以两头兼顾:
sql复制-- 主业务账号保持默认的新插件
CREATE USER 'app_new'@'%' IDENTIFIED BY 'App@2024';
-- 历史遗留老客户端单独用一个兼容账号
CREATE USER 'app_legacy'@'%' IDENTIFIED WITH mysql_native_password BY 'App@2024';
两种账号同时存在,权限基于最小化原则各自分配,这样既不影响新业务的安全基线,也不阻塞老旧系统的运维节奏。
4.4 远程连接被拒:账号host和网络配置的联合排查
远程连接MySQL时收到 Access denied,或者 Host 'x.x.x.x' is not allowed to connect to this MySQL server,需要把账号授权和网络配置放一起排查,不能只看其中一项。
先看报错类型。密码或账号错误时,常见的报错是:
text复制ERROR 1045 (28000): Access denied for user 'app'@'x.x.x.x' (using password: YES)
这种情况有几种可能:密码确实输错了,或者MySQL里没有匹配到 'app'@'x.x.x.x' 这个用户,却存在 'app'@'localhost' 或 'app'@'%'。如果只给 'app'@'localhost' 授权,来自远程IP的连接当然匹配不上。有同名账号时,MySQL的匹配规则会查完所有对应host后,按优先级决定是否允许登录。多数情况下,你应该为远程来源单独创建或者调整host匹配:
sql复制CREATE USER 'app'@'10.10.%.%' IDENTIFIED BY 'App@2024';
GRANT SELECT, INSERT, UPDATE ON business_db.* TO 'app'@'10.10.%.%';
另一种常见报错是:
text复制ERROR 1130 (HY000): Host 'x.x.x.x' is not allowed to connect to this MySQL server
这表示连接到达了MySQL服务端,但服务端层面拒绝了该来源主机,账号密码还没走到验证那一环。此时要检查 bind-address 和 skip-networking 这两个服务端参数。如果配置了 skip-networking,MySQL不会监听TCP端口,只接受本地socket连接,任何远程请求都到不了。如果 bind-address 设置为 127.0.0.1,同样只允许本机回环地址连接。需要跨机器访问时,bind-address 至少要是内网地址,或者直接设为 0.0.0.0 表示监听所有网卡,但这同时会带来更大的暴露范围,生产环境还需要额外用防火墙收敛访问来源。
最后一道常见关卡是云服务器或本机防火墙。即使MySQL账号授权没问题、bind-address也正确,远程IP依然可能因为防火墙没有放行3306端口而超时或拒绝连接。Linux上可以先看是否安装了防火墙并放行端口,云主机还要看安全组规则。
这类问题排查的通用顺序,我是这样固定下来的:先本地用TCP -h127.0.0.1 测试,排除MySQL自身服务问题;再在远程机器用 telnet 目标IP 3306 或 nc -vz 目标IP 3306 测试端口连通性,判断是否被防火墙拦截;最后再根据报错类型回到账号授权和认证插件上做处理。分步定位,比在一堆可能性里瞎猜要快得多。
5. 把用户管理变成日常制度:审计SQL与巡检建议
5.1 一屏看全所有账号和权限的审计SQL
账号多起来之后,记忆是不可靠的,巡检必须依赖工具和SQL。我自己常用的几个审计查询可以分享出来。
查看所有账号基础信息:
sql复制SELECT user,
host,
plugin,
account_locked,
password_expired,
password_last_changed
FROM mysql.user
ORDER BY user, host;
查找可能拥有全局高权限的账号:
sql复制SELECT user,
host,
Select_priv,
Insert_priv,
Update_priv,
Delete_priv,
Create_priv,
Drop_priv,
Grant_priv
FROM mysql.user
WHERE Select_priv = 'Y'
OR Insert_priv = 'Y'
OR Update_priv = 'Y'
OR Delete_priv = 'Y'
OR Create_priv = 'Y'
OR Drop_priv = 'Y'
OR Grant_priv = 'Y'
OR Super_priv = 'Y'
OR Reload_priv = 'Y';
查看所有拥有授权能力的账号,这类账号是最需要重点关注的:
sql复制SELECT user, host
FROM mysql.user
WHERE Grant_priv = 'Y'
OR user = 'root';
查看库级和表级权限分布:
sql复制SELECT user,
host,
db,
Select_priv,
Insert_priv,
Update_priv,
Delete_priv,
Create_priv,
Drop_priv
FROM mysql.db
ORDER BY user, host, db;
需要精确查看某个账号的全部权限时,用:
sql复制SHOW GRANTS FOR 'app'@'%';
把这些SQL固化成一个巡检脚本,定期执行并把结果归档,权限漂移就能被及时发现。比如某天发现原本只该有只读权限的账号多了 Drop_priv,就能顺藤摸瓜找到是谁、什么操作导致的。
5.2 账号命名规范和权限申请走流程
账号数量一旦增多,没有规范就会变成一团乱账。我见过一个数据库实例上同时存在 test、dev、123456、root 各种千奇百怪的账号,到最后没人说得清哪个是哪个业务在用,清理时只能靠猜。
建议从制度上做几件事。
第一,账号命名统一格式,比如“业务标识-环境-用途”,例如 order_prod_app、report_bi_readonly。host部分尽量明确网段,不要习惯性写 %。
第二,建立账号和权限的申请回收流程。最小的闭环是:开发提出申请,说明用哪个库、哪几张表、要哪些权限、从哪台机器连,DBA审批后按最小权限创建账号并做记录;员工转岗或离职时,及时锁定或删除账号。很多公司出事就出在离职员工的数据库账号没有及时回收。
第三,对高权限账号做双人复核。root和带 Grant_priv 的账号,每次变更都需要有操作记录和复核,防止“管理员删库跑路”这类极端情况。
5.3 自动化巡检的落地思路
对于有一定规模的集群,人工巡检迟早覆盖不过来。可以把下面这些检查项做成脚本,周期执行并输出报告:
- 查找存在
mysql.user表中但长期未登录的僵尸账号; - 查找拥有全局写权限或
Grant_priv的账号数量; - 查找密码已经过期但仍然在使用的账号;
- 查找host为
%的高权限账号; - 对比上期快照,找出新增、删除或权限变更的账号。
脚本实现不复杂,核心就是定时执行上面那几条SQL,并把结果和上一次进行比较。排查出变化后,推送到告警群或者工单系统,交由对应负责人确认是否合规。
我自己实践下来的经验是:这类检查的性价比很高,耗费的数据库资源极小,但在安全事件发生前能发现大量隐患。有一次例行巡检让我发现某测试库账号居然带着全局创建权限,而密码还是写在项目文档里的弱口令。这种情况如果不主动扫描,可能在系统被攻破时才会暴露出来,那时候代价就大了。
最后再分享一个让我印象深刻的教训,也是想强调的一环。有一次一个业务方找我说应用突然登录不了,我在MySQL客户端里执行各种测试权限都正常,甚至用业务账号在命令行里手动连接同一台机器也能成功。排查了很久才发现,应用服务器上的旧配置文件里写着另一个数据库地址,那个地址早就下线了。所以,做用户管理类的故障排查,先确认应用连接串连的到底是不是你正在看的那台实例,再回头查MySQL端的账号和权限,这一条可以帮你省下大量无效排查时间。
