☰
MySQL用户创建与授权指南:从CREATE USER到GRANT的权限管理实战
2026/10/22 5:14:57 网站建设 项目流程

上周帮某同事收拾烂摊子——测试环境的应用一直用 root 连数据库,密码被写进一个临时脚本,后来脚本泄露,整个测试库被人清了。虽然只是测试环境,但也把大家吓出一身冷汗。我从头到尾做了一遍检查,顺手把所有账号收拢,重新用最小权限的应用账号替换了 root。弄完之后我就在想,MySQL 创建用户和授权这种最基础的操作,其实很多写了好几年代码的开发同学都未必能一次做对。

这篇文章就把我个人在项目中常用的 MySQL 用户创建与授权流程完整写一遍。内容覆盖 CREATE USER、GRANT、REVOKE、DROP USER、远程连接排查、认证插件兼容等常见问题,适合刚接触数据库的开发者,也适合已经写过一些 SQL 但没系统梳理过权限体系的后端工程师。看完之后你可以直接照着抄,也能搞明白每条语句背后的逻辑,不用再凭印象拼 SQL。

1. 先明白为什么不能一直用 root:账号与授权的核心逻辑

很多开发者的习惯是装完 MySQL 就直接用 root 干活,建库后把 root 密码发给全组共享。这在一两个人的本地开发环境里看着没啥,但只要代码要部署,账号要接入测试环境,问题就会接踵而来:密码改一次全组都要同步、权限收不干净、误操作没法追责、被拖库以后整个实例的所有数据都暴露。

MySQL 的权限体系其实解决的就是这几件事:谁来连、从哪里连、能看什么、能改什么。理解清楚这四个问题,创建用户和授权就不是背语法的问题了。

1.1 登录用户和业务账号是两码事

MySQL 里的“用户”和我们平时说的用户名不太一样。一个完整的账号由两部分组成:用户名@主机。比如root@localhost表示只能从本机登录的 root 账号,app@'192.168.1.%'表示来自 192.168.1 这个网段的 app 账号。注意,同名的用户绑定不同主机,会被当成两个完全独立的账号对待,密码、权限都可以各自独立设置。

这一点特别重要。很多人运行mysql -u app -p后连不上,第一反应是密码错了,实际上可能是 MySQL 把当前连接的地址解析后,根本没匹配到app@'%'这个账号,而是匹配到了别的记录,或者压根没匹配到。你登录时用的 app 只是用户名,真正参与匹配的是“用户名+来源地址”这个组合。

所以业务账号的第一原则:不要用 root,创建一个专用账号,只给这个业务真正需要的权限。第二原则:创建账号时就想清楚这个账号会被哪些机器连接,尽量把主机范围限制到网段或固定 IP,而不是一把梭%。

1.2 主机部分才是最容易看漏的地方

主机部分并不只是 localhost 和 % 两种写法。它支持 IP、网段、主机名和通配符。常见写法有:

写法含义
'localhost'本机通过 socket 或回环连接
'127.0.0.1'本机通过 TCP 回环连接
'192.168.1.10'固定 IP
'192.168.1.%'192.168.1 网段
'10.%'以 10. 开头的所有 IP
'%'任意主机

这里有一个实际踩过的坑:'localhost'和'127.0.0.1'在 MySQL 的账号匹配里并不等价。有些客户端工具默认走 socket,就会命中localhost条目;有些驱动默认走 TCP 127.0.0.1,命中的却是另一个条目。所以如果你创建了'user'@'localhost'然后发现程序用127.0.0.1连不上,别怀疑密码,先查一下主机段。

另外,MySQL 在多个账号记录都匹配当前连接时,会选最精确的那条,而不是按创建顺序。比如app@'192.168.1.10'和app@'192.168.1.%'同时存在,来自 192.168.1.10 的连接会命中前者,不会命中最宽泛的%。这个机制平时没问题,但当你以为改了app@'%'的密码就能影响所有连接时,现实可能会给你当头一棒——固定 IP 那条记录可能还留着旧密码。

2. CREATE USER 建账号的实操细节

创建用户的核心语句是CREATE USER。在 MySQL 8.0 之前,很多人习惯一条GRANT ALL ON *.* TO 'user'@'host' IDENTIFIED BY 'password'直接把创建和授权一起干完,因为旧版本的 GRANT 自带建用户能力。但 8.0 之后这个行为被移除,必须先CREATE USER,再GRANT。如果你的脚本跑到老版本库上出现莫名报错,多半就是版本行为差异。

