
先讲个我前阵子遇到的场景。公司测试库里所有开发都共用一个账号密码写在企微群的置顶公告里。有天下午某个同事跑了一条不带WHERE的UPDATE一张十几万行的订单表全被改成同一个状态。事后复盘的时候没人能说清是谁执行的因为连的是同一个账号审计日志里只能看到一个公共用户名。从那之后我养成了一个习惯只要经手的数据库用户权限必须拆清楚。MySQL中查看用户、创建用户、删除用户、授权用户、回收授权这一整套操作是每个后端开发、运维和初级DBA都应该刻进肌肉记忆的基本功。它们看起来简单但坑几乎全藏在细节里——host匹配、认证插件、8.0的语法变化、权限生效时机哪一步理解不到位都可能在生产环境引起事故。这篇文章就从底层存储结构开始把这套操作完整过一遍。全文以MySQL 8.0为主5.7及更早版本有差异的地方我会单独标注。不管你是刚入门写SQL的新手还是准备数据库面试的求职者照着这篇文章的操作走一遍至少能避开我踩过的那些坑。1. 先说说为什么“用户管理”值得单独拎出来写一篇1.1 一个真实事故背后的核心问题开头那个测试库的事故其实只是个缩影。更常见的情况是自己电脑上的MySQL用root用惯了上了生产环境也习惯性地用root连接业务库或者一个应用给一个“万能权限”账号SELECT、INSERT、UPDATE、DELETE、DROP全都带上一旦SQL注入或者人为误操作整个库连骨头都不剩。用户管理的本质是把“谁”和“能做什么”这两件事拆清楚。“谁”解决的是身份边界——mysql.user表里的一条记录代表一个账号由用户名和来源主机共同确定“能做什么”解决的是权限边界——你是只读、读写还是DDL变更必须按最小权限原则分配。1.2 这套操作覆盖的完整链路我梳理了一下MySQL用户管理完整覆盖以下操作查看用户列表、查看用户权限创建新用户、指定密码和认证方式对用户授权、修改权限层级回收授权、删除用户排查登录失败、权限不生效等常见问题这里面每一个点都有对应的SQL语法也有对应的底层表结构。光记住语法不够还得知道每一条语法背后改了哪张表、影响了什么场景。比如GRANT在8.0之前可以直接创建用户在8.0之后就变得严格——必须先CREATE USER再GRANT这个变化很多人根本不知道。1.3 版本约定与阅读建议本文默认MySQL 8.0命令示例在5.7上大部分也能跑但8.0特有的语法比如caching_sha2_password、角色功能我会单独标注。建议你安装一个干净的MySQL 8.0实例开一个测试库跟着文章把每个命令都执行一遍。用户管理操作不像业务SQL平时不用就容易忘动手敲一遍比看十遍都强。2. 用户到底存在哪mysql.user表与权限模型的底子2.1 身份识别的关键user和host不是一回事很多新手搞不懂一个问题为什么MySQL创建用户非要写成userhost这种格式直接在后面跟个用户名不行吗原因在于MySQL判断“用户是谁”用两个字段user用户名和host来源主机。一条记录唯一确定一个连接身份。举个例子下面三个其实是完全不同的账号账号含义rootlocalhost只能从本机连接用的是socket或127.0.0.1root%可以从任意主机连接root192.168.1.%只能从192.168.1这个网段连接在实际排查问题的时候最常见的场景就是明明创建了用户客户端连不上报Access denied for user app_userx.x.x.x。一看mysql.user表原来只创建了app_userlocalhost客户端从远程连接当然匹配不上。2.2 host的匹配规则越具体越优先MySQL在连接阶段匹配账号时并不是按“找到第一条就结束”的规则而是按照host的精确程度来排优先级。具体来说精确IP如192.168.1.10优先于网段如192.168.1.%网段优先于%localhost在处理本地连接时有特殊地位举个例子。如果mysql.user表里同时存在app192.168.1.10和app%那么客户端从192.168.1.10发起连接时MySQL会选用app192.168.1.10这个账号并且只看这个账号的权限。哪怕app%是超级管理员权限app192.168.1.10只有SELECT那这个客户端登录后也只能SELECT。这个机制容易让人困惑但理解了它很多“为什么授权不生效”的问题就迎刃而解了。2.3 权限分层的概念全局、库、表、列MySQL的权限模型是分层的一共五张权限表权限表控制粒度授权写法mysql.user全局权限ON *.*mysql.db数据库级权限ON db.*mysql.tables_priv表级权限ON db.tblmysql.columns_priv列级权限ON db.tbl (col)mysql.procs_priv存储过程/函数权限ON PROCEDURE/FUNCTION db.proc权限校验时MySQL按“全局权限 → 库权限 → 表权限 → 列权限”的顺序逐层判断只要有一层允许操作就能执行。用大白话说全局权限是“大赦天下”只要mysql.user表里有SELECT权限后面三层都不用看所有库的表都能查。我见过一个开发同学的授权是GRANT ALL ON *.*问他为什么他说“这样省事后面不会莫名其妙报权限不足”。短期的确是省事但一旦账号被盗用攻击者能做的操作范围就是全库全表——这就是典型的把安全风险转嫁给了未来的自己。2.4 8.0新增的ROLE权限的“模板化”MySQL从8.0开始支持角色ROLE可以简单理解为一组权限的集合。比如建一个只读角色把SELECT权限扔进去再把角色赋予多个用户比给每个用户单独GRANT一遍要省事得多。-- 创建角色并授权 CREATE ROLE read_only_role; GRANT SELECT ON business_db.* TO read_only_role; -- 把角色赋予用户 GRANT read_only_role TO report_user%; -- 设置默认激活角色否则登录后角色不生效 SET DEFAULT ROLE ALL TO report_user%;这个功能在账号数量多了之后特别好用。后面第5章我会再详细展开角色怎么和GRANT配合。3. 查看用户不是只有SELECT user FROM mysql.user这一种姿势3.1 查看所有账号及关键字段最基本的查看用户列表语句SELECT user, host, plugin, account_locked, password_expired FROM mysql.user;输出大概长这样不同版本字段略有差异-------------------------------------------------------------------------------------- | user | host | plugin | account_locked | password_expired | -------------------------------------------------------------------------------------- | mysql.infoschema | localhost | caching_sha2_password | Y | N | | mysql.session | localhost | caching_sha2_password | Y | N | | mysql.sys | localhost | caching_sha2_password | Y | N | | root | localhost | caching_sha2_password | N | N | | app_user | % | caching_sha2_password | N | N | --------------------------------------------------------------------------------------这里mysql.infoschema、mysql.session、mysql.sys是系统自带的内部账号正常情况下是锁定的account_locked为Y不需要去动它们。你真正关注的是root和自己创建的账号。plugin字段很重要它表示这个账号的认证插件。8.0默认是caching_sha2_password如果某个账号还是老旧的mysql_native_password说明可能是从5.7升级过来的需要关注客户端兼容性。这个后面创建用户章节会细说。3.2 检查哪些账号允许远程登录想知道版图里有多少“裸奔”的账号一条SQL就能筛出来SELECT user, host FROM mysql.user WHERE host NOT IN (localhost, 127.0.0.1, ::1);如果结果里出现一个host%且密码很弱的账号那基本等于把数据库端口暴露在公网上的话谁连上都能猜密码。这种账号应及时收紧host范围至少改成业务服务器的IP段。3.3 查看某个用户的具体权限查看用户列表只是第一步“这个用户到底能干什么”才是更重要的。-- 查看指定用户权限 SHOW GRANTS FOR app_user%; -- 查看当前登录用户的权限 SHOW GRANTS FOR CURRENT_USER();输出是GRANT语句的形式直接告诉你这个账号被授权了什么。以SHOW GRANTS FOR app_user%;为例输出可能是------------------------------------------------------ | Grants for app_user% | ------------------------------------------------------ | GRANT USAGE ON *.* TO app_user% | | GRANT SELECT, INSERT, UPDATE ON business_db.* TO app_user% | ------------------------------------------------------第一行GRANT USAGE ON *.*表示这个账号“能连接数据库但没有任何实际权限”。这是刚创建用户、还没授权时的默认状态。也就是说你创建了一个用户如果不给他授权他登录后什么都做不了。3.4 利用information_schema视图查看更多角度除了SHOW GRANTSinformation_schema库里有几张视图也能查权限-- 全局权限 SELECT * FROM information_schema.USER_PRIVILEGES WHERE GRANTEE app_user%; -- 库级权限 SELECT * FROM information_schema.SCHEMA_PRIVILEGES WHERE GRANTEE app_user%; -- 表级权限 SELECT * FROM information_schema.TABLE_PRIVILEGES WHERE GRANTEE app_user%;这些视图适合在脚本里做权限巡检——比如定期扫一遍哪些账号在mysql.db表里有DELETE权限哪些账号在mysql.tables_priv表里有DROP权限输出一个报表给负责人确认。3.5 顺带一提找僵尸账号的小技巧热搜词里有“general.log”相关的内容确实通用日志可以用来观察哪些用户在实际执行SQL。不过在生产环境长期开着general_log会很占磁盘性能影响也大属于排查问题时临时打开的手段。另外performance_schema.accounts表会记录哪些账号曾经建立过连接SELECT USER, HOST, CURRENT_CONNECTIONS, TOTAL_CONNECTIONS FROM performance_schema.accounts;如果一个账号在performance_schema里完全查不到连接记录只能说明它“至少从MySQL启动以来”没有连过。结合授权记录和业务变更时间就能判断哪些是僵尸账号可以进入删除流程。4. 创建用户语法很简单坑全在host和认证插件上4.1 最基本的CREATE USER命令创建用户的完整语法非常直白CREATE USER app_userlocalhost IDENTIFIED BY StrongPassw0rd;执行成功后mysql.user表里多了一条记录。这条命令做了两件事创建账号并设置密码。注意并没有给任何权限此时这个用户只能连接数据库不能执行任何查询或写入操作。MySQL 8.0还支持一次性创建多个用户CREATE USER app_userlocalhost IDENTIFIED BY pass1, report_user192.168.1.% IDENTIFIED BY pass2;如果担心重复创建导致报错可以加IF NOT EXISTSCREATE USER IF NOT EXISTS app_userlocalhost IDENTIFIED BY StrongPassw0rd;4.2 密码策略会拦你ERROR 1819如果你设置的密码太简单MySQL会直接拒绝创建ERROR 1819 (HY000): Your password does not satisfy the current policy requirements这是因为默认安装了validate_password组件。查看具体策略SHOW VARIABLES LIKE validate_password%;8.0里的变量名带点5.7的变量名是下划线比如validate_password_policy。常见的几个变量变量含义validate_password.policy密码策略等级LOW / MEDIUM / STRONGvalidate_password.length密码最短长度validate_password.mixed_case_count大写和小写字母至少各多少个validate_password.number_count数字至少多少个validate_password.special_char_count特殊字符至少多少个如果只是在本地测试环境不想被密码策略卡可以临时调低策略SET GLOBAL validate_password.policy LOW; SET GLOBAL validate_password.length 6;但要注意SET GLOBAL只对当前运行实例有效重启MySQL后失效。要永久生效得写到配置文件里。生产环境不建议降低策略密码强度就是数据库的第一道门。4.3 认证插件8.0默认值和老客户端的兼容问题MySQL 8.0默认的认证插件是caching_sha2_password安全性比5.7的mysql_native_password强不少。但问题在于老版本的客户端驱动比如5.x的JDBC驱动、老版本的Navicat不认识这个新插件连接时会报Authentication plugin caching_sha2_password cannot be loaded碰到这种情况有两种解法第一种升级客户端驱动。这是长期正确的方向因为MySQL 8.4之后mysql_native_password插件默认被禁用还在用老插件的账号升级时会很被动。第二种短期内兼容老客户端——创建用户时显式指定认证插件CREATE USER old_client_user% IDENTIFIED WITH mysql_native_password BY StrongPassw0rd;或者对已存在的用户修改认证插件ALTER USER old_client_user% IDENTIFIED WITH mysql_native_password BY StrongPassw0rd;我的建议是新做的系统一律走新驱动不要为了省事把账号降级到老插件。老驱动后面还会遇到其他兼容性问题比如时区、字符集早升早安心。4.4 host别乱写%和localhost是两个世界关于host我在第2章讲过匹配规则这里再强调一个实际场景。如果你创建的是app_user%而应用服务器连接数据库时用的连接串是jdbc:mysql://localhost:3306/xxxMySQL内部解析时可能走socket或回环地址有时候会匹配到app_userlocalhost而不是app_user%。如果后者不存在就直接登录失败。所以稳妥的做法是应用走远程连接就明确用内网IP或主机名不要用localhost本地运维操作再单独建一个localhost账号。如果你只是自己本机开发用app_userlocalhost就够了别上来就%。4.5 创建用户时顺便设置的账号控制项CREATE USER语句还能带几个实用选项CREATE USER app_user% IDENTIFIED BY StrongPassw0rd PASSWORD EXPIRE INTERVAL 90 DAY ACCOUNT LOCK;PASSWORD EXPIRE INTERVAL 90 DAY密码90天后过期到期需要修改密码后才能继续操作。ACCOUNT LOCK创建后立即锁定谁也不能登录。这种操作一般用于准备一个账号但暂不启用或者作为后续解锁的“先占位”。对应地修改密码、锁定、解锁用ALTER USER-- 修改密码 ALTER USER app_user% IDENTIFIED BY NewStrongPassw0rd; -- 锁定账号 ALTER USER app_user% ACCOUNT LOCK; -- 解锁账号 ALTER USER app_user% ACCOUNT UNLOCK; -- 要求用户下次登录必须改密码 ALTER USER app_user% PASSWORD EXPIRE;4.6 重命名用户严格来说这算是修改用户而不是创建但很多朋友不知道有RENAME USER这个命令这里一并提了RENAME USER old_user% TO new_user%;它会保留原有的授权信息只是改了账号名或host。比“重新建一个用户再重新授权”要省事得多。5. 授权GRANT的权限粒度、生效时机与8.0的语法变化5.1 GRANT的基础语法创建用户之后下一步是给用户分配权限。GRANT命令长这样GRANT SELECT, INSERT, UPDATE, DELETE ON business_db.* TO app_userlocalhost;拆开看就是权限列表SELECT, INSERT, UPDATE, DELETE权限范围ON business_db.*表示business_db库下的所有表目标用户TO app_userlocalhost如果是多个库可以多次授权。如果要把某个库的所有权限都给用户用ALL PRIVILEGESGRANT ALL PRIVILEGES ON business_db.* TO dev_user%;这里要小心ALL PRIVILEGES包含DROP、ALTER、CREATE等DDL权限一般只给开发或者管理员账号不要无脑给业务应用账号。5.2 常用权限清单权限作用SELECT查询数据INSERT插入数据UPDATE更新数据DELETE删除数据CREATE创建数据库、表、索引ALTER修改表结构DROP删除数据库、表、视图等INDEX创建、删除索引REFERENCES外键约束的引用权限TRIGGER创建、删除触发器CREATE VIEW创建视图SHOW VIEW查看视图定义EXECUTE执行存储过程、函数EVENT创建、修改、删除事件LOCK TABLES锁表权限GRANT OPTION允许把自己拥有的权限再授予别人最小权限原则说起来简单实操时我的建议是先用只读权限跑一段时间确实不够再往上加。比如一个新应用上线先给SELECT跑一段时间没有异常再补INSERT、UPDATE、DELETE。粒度一步步放开出问题的概率会小很多。5.3 ON后面的层级怎么写授权范围有几种写法对应不同的权限层级写法控制范围ON *.*所有库的所有表全局权限ON business_db.*business_db库下的所有表ON business_db.ordersbusiness_db库的orders表ON business_db.orders (id, amount)business_db库的orders表的指定列举个例子只允许通过报表系统查询订单表的订单号和金额列级授权可以这样写GRANT SELECT (id, order_no, amount, status) ON business_db.orders TO report_user%;列级权限在真实业务中很实用但很多人不知道。比如一个查询用户信息的接口只需要user表的name、email字段你没必要让它能SELECT整张user表尤其是那些包含手机号、身份证号这些敏感信息的表。5.4 8.0之后GRANT不能再创建用户这是我最想强调的版本差异。在MySQL 5.7及更早版本里GRANT语句可以直接创建一个用户并同时授权-- 5.7写法 GRANT SELECT ON *.* TO new_user% IDENTIFIED BY password;在MySQL 8.0里执行同样语句直接报错ERROR 1410 (42000): You are not allowed to create a user with GRANT8.0强制要求你分两步走CREATE USER new_user% IDENTIFIED BY password; GRANT SELECT ON *.* TO new_user%;这是为了安全考虑——不再允许在一条GRANT里隐式创建用户避免误操作把一个弱密码账号暴露出去。如果你在网上搜到老教程里那种“GRANT ... IDENTIFIED BY”的写法注意先确认对方写的MySQL版本。5.5 WITH GRANT OPTION把授权能力也交出去GRANT可以附带WITH GRANT OPTION意思是这个用户不仅拥有列出的权限还能把这些权限再授予别人。GRANT SELECT ON business_db.* TO team_leader% WITH GRANT OPTION;这个选项很危险。它的实际效果是team_leader可以登录后执行GRANT SELECT ON business_db.* TO someone_else%自己往下分发权限。如果这个账号的密码泄露攻击者就能永久给自己留后门账号。除非确有必要比如你希望某个负责人能独立管理自己团队成员的账号否则不要随便加WITH GRANT OPTION。5.6 授权后要不要FLUSH PRIVILEGES这是一个经典问题。直接说结论通过CREATE USER、GRANT、REVOKE、DROP USER这些账号管理命令做的操作会同步更新内存中的权限缓存不需要执行FLUSH PRIVILEGES。如果你手贱直接对mysql.user表执行了INSERT、UPDATE、DELETE这时需要FLUSH PRIVILEGES让权限缓存重新加载。所以日常管理用户直接用官方语法就行不用管FLUSH。FLUSH PRIVILEGES真正的作用场景是“绕过官方语法直接改表”这种紧急修复操作。另外要注意权限生效时机。全局权限比如SUPER、PROCESS在已建立的连接里不会立即生效需要重连才会重新加载库级、表级权限在下次执行语句前会动态刷新。所以测试授权是否生效时最稳妥的方式是断开重连。5.7 用角色简化重复授权回到第2章提到的角色。假如你有10个业务账号都需要对business_db库只读逐个GRANT当然可以但后续要收回权限时会很痛苦——要更10条REVOKE。更优雅的姿势是角色-- 创建角色 CREATE ROLE business_readonly; -- 给角色授权 GRANT SELECT ON business_db.* TO business_readonly; -- 把角色授予多个用户 GRANT business_readonly TO app_user%; GRANT business_readonly TO report_user%; GRANT business_readonly TO audit_user%;这样收回权限只需要改角色REVOKE SELECT ON business_db.* FROM business_readonly;所有拥有该角色的用户立即失去business_db库的查询权限。这个思路和Linux里的用户组非常像把“账号”和“权限”解耦管理效率提升一个档次。注意默认情况下新授予的角色不会自动激活需要给用户设置默认角色SET DEFAULT ROLE ALL TO app_user%;6. 回收授权和删除用户收权限比给权限更需要小心6.1 REVOKE的基本写法回收授权是GRANT的逆操作-- 回收某个用户在business_db库的INSERT、UPDATE、DELETE权限 REVOKE INSERT, UPDATE, DELETE ON business_db.* FROM app_userlocalhost; -- 回收所有权限同时回收GRANT OPTION REVOKE ALL PRIVILEGES, GRANT OPTION FROM app_userlocalhost;REVOKE不会删除账号用户还能登录但能执行的语句会变少。这条命令在日常工作中非常常用——比如某员工转岗不再需要写权限先把写权限收掉账号保留几天观察确认没有程序依赖后再删。回收角色也是一样的语法REVOKE business_readonly FROM app_user%;6.2 DROP USER删除账号如果确定账号彻底不用了执行DROP USER app_userlocalhost;一次可以删多个DROP USER app_userlocalhost, old_user%;DROP USER会同时删除该用户在mysql.user、mysql.db、mysql.tables_priv等所有权限表中的记录相当于“连人带权限一起注销”。这是最干净的删除方式。注意MySQL 8.0的DROP USER不支持IF EXISTS。如果用户不存在直接报错ERROR 1396 (HY000): Operation DROP USER failed for xxx%MariaDB支持IF EXISTSMySQL不支持别记混了。6.3 删除前必须做的检查我见过不止一次“删错账号”导致线上应用连接池爆满的事故。所以删除用户之前务必先看清楚这个用户正在被谁用-- 1. 看账号的权限全貌 SHOW GRANTS FOR app_userlocalhost; -- 2. 看这个账号出现在哪些库级授权里 SELECT * FROM mysql.db WHERE user app_user; -- 3. 看这个账号出现在哪些表级授权里 SELECT * FROM mysql.tables_priv WHERE user app_user; -- 4. 看当前是否有活动连接 SELECT * FROM information_schema.processlist WHERE user app_user;如果第4步查出来有活动连接最好不要直接DROP先REVOKE所有权限锁掉账号ACCOUNT LOCK再和应用负责人确认后处理。删除账号是不可逆操作宁可多一步确认也不要图快。6.4 锁定账号有时候比删除更合适如果你不确定账号是否还有隐藏的定时任务在连接建议先锁再删ALTER USER app_userlocalhost ACCOUNT LOCK;锁账号后新的连接会被拒绝已经建立的连接继续存在不会被动踢掉。观察几天没有收到任何连接异常告警再执行DROP USER风险就小很多。我在团队里定的规矩是任何账号下线先锁24小时确认无告警后删除。这条规矩帮我挡住过好几次“明明测试过没程序用了结果还有人肉定时任务在连”的尴尬情况。6.5 用RENAME USER代替“删了重建”如果某个账号的host要改或者用户名要和命名规范对齐不要删了重建——直接用RENAME USERRENAME USER app_user% TO app_user192.168.1.%;改名后原账号的所有授权会自动迁移到新账号上。如果删了重建还得重新GRANT一遍很可能漏掉某个库的授权。7. 实操中很容易翻车的几个排查场景7.1 登录报Access denied的完整排查链路这个是最常见的错误之一。错误信息长这样ERROR 1045 (28000): Access denied for user app_user192.168.1.25 (using password: YES)看到这个报错按下面顺序排查第一步确认用户是否存在。SELECT user, host FROM mysql.user WHERE user app_user;如果压根没有这条记录问题变成了“这个用户为什么没建成功”——可能是当初CREATE USER执行时因为密码策略报错没注意也可能建到了另外一台实例上。第二步确认host是否匹配。报错信息里已经有了发起连接的IP192.168.1.25。检查mysql.user表里有没有能匹配这个IP的hostapp_user192.168.1.25精确匹配app_user192.168.1.%网段匹配app_user%全匹配如果只有app_userlocalhost那远程连不上是必然的。第三步确认密码是否正确。用这条命令无密码试一下重点区分是密码错还是账号错mysql -uapp_user -h192.168.1.100 -p如果提示Access denied using password: NO说明密码问题如果using password: YES大概率是密码不对。第四步检查账号状态。SELECT user, host, account_locked, password_expired FROM mysql.user WHERE user app_user;账号被锁account_lockedY或密码过期password_expiredY都会导致登录失败。很多连不上的问题最后都栽在host或账号状态上而不是密码本身。排查时别一门心思猜密码。7.2 初始密码去哪找热搜词里有“mysql的初始密码是什么”这里统一回答。如果你在Linux上用mysqld --initialize初始化数据目录MySQL会为root生成一个临时密码写在错误日志里。错误日志一般在数据目录下文件名类似/var/lib/mysql/hostname.err进去找关键词temporary password[Note] A temporary password is generated for rootlocalhost: xxxxxx用这个临时密码登录后MySQL会强制要求先修改密码ALTER USER rootlocalhost IDENTIFIED BY NewStrongPassw0rd;Windows上如果用mysqld --initialize-insecureroot可能初始密码为空。不管哪种情况第一次登录后第一件事都是修改root密码。7.3 root密码忘光的应急处理说一个不太光彩但很实用的场景root密码忘了怎么办应急方案是跳过权限表启动# 先停掉MySQL服务 systemctl stop mysqld # 以跳过授权表的方式启动 mysqld_safe --skip-grant-tables # 登录 mysql -uroot登录后先刷新权限再修改密码FLUSH PRIVILEGES; ALTER USER rootlocalhost IDENTIFIED BY NewStrongPassw0rd;然后重启MySQL恢复正常权限校验模式。需要特别强调--skip-grant-tables模式等于完全关闭了权限校验任何人都能无密码登录数据库。这种模式只能在内网维护窗口临时使用操作过程中不要让数据库暴露在外部网络改完密码立刻重启一分钟都不要多留。这也是为什么生产环境绝对不能用默认端口裸奔的原因。7.4 明明授权了客户端还是提示没有权限这类问题的排查思路比较固定。先确认“客户端连的到底是哪个账号”-- 登录后执行 SELECT CURRENT_USER();这一步很关键。因为客户端连接串里写的账号只是“意图”MySQL实际匹配到的账号可能完全不同。比如你授权给了app%但客户端用了-h 127.0.0.1连接实际匹配到的可能是applocalhost而后者你没授权执行SELECT时就报权限不足。确认账号无误后再检查权限层级是不是授了库级权限但实际语句访问的是另一个库或者授了表级SELECT但程序还执行了UPDATE。还有一个容易忽略的场景连接池。应用连接池里可能持有的是授权前的旧连接授权后并没有重新建立连接所以看起来“怎么还不生效”。清一下连接池或者重启应用问题就消失了。7.5 创建用户时提示密码不满足策略这个前面第4章提过这里补充一个实际场景。你明明设了一个很复杂的密码还是报ERROR 1819那大概率是策略要求更高。SHOW VARIABLES LIKE validate_password%;重点看LENGTH和policy。测试环境可以临时调低生产环境务必使用满足策略的高强度密码。7.6 科普一点MySQL 8.4之后mysql_native_password更不好用了MySQL 8.4开始mysql_native_password插件默认被禁用。也就是说你不能再创建IDENTIFIED WITH mysql_native_password的账号除非在配置里显式开启。这意味着依赖老插件的应用会越来越难跑通连接升级客户端驱动才是正道。如果你手里还有一批用mysql_native_password创建的账号建议尽早规划升级驱动、修改账号认证插件、回归测试一步一步来别拖到数据库大版本升级时一起爆雷。8. 上手就能用的账号权限规范建议8.1 一套可落地的账号命名与权限分层踩过这么多坑我把自己总结的规范整理成了一张表新项目直接照抄即可账号类型命名规范权限范围授权示例应用只读账号app_业务_ro只读业务库GRANT SELECT ON db.* TO ...应用读写账号app_业务_rw业务库DMLGRANT SELECT,INSERT,UPDATE,DELETE ON db.* TO ...DDL变更账号dev_姓名_admin业务库DDLGRANT CREATE,ALTER,DROP,INDEX ON db.* TO ...备份账号bak_db_user全局SELECT/LOCK TABLESGRANT SELECT,LOCK TABLES ON *.* TO ...报表账号rpt_业务_ro只读指定库或指定表GRANT SELECT ON db.table TO ...账号命名里包含了业务、用途、权限等级任何人看到账号名立刻知道它是干什么的权限该有多大。8.2 生产环境下的几条红线结合实操经历下面几条是我视为“红线”的规定禁止使用root账号连接业务库。业务连接账号必须是最小权限连root碰都不能碰。禁止GRANT ALL ON *.*给业务账号。全局ALL等同于把整个实例的管理权交出去了。禁止跨环境复用账号和密码。测试库和生产库的账号必须分开密码不能一样。禁止长期不轮换密码。至少每90天通过PASSWORD EXPIRE INTERVAL 90 DAY让密码自动过期。删除账号前必须锁定观察24小时。这条规则帮团队规避过两次误删事故。8.3 最后再分享一点个人经验我一般给开发同学开通数据库权限时一定会附带发一条变更记录申请人、申请时间、授权的环境、授权的库表、权限范围、计划回收时间。半年下来翻一翻哪些账号该清了、哪些权限给宽了一目了然。另外就是第6章提到的删除账号前先锁24小时观察确认没有告警再DROP。这个习惯看起来保守但一次误删的代价远远超过多等24小时的耐心。还有一点比较隐蔽如果你在MySQL里直接DELETE FROM mysql.user删过用户记得执行FLUSH PRIVILEGES否则权限缓存不会自动更新后续会出现各种奇奇怪怪的权限错乱。用DROP USER就不会有这个问题。数据库权限管理没那么刺激但它是数据库安全的第一道闸门。把这套操作练熟比装再多安全插件都管用。下次再有人拿着公共账号在库上跑DROP你可以理直气壮地指着他鼻子说这个账号是谁的谁批准他有DROP权限的心里没数吗。