做后端开发和数据库运维这些年,MySQL用户管理一直是我最看重又最容易看到别人踩坑的地方。大多数人一开始只关心建库建表、写SQL,等到权限报错、账号被锁、或者被扫出个弱密码账号之后,才回头补这一课。MySQL的用户管理其实就两件事:谁能连上数据库,连上之后能干什么。这两件事理不清,后面所有操作都像在沙地上盖楼。
这篇文章把我平时处理用户和权限的实际经验完整拆一遍:从MySQL用户体系长什么样、怎么创建用户、怎么授权,到角色怎么用、日常怎么排查问题,全部用实际操作来说。无论是刚入门的开发、要自己搭库的测试,还是刚接手数据库的运维,照着这套思路走,基本不会再在用户权限上翻车。
1. 先把MySQL用户体系搞清楚
1.1 用户不是简单的“用户名”
MySQL里的用户和你平时理解的不太一样。一个MySQL用户由两部分组成:用户名和登录主机,写法上就是'用户名'@'主机'。
这个主机指的不是用户坐在哪台机器前,而是“这个账号允许从哪个IP或者哪个网段发起连接”。比如'app'@'10.10.0.%'表示只有来自10.10.0.x网段的连接才会被当成这个用户,从其他地方连过来,哪怕输入完全相同的用户名和密码,也会被拒绝。
这其实是个很实用的安全边界。同一套业务可以有多个账号:开发账号只允许从办公网段登录,应用账号只允许从应用服务器所在网段登录,DBA账号从运维跳板机登录。想一下如果所有人都用一个账号、能从任何地方登录,一旦密码泄露,影响范围会比现在大得多。
用户和主机的组合才是真正的身份标识,这也是为什么你看到的mysql.user表里,Host是主键的一部分,不能只按User字段来判断一个账号是否存在。
1.2 用户信息存在哪里
用户相关的数据全都存在系统自带的mysql库里,核心表包括:
user:全局账号信息和全局权限,一个账号在这里最多一行db:数据库级别的权限tables_priv:表级别的权限columns_priv:列级别的权限procs_priv:存储过程和函数的权限
MySQL在收到一条SQL请求时,会分两个阶段做检查。第一个阶段是连接认证,校验用户名、来源主机、密码、账户是否锁定;第二个阶段才是权限校验,根据你执行的操作类型去不同的权限表里查对应的权限。比如查询一张表,会先看全局权限,再看库权限,再看表权限,一层层往下判断。
日常操作里,我们一般不用直接去改这几张表,而是通过CREATE USER、GRANT、REVOKE这些SQL命令来管理。MySQL会自动帮忙维护系统表里的记录。直接人工去INSERT、UPDATE系统表的做法不推荐,稍不留神就会造成字段遗漏或者权限状态不一致。
1.3 用户管理的本质是边界控制
把用户管理这件事想成给不同的人发门禁卡:核心机房员工卡能进普通办公区但不能进财务室;外包保洁卡只能进公共区域,刷不开研发部门的门。MySQL用户管理的本质就是按照职责边界控制访问范围。
权限给得太宽,是生产环境最常见的隐患。很多项目早期图省事,所有开发都用同一个root账号连库,等出了问题想排查是谁干的都查不出来,更不用说一个高权限账号泄露后整个库都暴露了。用户管理做得好的系统,出了安全问题能快速定位、快速隔离,这比安装任何安全软件都实在。
2. 创建用户:从一行命令开始
2.1 CREATE USER 的基本语法
创建用户用CREATE USER,最简单的写法:
CREATE USER 'dev'@'10.10.0.%' IDENTIFIED BY '这里写密码';8.0版本里,这条命令执行完,用户就建好了,默认这个账号什么库都访问不了,连登录后执行SHOW DATABASES都只能看到一个空列表和系统库。
有一点要特别提醒:创建用户之前要确认主机匹配规则。'dev'@'%'表示可以从任意主机登录,这个写法在测试环境图省事可以,生产环境我基本不用。因为一旦密码泄露,攻击面就是全网了。业务账号的主机限制写得越具体越好,比如'10.10.0.%'或'10.10.1.15'。
这里顺带提一下MySQL 8.0和5.7在创建用户上的差异。8.0里CREATE USER和GRANT是分开的两条语句,不能像5.7那样直接GRANT ALL ON db.* TO 'user'@'host' IDENTIFIED BY 'password'。8.0要先把用户建出来,再单独授权。代码迁移到8.0时,这一步经常会报语法错误,需要留意。
2.2 密码策略与认证插件
密码这一块,MySQL 8.0默认启用了密码校验策略组件。简单说,你设置密码太短、太常见、或者只有数字,MySQL会直接拒绝执行。我在测试机上试过设置纯数字短密码,大概率会收到类似“密码不符合当前策略”的报错。
生产环境我建议保留这个默认策略,至少要求密码达到一定长度并包含大小写、数字和特殊字符。不要嫌麻烦,也不要嫌密码难记,密码这东西本来就是用来防人的,好不好记排在第二位。
认证插件方面,MySQL 8.0默认用的是caching_sha2_password,安全性比5.7时代的mysql_native_password高不少。老版本客户端(比如某些旧版驱动)可能不支持新插件,连接时会报错。如果遇到这个问题,要么升级客户端驱动,要么在 MySQL 侧把账号指定为旧插件,但这样做会降低安全性,能不用就不用。
密码策略相关参数存在系统变量里,比如validate_password.policy的取值有LOW、MEDIUM、STRONG。需要临时调低策略时,可以在测试库上改一下,但生产库建议保持默认或更高:
SHOW VARIABLES LIKE 'validate_password%';2.3 一个标准创建流程实例
这里给一个我平时在项目里最常创建的一种账号:只读账号。很多报表、BI系统、数据抽取任务只需要读数据,完全不需要写权限。
CREATE USER 'bi_read'@'10.10.0.%' IDENTIFIED BY 'Read@2024!'; GRANT SELECT, SHOW VIEW ON reporting_db.* TO 'bi_read'@'10.10.0.%'; FLUSH PRIVILEGES;到这里我每次都会提醒一句:FLUSH PRIVILEGES这条命令不是每次授权后都要执行的。用CREATE USER和GRANT改权限,MySQL会自动加载生效;只有当你直接操作了mysql.user等系统表(虽然我们不推荐)时,才需要FLUSH PRIVILEGES让它重新读取权限表。
为什么网上还有那么多人每次授权后都刷一下?因为旧版本的某些操作路径确实需要,加上习惯性操作,慢慢就成了“学了这么干”的典型例子。实际上多刷一次没有危害,但没必要让它变成程序化口诀,搞清楚背后的原理更重要。
上面实例里的SHOW VIEW是很多报表账号需要的,没有它,即使有SELECT权限,查看视图定义也会被拒绝。这类细节不是日常坑你的高频点,但遇到了会莫名其妙花半天时间。
用户创建完后,建议立即验证一下:
SHOW GRANTS FOR 'bi_read'@'10.10.0.%';结果里如果看到GRANT SELECT, SHOW VIEW ON reporting_db.* TO 'bi_read'@'10.10.0.%',就说明用户和权限都生效了。
3. 用户权限:不是只有“全部”和“没有”
3.1 MySQL权限的分级体系
MySQL的权限是按层级组织的,从全局到具体列,可以按需分配。理解这个层级,是管理好权限的前提。
- 全局权限:存在
mysql.user表里,作用于整个MySQL实例,比如CREATE USER、PROCESS、RELOAD - 数据库权限:存在
mysql.db表里,作用于指定数据库的所有对象,比如对某库的SELECT、INSERT - 表权限:存在
mysql.tables_priv表里,作用于指定表,比如只让某账号查某一张订单表 - 列权限:存在
mysql.columns_priv表里,作用于指定表的指定列,比如只允许查看用户表的姓名列、不允许查看手机号列 - 存储过程和函数权限:存在
mysql.procs_priv表里,控制对存储过程的调用和查看
理解这个层级对设计权限方案很有帮助。举个例子,一个客服运营看板账号,可能只需要读订单表的近30天数据,那给SELECT权限就够了。如果库表很多,还可以单独限制到某几张表上,而不是给整个库。
这里最常见的误区是“权限给得差不多就行”。MySQL的权限是累加的,几个不同层级的权限会同时生效。比如一个账号既有设置某张表的INSERT权限,又在数据库层级拥有ALL权限,那么这张表的INSERT权限实际上是冗余的。这种情况下,权限管理就会变得混乱,最好定期梳理,去掉不必要的授权。
3.2 GRANT和REVOKE的实用操作
授权用GRANT,收回用REVOKE。这两条命令的格式对应清晰:
-- 授权 GRANT 权限类型 ON 权限级别 TO 'user'@'host'; -- 收回 REVOKE 权限类型 ON 权限级别 FROM 'user'@'host';权限类型的选择要看具体业务。常用权限包括:SELECT查询、INSERT插入、UPDATE更新、DELETE删除、CREATE建表建库、DROP删表删库、ALTER修改表结构、INDEX索引管理、PROCESS查看进程、SUPER超级权限等。
SUPER这个权限要特别控制。它允许执行很多高危险操作,比如SET GLOBAL、结束其他用户的连接等。普通开发账号绝对不要给,DBA账号也建议用单独的账号,按需授予。
下面是一个常见的开发环境授权,给开发同学某库的全部表操作权限,但不允许删除库本身:
CREATE USER 'dev_ops'@'10.10.0.%' IDENTIFIED BY 'Dev@123!'; GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, ALTER, INDEX ON myapp_db.* TO 'dev_ops'@'10.10.0.%';如果把myapp_db.*替换成*.*,那就是全局权限了,这个操作要极其慎重。*.*授权意味着这个账号可以在整个实例的所有库上执行对应操作,一旦和DROP、SUPER这类高危权限组合,风险会呈指数级上升。
3.3 几个常用授权场景直接抄
授权方案不必每次从零设计,常见的场景完全可以套用模板,再根据实际微调。
| 账号类型 | 权限范围 | 典型授权语句 |
|---|---|---|
| 应用连接号 | 业务库基本增删改查 | GRANT SELECT, INSERT, UPDATE, DELETE ON app_db.* TO 'app'@'10.10.1.%' |
| 只读报表号 | 业务库只读加视图 | GRANT SELECT, SHOW VIEW ON app_db.* TO 'report'@'10.10.0.%' |
| 开发调试号 | 开发库建表改表 | GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, ALTER, INDEX, DROP ON dev_db.* TO 'dev'@'10.10.0.%' |
| DBA管理号 | 全局管理权限 | GRANT ALL PRIVILEGES ON *.* TO 'dba'@'10.10.0.%' WITH GRANT OPTION |
WITH GRANT OPTION是最后一个大坑:它表示这个账号可以把自己拥有的权限再授给其他用户。给DBA账号加上没问题,这是工作职责需要。但给应用账号加这个选项,基本等于绕过了权限审批。我在排查权限问题时经常发现,很多人当初只是觉得“加上这个选项以后方便”,实际上权限已经悄悄扩散了。日常维护时,非必要不加这个选项。
4. 角色:让权限管理更清爽
4.1 角色到底是什么
MySQL从8.0开始支持角色(Role)。角色的本质就是一个“权限集合”,你可以把一组权限定义成一个角色,然后把角色分配给多个用户。用户一登录,自动拥有角色对应的全部权限。
为什么要用角色?最直接的理由是:当一组用户需要同样权限时,不用逐个用户重复授权。比如三个报表账号都需要SELECT, SHOW VIEW权限,没有角色的时候,要执行三遍授权;有角色之后,授权一次给角色,再把角色赋给三个用户就够了。要收回权限时也只需改角色,不用挨个去处理账号。
这会让人联想到权限继承,但MySQL的角色比简单的权限复制更灵活,角色本身还能再包含系统权限,也能通过默认角色机制控制用户登录后是否自动激活。
4.2 角色的创建和使用流程
角色操作和用户操作高度类似:
-- 创建角色 CREATE ROLE 'report_role'; -- 给角色授权 GRANT SELECT, SHOW VIEW ON reporting_db.* TO 'report_role'; -- 把角色给用户 GRANT 'report_role' TO 'bi_read'@'10.10.0.%'; -- 设置默认角色,登录即生效 SET DEFAULT ROLE 'report_role' TO 'bi_read'@'10.10.0.%';这里有一个常见困惑:角色赋予了用户,为什么用户登录后权限还是不可见?因为MySQL的角色有两种状态:默认激活和非默认激活。只GRANT了角色,但没有设置默认角色,用户每次连接后要手动执行SET ROLE 'report_role'才能把角色激活。如果希望登录就自动有权限,就执行上面的SET DEFAULT ROLE。
还有一个更彻底的方式,在系统变量里设置:
SET GLOBAL activate_all_roles_on_login = ON;这个参数设置为ON后,所有用户登录时就自动激活所有被授予的角色。测试环境图省事可以开,生产环境我建议还是明确设置默认角色,这样权限更可控。
4.3 角色封装的最佳实践
用了角色之后,整个权限管理的思路可以从“给每个用户单独授权”变成“先设计角色,再把人归类进角色”。
实际操作中我一般按业务功能来设计角色:写业务角色、只读角色、运维角色、备份角色。一个刚入职的开发同学,只需分配“只读角色”和“开发库角色”,他的访问范围立刻就被约束在职责范围内。
角色出现后,账号交接也更方便了。离职员工账号直接禁用,新人使用新的账号,然后继承同一角色,权限和前任保持一致。整个过程不会出现“前任有某个表权限,新人没有”这种权限漂移问题。
角色清点也要定期做。可以用下面的SQL查所有角色以及被授权用户:
SELECT DISTINCT r.from_user AS role_user, r.to_user AS grantee_user, r.to_host AS grantee_host FROM mysql.role_edges r;这个查询能帮你一眼看出来哪个角色给了哪些账号。如果发现某个角色已经很久没有对应账号在使用,可以直接DROP ROLE清理。
5. 日常运维和问题排查:别让权限问题过夜
5.1 查看和盘点现有用户
接手一套MySQL实例,第一件事一定是盘点账号。查询所有用户:
SELECT User, Host, plugin, account_locked FROM mysql.user;可以看到每个账号允许从哪里登录、用什么认证插件、是否被锁定。接着对重点账号查看具体权限:
SHOW GRANTS FOR 'app'@'10.10.1.%';SHOW GRANTS返回的就是当前生效的授权语句。定期导出所有账号的权限清单,在变更前留档,是我一直坚持的操作。权限变更出问题后回滚时,这份东西就是救命稻草。
还有一个常用查询,找全局权限账号:
SELECT User, Host FROM mysql.user WHERE Grant_priv = 'Y' OR Super_priv = 'Y';这个结果会列出所有拥有GRANT或SUPER权限的账号,是排查高权限账号扩散的首选查询。
5.2 忘记root密码的恢复流程
root密码忘了,可以说是每个DBA迟早会遇到的事。在本地服务器上有恢复手段,思路无非是跳过授权表启动,然后重置密码。
步骤如下:
- 停掉MySQL服务
- 以跳过授权表方式启动:
mysqld --skip-grant-tables - 连接后先刷新权限:
FLUSH PRIVILEGES - 修改root密码
- 正常重启服务
注意,--skip-grant-tables方式启动的实例等于是裸奔状态,任何能连上来的人都不需要密码。所以恢复完成后要立刻正常重启,并且在这种状态下不要执行无关操作。
这个操作在生产库上要慎之又慎,最好在维护窗口执行,并提前做好备份。因为跳过授权表模式下,一些依赖权限校验的功能可能表现异常,贸然操作存在风险。如果实例上有其他高可用架构,还要考虑主从切换带来的影响,不要只盯着单机步骤。
5.3 权限不生效、连接被拒这些常见问题
我在平时答疑时,被问到最多的是以下几类问题,基本覆盖了90%的场景。
问题一:明明用户存在,密码也对,但就是连不上。优先检查来源主机是否匹配。用户是'app'@'10.10.1.%',你却从10.10.2.88这台机器连接,MySQL会直接拒绝。排查时可以临时把客户端IP加入主机范围,但用完后要收回,不要长期保留。
问题二:用root连上了,给某个用户授权时报权限不足。8.0里root账号不一定默认带GRANT OPTION。需要在root账号上授权WITH GRANT OPTION,或者确认当前用的是有授权能力的DBA账号。有些云数据库默认的“管理账号”权限范围是专门做过的,需要读一下服务商说明。
问题三:刚授权完,用户那边还是提示没有权限。新权限对已经建立的连接不会立刻生效,需要断开重连。MySQL在每次新建连接时会读取一次权限快照。正在跑的长事务不会因为中途授权就获得新权限,这是设计如此,不是故障。
问题四:账号被锁定。登录时提示Access denied, account is locked,说明账号状态是锁定状态。解锁语句:
ALTER USER 'app'@'10.10.1.%' ACCOUNT UNLOCK;连续密码错误登录达到次数后自动锁定,这个行为在MySQL 8.0可以通过FAILED_LOGIN_ATTEMPTS和PASSWORD_LOCK_TIME选项配置,默认情况下不开启自动锁定。
问题五:改了用户密码后,旧连接还能继续跑。密码修改只影响后续连接,已经建立的连接会继续存在。如果怀疑密码泄露,除了修改密码,还要手动结束掉旧的活跃连接,让所有会话都使用新密码重新认证。
5.4 安全习惯:不给权限留死角
用户管理持久化,拼的不是某一次操作多惊艳,而是日常有没有把该做的小事做干净。我这边有几条底线是长期坚持的:
- 禁止所有账号使用简单密码,密码统一由生成器生成并妥善保管
- 删除长期不使用的账号,尤其是离职员工的账号
- 每年做一次全量权限盘点,核对每个账号的实际用途
- 关键库表的高危操作(DROP、ALTER)建议双人复核,或用专门的高权限账号在受控环境执行
- 账号命名带上用途标识,比如
app_、dev_、report_,维护时会省很多力气
这些习惯看起来很基础,但我在实际维护中看到太多事故,都是因为一两个被忽略的弱密码账号或者一个忘记收回的高权限账号引起的。数据库里的数据是一个系统的重要资产,权限控制就是这扇门的锁。锁芯好不好、钥匙发给了谁、哪些钥匙该作废,值得每个用MySQL的人花心思。
最后分享一个小技巧:每次权限变更后,把变更前后的SHOW GRANTS导出保存,放到运维变更记录里。过几个月再回头看,你会感谢当时的自己留下了这么清晰的审计线索。用户管理这件事,真的就是细节决定安全。