2.1 最基本的创建语句

CREATE USER 'app_reader'@'192.168.1.%' IDENTIFIED BY 'SomeStrongPass123!';

这条语句做的事情是:在mysql.user表里插入一条账号记录,并设置初始密码。你可以随后给这个账号授权,也可以等需要时再授权。刚创建的用户如果没有任何授权,连SELECT都执行不了,这是正常的,不要慌。

密码部分有几个建议:

  • 不要使用弱密码。MySQL 8 的 validate_password 组件可以强制密码强度,但默认不启用,如果你所在团队没人配置,数据库不会替你把关。
  • 密码里包含特殊字符时,注意 SQL 语句中的引号转义。如果密码里含单引号,建议用双引号包裹或提前在配置管理工具里处理好,否则你会收获一个语法报错。
  • 密码是直接落库的,不要用简单可推测的组合。生产环境的数据库账号密码应该走密码管理工具或云厂商的密钥服务下发,而不是在聊天工具里传来传去。

2.2 主机段的常见写法与匹配优先级

我们组现在约定俗成的规则是:开发环境统一用%方便大家连,测试环境限定办公室出口 IP 或跳板机 IP,生产环境限定应用服务器内网网段。这个约定来自一次事故的教训:生产账号设成%,某天一个开发在本地不小心执行了 DELETE,等发现时已经来不及了。后来所有生产账号一律按网段收敛,意外删数据这种事再没发生过。

主机段如果写错了,最直接的现象就是“账号建好了,但应用连不上”。我自己排查这类问题时会先看mysql.user表里的 host 字段:

SELECT user, host, plugin, account_locked FROM mysql.user WHERE user = 'app_reader';

这条查询能快速确认账号到底绑定在哪个主机段上。如果发现主机段和实际来源地址不匹配,直接改就行:

RENAME USER 'app_reader'@'%' TO 'app_reader'@'192.168.1.%';

比删了重建要干净,因为权限会跟着迁移。

2.3 密码策略和 MySQL 8 的认证插件

MySQL 8.0 默认的认证插件是caching_sha2_password,而 5.7 及更早版本默认是mysql_native_password。很多老项目里的客户端驱动、ORM 连接池没跟上,连接 MySQL 8 时会直接报认证失败。

如果暂时没法升级驱动,可以用下面的方式创建一个走旧插件的账号:

CREATE USER 'app_legacy'@'%' IDENTIFIED WITH mysql_native_password BY 'SomeStrongPass123!';

但要注意,MySQL 8.4 开始已经将mysql_native_password标记为废弃,后续大版本迟早要移除。我的建议是:优先升级驱动、适配caching_sha2_password;只有被老系统卡脖子的时候才退回到旧插件,并且要把它视为临时方案,排期处理掉。

还有一个密码过期的问题。如果你或者运维通过脚本批量改过密码,可能会遇到“账号能连上但一执行 SQL 就报密码过期”的情况。这是因为 MySQL 开启了default_password_lifetime或者手动设置了过期策略。临时可以这样处理:

ALTER USER 'app_reader'@'192.168.1.%' PASSWORD EXPIRE NEVER;

密码安全是底线,但千万别让密码过期策略把线上应用搞挂。生产环境的账号过期设置必须有变更流程配合,而不是直接在数据库里改一下就完事。

3. GRANT 授权语句的拆解

账号建好之后,接下来就是授权。授权最核心的一条准则是最小权限原则:只给业务当前需要的权限,给多了就是在给未来的事故买彩票。权限粒度越细,越能把误操作、恶意操作的影响范围控制在单库甚至单表内。

3.1 从最小权限出发的授权组合

一个只读报表账号,通常只需要这样:

GRANT SELECT ON reporting_db.* TO 'app_reader'@'192.168.1.%';

一个普通业务账号,一般会给增删改查:

GRANT SELECT, INSERT, UPDATE, DELETE ON shop_db.* TO 'app_reader'@'192.168.1.%';

注意这里没有给CREATE、ALTER、DROP这些 DDL 权限。业务代码运行时根本不需要改表结构,就算代码里有建表的初始化逻辑,也应该通过专门的迁移工具账号去执行,而不是让运行期账号揣着 DDL 权限到处跑。否则一旦 SQL 注入或代码漏洞被利用,攻击者可以直接把整张表 drop 掉。

