半夜被一个电话薅起来处理线上连接报错,登录服务器一看,日志里清一色的Access denied for user 'app_rw'@'10.20.33.18'。密码没改过,配置没动过,最后查出来是上个月新扩的两台应用机不在授权网段里。这件事之后我就把MySQL 创建用户并授权这套流程从"随手敲两条命令"改成了带检查、带验证、带留痕的标准动作。这篇就聊聊这套东西:它看着简单,CREATE USER加GRANT两条语句谁都会写,但真正让它在生产环境不出事的,是主机名匹配规则、权限粒度取舍、插件与密码策略、以及授权之后的验证闭环这些细节。适合刚接手数据库的开发和运维,也适合那些一直用 root 连库、准备把账号拆开但不知道从哪下手的朋友。下面所有内容都是围绕"怎么安全、准确、可复现地把一个账号建出来并给它恰好够用的权限"展开的。
1. 权限模型先想明白,再动手敲 CREATE USER
1.1 认证和授权是两道独立的关卡
很多人把"建用户"和"给权限"混成一件事,其实 MySQL 里这是两条完全独立的路径。第一道是认证(Authentication):服务器要知道"你是谁",靠的是mysql.user里的用户名、主机名、密码哈希和认证插件。第二道是授权(Authorization):服务器要知道"你能干什么",靠的是mysql.db、mysql.tables_priv、mysql.columns_priv这些权限表,内存里还有一份 ACL 缓存。
这个区分为什么重要?因为排错的时候,报错信息会直接告诉你卡在哪一关。ERROR 1045 Access denied ... (using password: YES)属于第一道关卡没过,问题在账号、密码或者主机名匹配上;ERROR 1142 SELECT command denied to user ...属于第二道关卡没过,账号是认出来了,但这个操作没权限。分不清这两类报错,排查方向就会跑偏——我见过有人对着 1142 拼命改密码,改了一下午。
再往深一层,认证阶段的匹配规则是这样的:客户端连上来之后,服务器拿到对方的 IP(如果skip_name_resolve关闭,还会做一次反向 DNS 解析得到主机名),然后拿这个 IP 和主机名去mysql.user表里逐行比对host字段。匹配上的行如果有多条,按 host 的具体程度排序,越具体的优先级越高,localhost高于10.20.%,10.20.%高于%。这个排序规则是理解后面所有"为什么我明明建了账号却连不上"的钥匙。
注意:MySQL 里的
localhost和你以为的127.0.0.1是两条不同的账号记录。Linux 下mysql -hlocalhost走 Unix Socket,服务端看到的 host 是localhost;mysql -h127.0.0.1走 TCP,看到的可能是127.0.0.1。你只建了'app'@'127.0.0.1',用localhost连就是连不上,反过来也一样。
1.2 为什么业务账号必须和 root 分开
我接手过一套系统,所有服务共用 root 账号,配置文件里明文写着密码。这种做法的风险不在于"谁会偷密码",而在于爆炸半径失控。一个只会SELECT的报表服务,因为拿的是 root,误删库、误改表结构、误 kill 别人的会话,全都能干成。更麻烦的是审计:出问题之后看日志,所有操作都来自 root,你根本不知道是哪个应用干的。
最小权限原则落地到 MySQL,具体就三条:
- 一个应用一个账号,不要多个服务共用。这样连接数、慢查询、锁等待都能按账号切分归因。
- 只给业务真正需要的库和操作。CRUD 服务给
SELECT/INSERT/UPDATE/DELETE就够了,别顺手ALL PRIVILEGES。 - 管理账号和业务账号分离。DDL 变更走独立的迁移账号,只在发布窗口用,平时可以
ACCOUNT LOCK锁着。
有人会说"分账号太麻烦,反正内网"。但真出事的时候,能不能在十分钟内定位到是哪个服务把表锁死了,取决于你当初有没有分账号。这不是洁癖,是排障效率问题。
1.3 5.7 到 8.0,账号管理接口变了不少
如果你手里的库版本比较杂,这一节能帮你少踩几个坑。最典型的就是默认认证插件的切换:5.7 默认mysql_native_password,8.0 默认caching_sha2_password。老版本的客户端驱动、老版本的可视化工具、某些语言的旧版连接库,连 8.0 会直接抛Authentication plugin 'caching_sha2_password' cannot be loaded。这个报错跟账号密码一点关系都没有,是客户端不认这个插件。
第二种变化是系统表不可直接改。5.7 时代流传着一种"野路子":直接UPDATE mysql.user SET password=...。8.0 把系统表改成了数据字典,这么干会直接报错。所以网上那些老教程里的语句,在 8.0 上不通用,看到PASSWORD()函数、看到直接改mysql.user的,一律跳过。对应的,FLUSH PRIVILEGES这个命令在 8.0 的日常授权流程里也基本不需要了——CREATE USER、GRANT、REVOKE都是自动刷新的。
第三种是角色(ROLE)机制从 8.0 开始可用。这个在后面第 3 节会展开,简单说就是把一组权限打包成一个角色,再把这个角色授予多个账号,适合账号数量上来了、权限组合开始重复的场景。
| 对比项 | 5.7 | 8.0 |
|---|---|---|
| 默认认证插件 | mysql_native_password | caching_sha2_password |
| 系统表能否直接 DML | 可以(不推荐) | 不可以 |
| 角色 ROLE | 不支持 | 支持 |
| 密码失败锁定 | 不支持 | 8.0.19 起支持 |
| 动态权限 | 不支持 | 支持(替代部分 SUPER) |
| 授权后是否需要 FLUSH | 不需要 | 不需要 |
2. CREATE USER 的写法与主机名匹配的坑
2.1 账号 = 用户名 + 主机名,这是两个字段
新手最容易犯的认知错误,是把"用户"理解成一个字符串。MySQL 里一个账号的完整标识是'用户名'@'主机名',两个部分共同构成主键。'app_rw'@'%'和'app_rw'@'10.20.%'是两个不同的账号,密码可以不一样,权限也可以不一样,互不影响。
标准写法是这样:
CREATE USER 'app_rw'@'10.20.%' IDENTIFIED BY 'P@ssw0rd_ChangeMe_2024';10.20.%表示只允许 10.20 网段连进来。这个写法比%好在哪里?如果哪天数据库端口因为配置疏忽暴露到了不该到的地方,%就是敞开的门,网段限制则多一层门槛。当然它不是安全边界(IP 可以伪造,也扛不住内网横向),但属于"低成本、有收益"的配置。
几个常见的主机名写法对比:
| host 值 | 含义 | 适用场景 |
|---|---|---|
% | 任意来源 | 临时调试,生产慎用 |
localhost | 本机 Socket 连接 | 本机脚本、本地运维 |
127.0.0.1 | 本机 TCP 连接 | 本机走 TCP 的程序 |
10.20.% | 10.20 网段 | 应用服务器固定网段 |
10.20.33.18 | 单个 IP | 单机部署、故障排查 |
app-01.internal | 主机名 | 开启了 DNS 解析的环境 |
这里有个隐蔽的坑:库名在 GRANT 语句里是支持通配符的,账号的 host 也支持,但两者的转义规则不一样。库名里如果带下划线,比如biz_db,GRANT SELECT ON biz_db.* TO ...里的_会被当成"任意单字符"通配符,可能意外匹配到biz1db、bizadb。严格写法要转义:GRANT SELECT ON \biz_db`.* TO ...`。这一点很多干了几年的人都没注意,因为恰好没有第二个同模式的库存在,所以一直没暴露。
提示:如果环境里存在匿名账号(
''@'localhost'),它会干扰所有本机连接的用户匹配。清理方法很简单,SELECT user, host FROM mysql.user WHERE user = '';查一下,有就DROP USER ''@'localhost';。这个坑在从旧环境迁移过来的实例上特别常见。
2.2 密码策略与认证插件怎么选
密码这块,8.0 默认装了validate_password组件(5.7 是插件形式),策略等级由validate_password.policy控制。三个档位:LOW只校验长度(默认 8),MEDIUM还要求包含数字、大小写字母和特殊字符,STRONG再加一个"不能是常见弱密码字典里的词"。
我的建议是生产环境用MEDIUM,开发环境可以降到LOW省点事。调整方法:
-- 8.0 SET GLOBAL validate_password.policy = MEDIUM; SET GLOBAL validate_password.length = 12; -- 5.7 SET GLOBAL validate_password_policy = MEDIUM; SET GLOBAL validate_password_length = 12;注意SET GLOBAL重启就失效,要持久化得写进配置文件。另外这类参数改完只对新密码生效,已有账号的密码不会被追溯校验。
认证插件怎么选,取决于你的客户端能力:
-- 明确指定插件(8.0 语法) CREATE USER 'app_rw'@'10.20.%' IDENTIFIED WITH caching_sha2_password BY 'YourStrongPwd_2024'; -- 老客户端兼容方案 CREATE USER 'app_legacy'@'10.20.%' IDENTIFIED WITH mysql_native_password BY 'YourStrongPwd_2024';caching_sha2_password的优势是密码传输不依赖明文等价物,安全性更好;代价是首次认证在非加密连接下需要走 RSA 公钥交换,老驱动可能不支持。我的处理原则是:新项目一律caching_sha2_password,老系统升级驱动;实在升不动的旧系统,单独开一个mysql_native_password的账号并限定网段,同时把"驱动升级"记进技术债清单,而不是全局把默认插件降级——那样等于为了一个旧系统牺牲了所有新账号的安全性。
密码本身的管理还有几个开关值得配上:
ALTER USER 'app_rw'@'10.20.%' PASSWORD EXPIRE INTERVAL 180 DAY -- 半年强制改密 PASSWORD HISTORY 5 -- 不能复用最近5次 FAILED_LOGIN_ATTEMPTS 5 -- 连续失败5次 PASSWORD_LOCK_TIME 1; -- 锁定1天最后两行需要 8.0.19 及以上。配上之后能挡掉一部分暴力尝试,但也意味着密码管理不能靠人脑记,必须进配置中心或密钥管理系统,否则半夜被锁在门外的是自己。
2.3 批量建号和命名规范
账号数量一旦超过二三十个,命名规范的价值就体现出来了。我一般用<系统>_<角色>_<环境>这样的结构,比如order_rw_prod、order_ro_prod、report_ro_prod。好处是SELECT user, host FROM mysql.user WHERE user LIKE 'order_%'一查就知道这套系统开了几个号,回收的时候也不会漏。
批量创建可以用脚本生成语句再执行,比手敲靠谱:
-- 先生成语句,肉眼过一遍再执行 SELECT CONCAT( 'CREATE USER ''', t.name, '''@''10.20.%'' IDENTIFIED BY ''', t.pwd, ''';' ) AS stmt FROM ( SELECT 'order_rw_prod' AS name, 'InitPwd_001' AS pwd UNION ALL SELECT 'order_ro_prod', 'InitPwd_002' UNION ALL SELECT 'report_ro_prod', 'InitPwd_003' ) t;这里有个经验:初始密码不要用统一值。我见过用Init@123456批量建号然后忘了改的,等于所有账号共用一把钥匙。更稳的做法是随机生成,建完立刻写进密码管理系统,人手上不落明文。
3. GRANT 的粒度选择与授权模板
3.1 五个授权层级与权限清单
GRANT的完整语法结构是:
GRANT 权限列表 ON 层级对象 TO '用户'@'主机' [WITH GRANT OPTION];层级对象决定了权限的作用范围,从大到小分五级:
| 层级 | 写法 | 说明 |
|---|---|---|
| 全局 | *.* | 所有库所有表,慎用 |
| 库级 | `biz_db`.* | 指定库的所有表 |
| 表级 | `biz_db`.`orders` | 单张表 |
| 列级 | `biz_db`.`orders`(id, amount) | 单表的指定列 |
| 例程级 | PROCEDURE `biz_db`.`proc_x` | 存储过程/函数 |
权限清单里,业务最常用的是这几个:SELECT、INSERT、UPDATE、DELETE、CREATE、DROP、ALTER、INDEX、CREATE VIEW、SHOW VIEW、EXECUTE、CREATE TEMPORARY TABLES、LOCK TABLES、TRIGGER。
关于ALL PRIVILEGES,有两个认知误区要澄清。第一,它不等于"所有权限",它只覆盖该层级上的静态权限集合,不包括GRANT OPTION(能不能把权限转授给别人)。第二,在库级用ALL PRIVILEGES意味着这个账号能在该库里建表、删表、改表结构,这对一个纯 CRUD 的业务账号来说是超配的——哪天代码里一句手滑的DROP TABLE就够喝一壶。
WITH GRANT OPTION这个尾巴要特别小心。它允许被授权者把权限再授出去,形成权限链。一旦某个业务账号带了它,你没法确定最终有多少个账号拥有这份权限,回收的时候也无从下手。除了 DBA 的管理账号,其他账号一律不加。
还有一个容易忽略的细节:GRANT是叠加的,不是覆盖。多次GRANT不同权限会累加;要减权限必须显式REVOKE。所以"重新授权一遍"这种操作不会重置权限,只是往上加。
3.2 四类典型业务账号的授权模板
把常见场景整理成模板,用的时候改库名和网段就行。
只读账号(报表、数据同步下游、排查问题用):
CREATE USER 'report_ro_prod'@'10.20.%' IDENTIFIED BY 'StrongPwd_Report_01'; GRANT SELECT, SHOW VIEW ON `biz_db`.* TO 'report_ro_prod'@'10.20.%';SHOW VIEW是给视图用的,如果只查表可以去掉。有人会问要不要给PROCESS看慢查询,我的建议是不给——排查慢查询应该用专门的监控账号,业务只读账号给PROCESS意味着它能看所有人的会话和执行中的 SQL,属于信息越权。
读写账号(常规业务服务):
CREATE USER 'order_rw_prod'@'10.20.%' IDENTIFIED BY 'StrongPwd_Order_01'; GRANT SELECT, INSERT, UPDATE, DELETE ON `order_db`.* TO 'order_rw_prod'@'10.20.%';如果业务用了INSERT ... ON DUPLICATE KEY UPDATE或者批量导入,INSERT和UPDATE都得给。如果用了SELECT ... FOR UPDATE,只需要SELECT加事务,不需要额外权限。
带 DDL 的迁移账号(发布窗口专用):
CREATE USER 'deploy_ddl_prod'@'10.20.%' IDENTIFIED BY 'StrongPwd_Deploy_01'; GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, ALTER, DROP, INDEX, REFERENCES, CREATE VIEW, SHOW VIEW, TRIGGER ON `order_db`.* TO 'deploy_ddl_prod'@'10.20.%';这个账号平时应该ACCOUNT LOCK掉,发布时ACCOUNT UNLOCK,发完再锁回去。多一步操作,但比长期挂着一个能删表的账号安全得多。
监控与备份账号(跨库,需要全局权限):
-- 监控账号 CREATE USER 'monitor_prod'@'10.20.%' IDENTIFIED BY 'StrongPwd_Mon_01'; GRANT PROCESS, REPLICATION CLIENT ON *.* TO 'monitor_prod'@'10.20.%'; GRANT SELECT ON `performance_schema`.* TO 'monitor_prod'@'10.20.%'; -- 逻辑备份账号 CREATE USER 'backup_prod'@'10.20.%' IDENTIFIED BY 'StrongPwd_Bak_01'; GRANT SELECT, LOCK TABLES, SHOW VIEW, TRIGGER, EVENT, PROCESS, RELOAD, REPLICATION CLIENT ON *.* TO 'backup_prod'@'10.20.%';备份账号的权限组合是有讲究的:LOCK TABLES用于一致性快照,RELOAD用于FLUSH TABLES,REPLICATION CLIENT用于读 binlog 位点,PROCESS用于看长事务避免备份卡住。少一个都可能在某个环节报错。
3.3 REVOKE 与角色(ROLE)的用法
权限回收的语法:
-- 收回特定权限 REVOKE INSERT, UPDATE ON `order_db`.* FROM 'order_rw_prod'@'10.20.%'; -- 收回该账号在该库的全部权限 REVOKE ALL PRIVILEGES ON `order_db`.* FROM 'order_rw_prod'@'10.20.%'; -- 收回转授权限 REVOKE GRANT OPTION ON `order_db`.* FROM 'order_rw_prod'@'10.20.%';要特别注意:REVOKE ALL PRIVILEGES不会删除账号,账号还在,密码还在,只是权限空了。真正删除账号是DROP USER 'x'@'y';。我遇到过有人以为REVOKE ALL就等于注销账号,结果账号一直活着,只是连不上任何库,审计扫描的时候被标了出来。
角色机制在账号多、权限组合重复的场景下非常省事:
CREATE ROLE 'role_order_read'; GRANT SELECT, SHOW VIEW ON `order_db`.* TO 'role_order_read'; CREATE ROLE 'role_order_write'; GRANT SELECT, INSERT, UPDATE, DELETE ON `order_db`.* TO 'role_order_write'; -- 授予用户 GRANT 'role_order_write' TO 'order_rw_prod'@'10.20.%'; -- 关键一步:设置默认角色,否则登录后不生效 SET DEFAULT ROLE ALL TO 'order_rw_prod'@'10.20.%';最后这一步是角色机制最容易翻车的地方。GRANT role TO user只是"给"了,不设置默认角色的话,用户登录后角色是未激活状态,权限查不到,表现就是"我明明授权了为什么还是 1142"。验证角色是否生效用SELECT CURRENT_ROLE();,查角色的实际权限用SHOW GRANTS FOR 'order_rw_prod'@'10.20.%' USING 'role_order_write';。
4. 完整实操:给一套业务系统配账号
4.1 前置检查与信息确认
动手之前,先把信息确认清楚,比事后返工强。我通常会确认这几项:应用服务器所在网段(ip addr或者问运维要)、业务实际用到的 SQL 类型(有没有 DDL、有没有临时表、有没有存储过程)、连接池配置(最大连接数、实例数量)、以及这个库上是否已经有同名的账号残留。
-- 1. 确认版本,语法差异全靠它 SELECT VERSION(); -- 2. 检查是否已有同名账号 SELECT user, host, plugin, account_locked FROM mysql.user WHERE user LIKE 'order_%'; -- 3. 确认库存在、字符集正确 SHOW CREATE DATABASE `order_db`; -- 4. 看一眼当前全局连接数上限 SHOW VARIABLES LIKE 'max_connections';第 2 步特别重要。如果之前有人用'order_rw_prod'@'%'建过账号,你又建了一个'order_rw_prod'@'10.20.%',那么同一个账号名会有两条记录,行为取决于匹配优先级——10.20.%比%更具体,会优先生效,但两条记录的密码可能不一样,非常容易混淆。这种情况我一般先把旧的%记录清掉,再建新号。
4.2 从建号到验证的完整命令序列
下面这套是完整流程,从管理员登录到验证收尾,可以直接抄。
# 管理员登录(走 socket,避免密码进 history) mysql -u root -p-- 第一步:建账号,明确插件、明确网段 CREATE USER 'order_rw_prod'@'10.20.%' IDENTIFIED WITH caching_sha2_password BY 'StrongPwd_Order_2024' PASSWORD EXPIRE INTERVAL 180 DAY FAILED_LOGIN_ATTEMPTS 5 PASSWORD_LOCK_TIME 1; -- 第二步:授业务权限,只给 CRUD GRANT SELECT, INSERT, UPDATE, DELETE ON `order_db`.* TO 'order_rw_prod'@'10.20.%'; -- 第三步:确认授权结果 SHOW GRANTS FOR 'order_rw_prod'@'10.20.%';SHOW GRANTS的输出应该是这样的(多出来的一行是系统自动附带的 USAGE,表示"账号存在但该库上无额外权限",属于正常现象):
+-------------------------------------------------------------+ | Grants for order_rw_prod@10.20.% | +-------------------------------------------------------------+ | GRANT USAGE ON *.* TO `order_rw_prod`@`10.20.%` | | GRANT SELECT, INSERT, UPDATE, DELETE ON `order_db`.* TO ... | +-------------------------------------------------------------+第四步是真正重要的验证环节:用新账号实际连一次,并且故意越界试一下。
mysql -h 10.20.1.10 -P 3306 -u order_rw_prod -p-- 正例:应该成功 SELECT COUNT(*) FROM `order_db`.`orders`; INSERT INTO `order_db`.`orders` (id, amount) VALUES (1, 100); -- 边界验证:应该被拒绝,如果成功了说明权限给多了 SELECT COUNT(*) FROM `other_db`.`some_table`; -- ERROR 1142 预期 DROP TABLE `order_db`.`orders`; -- ERROR 1142 预期 CREATE TABLE `order_db`.`tmp_test` (id INT); -- ERROR 1142 预期提示:验证时务必新建连接,不要复用已有的会话。权限变更后虽然服务器端会按新的 ACL 校验,但连接池、prepared statement 缓存这些中间层可能还持有旧状态,用新连接验证才是准的。
4.3 连接数这类参数怎么估
建号时如果打算加资源限制,MAX_USER_CONNECTIONS这个值需要算一下。假设应用部署了 8 个实例,每个实例连接池maxPoolSize = 20,那么理论峰值是 8 × 20 = 160 个连接。考虑到滚动发布期间新旧实例并存(会有 16 个实例的瞬时窗口),实际峰值可能到 320。
结论是限制值不能卡着 160 设,否则发布期间必然有实例连不上。我一般按实例数 × 池大小 × 1.5估,也就是 240,再留点余量取整到 300:
ALTER USER 'order_rw_prod'@'10.20.%' WITH MAX_USER_CONNECTIONS 300 MAX_CONNECTIONS_PER_HOUR 5000 MAX_QUERIES_PER_HOUR 200000;还要跟全局max_connections对得上。如果全局是 500,业务账号配额 300,剩下的 200 要留给监控、备份、管理员和别的账号。这两个数不协调的结果,要么是账号配额形同虚设,要么是全局先被打满,报ERROR 1040: Too many connections,那时候所有账号都连不进来,包括你自己的管理员连接。
5. 报错排查实录
5.1 六类高频报错与定位路径
1045 Access denied ... (using password: YES/NO)。YES说明密码传了但不对,或账号不匹配;NO说明根本没传密码。先确认账号名和 host 是否完全匹配——'app'@'10.20.%'用'app'@'10.21.5.7'的机器连就是匹配不上。再看是不是密码里带特殊字符被 shell 吃掉了,比如$、!、&在命令行里要单引号包裹。
1130 Host 'x' is not allowed to connect。这个报错很明确:账号存在,但没有任何一条记录的 host 覆盖这个来源 IP。要么改账号 host,要么新增一条。别图省事直接改成%,先想清楚为什么这个 IP 没被覆盖——是机器新扩的,还是本来就不该连。
1044 Access denied to database 'x'。账号认证过了,但目标库没有权限。典型场景是只给了order_db的权限,程序连上之后先执行了USE other_db。检查SHOW GRANTS里有没有对应库的行。
1142 command denied。同一类问题,粒度更细——库权限有了,表权限或者操作权限不够。比如给了SELECT但程序要做INSERT。
1698 Access denied for user 'root'@'localhost'。这是 Ubuntu/Debian 系的经典坑,root 用了auth_socket插件,只能用系统 root 身份通过 socket 登录。要么sudo mysql进去改插件,要么新建一个管理账号。
1820 You must reset your password。账号被标记了password_expired,首次登录或人工设置过期后必须改密码才能做其他操作。连上去执行ALTER USER USER() IDENTIFIED BY '新密码';即可。
5.2 授权改了不生效的几个原因
这算是我被问得最多的一类问题。现象是"权限明明加了,代码还是报 1142"。按概率从高到低排:
第一,改的是另一个 host 的账号。你GRANT ... TO 'app'@'%',但实际连接匹配到的是'app'@'10.20.%',那条没加权限。先SELECT user, host FROM mysql.user WHERE user='app';看清楚有几条。
第二,角色没设默认值。前面说过,GRANT role TO user之后必须SET DEFAULT ROLE,否则角色处于未激活状态。
第三,应用连接池没重连。老的连接还在用旧的会话上下文,尤其是用了 prepared statement 缓存的情况。重启应用或者等连接自然过期。
第四,改错了实例。主从架构下,读请求打到从库,你在主库上授权的操作确实同步过去了,但如果读写分离中间件缓存了路由信息,也可能出岔子。这个概率低但确实遇到过。
第五,库名通配符的锅。前面提到的biz_db里下划线被当通配符,导致实际授权范围和你以为的不一样。用SHOW GRANTS反查最直接。
5.3 常见问题速查表
| 现象/报错 | 最可能原因 | 快速确认方法 | 处理方式 |
|---|---|---|---|
| 1045 using password: YES | 密码错 / host 不匹配 | SHOW GRANTS FOR 'u'@'h' | 核对账号 host,重置密码 |
| 1045 using password: NO | 客户端没传密码 | 检查连接串 | 补上密码参数 |
| 1130 Host not allowed | host 未覆盖来源 IP | SELECT host FROM mysql.user WHERE user='u' | 新增对应网段账号 |
| 1044 to database | 库级无权限 | SHOW GRANTS | 补GRANT ... ON db.* |
| 1142 command denied | 操作权限不足 | SHOW GRANTS | 补具体权限 |
| 授权后仍 1142 | 角色未激活 / 连接池未重连 | SELECT CURRENT_ROLE(); | SET DEFAULT ROLE后重连 |
| 插件加载失败 | 客户端不支持 caching_sha2 | 看驱动版本 | 升驱动,或临时用 native 插件账号 |
| 1698 root 连不上 | auth_socket 插件 | SELECT plugin FROM mysql.user WHERE user='root' | sudo mysql进去改插件 |
| 1820 必须改密码 | 密码已过期 | SELECT password_expired FROM mysql.user | ALTER USER重置 |
| DROP USER 后仍能连 | 已建立会话不会被踢 | SHOW PROCESSLIST | 手动KILL对应连接 |
最后一行值得单独说一下。DROP USER的语义是删除账号记录,不会主动断开已经建立的连接。所以你在紧急情况下想通过删号来掐断某个应用的访问,删完之后它照样能跑,直到连接自己断开。正确做法是先SELECT id, user, host, db FROM information_schema.processlist WHERE user='要删的账号';拿到会话 ID,逐个KILL,再DROP USER。
6. 账号治理:生命周期与留痕
6.1 账号从申请到回收的闭环
账号管理真正难的不是建,是收。我统计过一套跑了三年的系统,mysql.user里有 40 多个账号,其中超过一半对应的是已经下线或重构过的服务。这些僵尸账号挂着权限没人管,是审计里最容易被挑出来的问题。
我的做法是给每个账号记三样东西:归属系统、责任人、创建时间。前两个靠命名规范部分承载,第三个直接看mysql.user的时间字段做不到(那张表没有创建时间列),所以我在授权完成后往一张自建的dba_account_registry表里插一条记录:
CREATE TABLE IF NOT EXISTS `dba_meta`.`account_registry` ( id INT AUTO_INCREMENT PRIMARY KEY, db_user VARCHAR(64) NOT NULL, db_host VARCHAR(64) NOT NULL, owner_system VARCHAR(64) NOT NULL, owner_person VARCHAR(64) NOT NULL, purpose VARCHAR(255), created_at DATETIME DEFAULT CURRENT_TIMESTAMP, expire_at DATETIME, UNIQUE KEY uk_user_host (db_user, db_host) );有了这张表,季度巡检的时候一条 SQL 就能捞出所有快到期的账号:
SELECT db_user, db_host, owner_system, owner_person, expire_at FROM `dba_meta`.`account_registry` WHERE expire_at < DATE_ADD(NOW(), INTERVAL 30 DAY) ORDER BY expire_at;回收时的顺序也有讲究:先KILL会话,再REVOKE权限,再DROP USER,最后更新登记表。少了KILL这一步,账号删了但连接还在跑,你以为什么都没了,其实业务还在往里写数据。
6.2 权限变更记录与回归脚本
生产环境的权限变更我坚持两件事:变更前导出当前状态,变更后跑一遍回归。
导出当前状态很简单:
mysql -u root -p -N -e " SELECT CONCAT('SHOW GRANTS FOR ''', user, '''@''', host, ''';') FROM mysql.user WHERE user NOT IN ('mysql.sys','mysql.session','mysql.infoschema'); " > /tmp/grant_dump.sh # 再逐条执行,收集所有授权语句 mysql -u root -p -N < /tmp/grant_dump.sh > /tmp/grants_before_$(date +%F).sql这个快照在出问题时能救命——知道原来是什么样,才知道往哪回。
回归脚本我一般写成一组"应该成功"和"应该失败"的断言,用新账号跑一遍:
-- 应该成功:只读账号能查业务表 SELECT 1 FROM `order_db`.`orders` LIMIT 1; -- 应该失败:只读账号不能写 -- 预期 ERROR 1142 INSERT INTO `order_db`.`orders` (id) VALUES (999);这块用程序包一层更好,把每条断言的预期结果写死,跑完输出通过/失败。账号多的时候,这套脚本能在权限调整后的五分钟内告诉你有没有把某个服务的权限改坏,比等业务报警快得多。
我个人在这套流程上的体会是:建账号那两条 SQL 花不了三十秒,剩下九成的时间都在确认网段、算配额、做验证和留记录上。这些活儿看着琐碎,但每一次省略都会在某天半夜变成一个电话。我现在的习惯是,任何一次权限变更都先问自己三个问题——这个账号属于谁、它真的需要这条权限吗、出了问题我能在一分钟内回滚吗。三个都能答上来,再动手。