如果业务需要执行存储过程或函数,还需要单独授权:

GRANT EXECUTE ON FUNCTION shop_db.calculate_price TO 'app_reader'@'192.168.1.%';

这里能看出来,授权是可以细化到“某个库的某个函数”的。粒度越细,配置越繁琐,但安全兜底越结实。开发环境图省事可以宽一些,生产环境我强烈建议按这个思路逐项核对。

3.2 权限层级:全局、库级、表级、列级

MySQL 的权限层级可以理解成一层层往里套的盒子。最外圈是全局权限(*.*),然后依次是数据库级(db.*)、表级(db.table)、列级(db.table (col1, col2))、存储过程和函数、以及PROXY、GRANT OPTION等特殊权限。

权限层级写法示例适用场景
全局GRANT SELECT ON *.* TO ...管理账号、跨库查询账号
库级GRANT SELECT ON shop_db.* TO ...最常见,按业务库隔离
表级GRANT SELECT, UPDATE ON shop_db.orders TO ...只允许访问特定表
列级GRANT SELECT (user_name, email) ON shop_db.users TO ...隐藏敏感列

列级权限听起来很美好,实际用起来很麻烦。比如你给用户只授了SELECT (user_name, email),但没法阻止他SELECT COUNT(*);而且只要表里加了新列,旧授权不会自动带上,业务可能莫名其妙报错。所以列级权限我一般只建议用在极端敏感的字段,比如只在表里屏蔽身份证号、手机号这类明文数据,而不是当成常态授权方案。

还有一个很多人忽略的权限:REFERENCES。外键约束依赖它,但绝大多数应用根本不需要创建外键,所以默认不给。同理,SUPER是高危权限,不能给普通账号;FILE权限允许用户读写服务器文件,也别随手给。

3.3 重复授权与 GRANT OPTION 的边界

GRANT 语句可以重复执行,每次执行都是叠加效果。比如第一次GRANT SELECT,第二次GRANT INSERT,最后账号同时拥有 SELECT 和 INSERT。这个特性方便了增量授权,但也埋了个坑:如果某个权限不该给,后来忘了撤销,它就会一直挂着。所以做完授权后,建议看一眼最终结果:

SHOW GRANTS FOR 'app_reader'@'192.168.1.%';

这条命令输出会告诉你这个账号当前的所有权限,权限排查时比翻mysql.user表直观得多。

WITH GRANT OPTION是另一个容易手滑的地方。它表示这个账号可以把它拥有的权限再授予别人。管理账号或 DBA 可以加,普通应用账号绝对不能加。否则一个只该查数据的账号,也可以给别的账号发 DELETE 权限,权限链就失控了。

关于GRANT OPTION还有个小例子:如果你撤销了一个权限,但账号曾经用WITH GRANT OPTION把这个权限授予了别人,那么后续的授权关系会变得很难梳理。我见过一个测试环境,某账号因为历史遗留的 GRANT OPTION,能在多个库之间互相授权,最后只能把所有相关账号删掉重建。所以从一开始就严格控制 GRANT OPTION,能省掉后面一大半权限审计的麻烦。

4. 改密、收回与删除:账号生命周期管理

创建用户和授权只是账号生命周期的前半段,后半段是日常维护:密码要换、权限要调整、离职或项目下线后账号要回收。很多团队到了这步就开始放飞,直接在mysql.user表上 DELETE、UPDATE,结果各种奇怪问题。其实 MySQL 提供了成套的命令,没必要绕开。

4.1 修改密码的正确姿势

修改密码用ALTER USER:

ALTER USER 'app_reader'@'192.168.1.%' IDENTIFIED BY 'NewStrongPass456!';

执行后立即生效,不需要重启数据库,也不需要刷新权限。

老教程里常见的SET PASSWORD FOR 'user'@'host' = PASSWORD('xxx')在 MySQL 8 里已经不再推荐,PASSWORD()函数也被移除了。如果你在网上搜到类似语法,先确认一下版本是不是太老,别整套搬到新版本库里。

还有一种情况是账号没被锁定,但提示密码过期。查看账号状态:

SELECT user, host, password_expired, account_locked FROM mysql.user WHERE user = 'app_reader';

如果password_expired是 Y,直接改一次密码或者执行:

ALTER USER 'app_reader'@'192.168.1.%' PASSWORD EXPIRE NEVER;

这个操作对取消过期限制很有效,算是账号维护里的基础动作。

4.2 撤销权限与删除账号

撤销权限用REVOKE,语法和GRANT对应。比如之前给了 INSERT,现在想收回:

REVOKE INSERT ON shop_db.* FROM 'app_reader'@'192.168.1.%';

要注意的是,撤销和授予必须保持同样的粒度。你当时授的是shop_db.*,撤销就得写shop_db.*;如果你试图用shop_db.orders去撤销库级权限,MySQL 不会匹配到任何授权记录,甚至可能报错。这种不匹配是权限管理里非常容易踩的坑。

删除账号用DROP USER:

DROP USER 'app_reader'@'192.168.1.%';

它会连账号带权限一次性删掉,不用先 REVOKE。删之前最好跑一下SHOW GRANTS确认这是要删的账号,尤其是当同一个用户名存在多条主机记录时,别把还需要的连接给断了。

有人习惯直接删mysql.user表的记录,我不建议这么做。直接操作系统表绕过了 MySQL 的权限管理层,出了问题时排查链路会变得很痛苦。官方给出的操作方式是DROP USER,你也应该用这种方式。

4.3 FLUSH PRIVILEGES 到底什么时候要执行

这是数据库圈里流传最广的误区之一。很多老教程会告诉你:改完权限必须执行FLUSH PRIVILEGES;。这句话在新版本里已经不那么准确了。

当你使用CREATE USER、GRANT、REVOKE、ALTER USER、DROP USER这些语句时,MySQL 服务器会直接更新内存中的授权信息,不需要额外刷新。只有一种场景你必须主动执行FLUSH PRIVILEGES:你手动编辑了mysql.user表或其他授权表(比如 INSERT、UPDATE、DELETE),这种绕开正规语句的操作不会自动更新内存缓存。

所以我的建议是:日常操作全部用标准命令,不要养成随手FLUSH PRIVILEGES的习惯。它本身不会造成什么严重问题,但容易给人一种“我只要刷一下,权限问题就能解决”的错觉,真正的错误反而被掩盖了。

5. “账号建好了却连不上”的排查链路

创建用户和授权只是第一步,接下来几乎每个人都会遇到同一个问题:明明账号、密码、权限都对,但程序就是连不上。很多开发遇到这种情况会反复改密码、反复授权,其实问题根本不在 MySQL 的账号权限上。我给你完整梳理一遍我的排查顺序,照着走能省下大量时间。

5.1 从应用服务器跑到数据库本机的三层检查

第一层:数据库本机自测。在本机执行:

mysql -u app_reader -p -h 127.0.0.1

如果这一步都失败,大概率是账号的 host 段或密码问题,跟网络无关。如果成功,说明账号本身没问题。

第二层:应用服务器上测 TCP 连通性。先确认数据库端口能通:

telnet 192.168.1.100 3306

或者用 nc 代替也可以。端口不通,就要去查防火墙、安全组、数据库的 bind-address 配置。端口通了但 MySQL 报 Access denied,才轮到账号权限环节。

第三层:用应用同款连接串去测。很多程序员本机能连,代码里连不上,通常是驱动、连接池配置、SSL 参数、字符集这些细节在作祟。这时候别怀疑 MySQL 账号问题,回去看代码配置就好。

5.2 绑定地址和远程访问

默认安装的 MySQL,my.cnf或my.ini里常常写着:

bind-address = 127.0.0.1

这个配置表示 MySQL 只监听本机回环地址,外部机器无论如何都连不进来。你需要把它改成应用服务器可达的地址,或者直接用0.0.0.0监听所有网卡:

bind-address = 0.0.0.0

改完记得重启 MySQL 服务。这里有个比较隐蔽的点:bind-address只影响 MySQL 服务监听哪些地址,和账号授权里的 host 段是两回事。即使你把 bind-address 改成0.0.0.0,如果账号是app@'localhost',远程依然无法登录,因为账号匹配根本没过。

还有一项要检查的是skip-networking。这个配置如果开启,MySQL 会直接不监听 TCP 端口,只走本地 socket,那远程连接百分之百失败。线上一般不会开,但某些精简安装的镜像可能会带,值得看一眼。

5.3 认证插件兼容性排查

确认网络没问题、账号匹配也没问题了,还剩最后一个常见拦截点:认证插件。MySQL 8 默认的caching_sha2_password遇到旧客户端版本时,会报类似于 Authentication plugin 'caching_sha2_password' cannot be loaded 的错。这个报错非常直白,基本就能锁定是驱动版本太旧。

解决办法有两个方向:

  • 升级数据库驱动或客户端库,让连接支持caching_sha2_password。
  • 给该账号改成mysql_native_password作为过渡:
ALTER USER 'app_reader'@'192.168.1.%' IDENTIFIED WITH mysql_native_password BY 'password';

我个人更推荐第一种,因为 8.0 的默认插件是官方主推的方向,继续用旧插件只会让债务越滚越多。临时方案可以做,但要在文档里标记清楚,别让它成为永久的遗留配置。

6. 一套可直接照抄的落地清单

前面把原理和坑都讲完了,这一节给出一套我在项目里实际使用的落地清单。你不需要记住所有细节,遇到对应场景直接拿过去改一改就能用。

6.1 三类典型账号的标准配置

第一类,只读账号。常用于报表、BI、数据分析:

CREATE USER 'bi_reader'@'10.' IDENTIFIED BY 'ReadOnlyPass123!'; GRANT SELECT ON analytics_db.* TO 'bi_reader'@'10.';

第二类,普通业务账号。常用于后端服务访问主库:

CREATE USER 'app_service'@'192.168.2.%' IDENTIFIED BY 'AppServicePass456!'; GRANT SELECT, INSERT, UPDATE, DELETE ON shop_db.* TO 'app_service'@'192.168.2.%';

第三类,开发维护账号。常用于开发和运维同学在测试环境操作:

CREATE USER 'dev_maintain'@'%' IDENTIFIED BY 'DevPass789!'; GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, ALTER, INDEX, REFERENCES ON dev_db.* TO 'dev_maintain'@'%';

注意开发维护账号只给了单库的 DDL 权限,没有跨库全局权限,也没给DROP。如果你需要删除表的操作,可以单独评估风险后再决定是否追加。

6.2 日常高频命令速查

目的命令
创建用户CREATE USER 'u'@'h' IDENTIFIED BY 'p';
授权GRANT SELECT ON db.* TO 'u'@'h';
查看权限SHOW GRANTS FOR 'u'@'h';
修改密码ALTER USER 'u'@'h' IDENTIFIED BY 'newp';
撤销权限REVOKE INSERT ON db.* FROM 'u'@'h';
删除用户DROP USER 'u'@'h';
锁定账号ALTER USER 'u'@'h' ACCOUNT LOCK;
解锁账号ALTER USER 'u'@'h' ACCOUNT UNLOCK;

ACCOUNT LOCK这条我觉得特别实用。项目下线时如果还打算保留账号数据做审计,不用删,先锁掉,比直接 DROP 安全,也方便后续恢复。

6.3 最后提醒:把账号脚本纳入版本管理

数据库账号和权限配置其实是基础设施的一部分,但很多团队没有把CREATE USER、GRANT这些脚本放进 Git 仓库。于是出现了这样的场景:某天 DBA 请假,没人知道生产库某个账号当初为什么开了那么宽的权限;或者新环境搭建时,建账号全靠资深同事回忆,漏一个权限就要排查半天。

我的建议是:每个环境的账号权限单独用 SQL 脚本管理,按环境、按业务模块分目录存放。脚本里用注释写清楚每个账号的用途、申请人和申请日期。这样即使权限有调整,也能在代码评审阶段被其他同事看到,而不是 DBA 一个人默默在服务器上执行。

还有一条尝试过很有效的习惯:每次上线前,把SHOW GRANTS的结果导出来比对一遍。线上权限和脚本不一致时,不要直接改线上,先改脚本再在变更窗口执行,保证数据库的最终状态和脚本永远对齐。这个习惯坚持下来,权限漂移的问题基本就能消灭掉。

我自己这几年管理 MySQL 账号最大的体会是:权限问题看着是技术问题,其实是流程问题。只要把“该谁用、用在哪里、给多大权限、怎么回收”这套流程想清楚,绝大多数数据库安全事故都能避免。希望这份整理能帮你少踩几个我当年踩过的坑。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询