1. 先搞清楚为什么不能直接拿 root 一把梭
每次看到有人建库建表都用 root 账号一把梭,我都替他的数据库捏把汗。MySQL 默认安装完成后,root 是超级管理员,拥有全部权限,任何表都能查能改,任何库都能删能建。这个账号一旦泄露、被爆破或者被误操作,整个数据库就是裸奔状态。而实际业务里我们需要的往往只是一个“能从应用服务器连上 MySQL,能读写某个库,但是不能碰其他库”的账号,这类需求用 root 来做,既不安全,也容易在事后排查时分不清是谁在操作。
这个标题“Mysql 创建用户并授权”看起来简单,但背后实际上涉及三件事:账号本身怎么建、权限怎么给、给完之后怎么验证和维护。这几步如果只记住几条 SQL,遇到生产环境会栽很多跟头。本文面向的是那些刚入门 MySQL 管理、或者准备把数据库账号从 root 收敛成专用账号的开发者,我会把创建、授权、远程连接、SSL 报错、权限回收这些环节全部走一遍,并把实际踩过的坑也放进来。
MySQL 的账号体系其实和 Linux 的用户体系很像:先有人(账号),再给他角色(权限),并且要告诉他能在哪台机器上登录(host 限定)。只不过 MySQL 的账号是由 user 和 host 两部分组成的,同一个用户名在不同来源下可能是两个独立的账号,这一点很多新手会忽略。理解了这套基础模型,后面的所有操作逻辑都会变得顺理成章。
2. 从零创建一个专用 MySQL 账号
2.1 基础命令:CREATE USER 与 IDENTIFIED BY
最简单的方式是在 MySQL 命令行里执行下面这段:
CREATE USER 'appuser'@'localhost' IDENTIFIED BY 'App@123456';这条语句的含义是:创建一个名为 appuser 的账号,只允许从本机(localhost)发起数据库连接,密码是 App@123456。注意 MySQL 8.0 里面必须先执行 CREATE USER,后面才能执行 GRANT 授权,两者是分开的。但在 MySQL 5.7 及更早版本里,可以直接用 GRANT 语句顺手创建用户,比如GRANT SELECT ON mydb.* TO 'appuser'@'localhost' IDENTIFIED BY 'App@123456';,也就是说老版本里 GRANT 自带“建人”功能。8.0 把这个行为废掉了,强制拆成两步,目的是让账号创建和权限授予边界更清楚。
很多人在 8.0 环境下习惯性地写GRANT ALL ON mydb.* TO ...却报错,就是因为没先建用户。另一个容易忽略的细节是,用户名后面那个@'localhost'千万别省。如果不写主机部分,默认是'appuser'@'%',意思是不限制来源,任何机器都能拿这套账号密码尝试连接。虽然最终能不能登录还取决于 bind-address 和防火墙,但权限面确实被放大了。
密码那一项,我建议不要用过于简单的纯数字或纯字母组合。MySQL 8.0 默认安装了 validate_password 密码校验组件,强度不够会直接拒绝执行,比如强制要求大小写字母、数字和特殊字符同时出现。5.7 如果装了对应插件也会有同样约束,没装的话则只受全局参数validate_password_policy控制。生产环境里“密码要强”不是口号,而是实打实的防爆破底线。
2.2 把授权粒度做到刚好够用
账号建完之后,第二步是授权。比如要给 appuser 分配“只读某个业务库”的权限:
GRANT SELECT ON mydb.* TO 'appuser'@'localhost';如果你想让他能做增删改,但不允许他改动表结构,就要分开列:
GRANT SELECT, INSERT, UPDATE, DELETE ON mydb.* TO 'appuser'@'localhost';这里mydb.*表示 mydb 库下面所有表。MySQL 的授权粒度其实是非常细的,可以从“所有库”一路细到“某张表”甚至“某个字段”:
-- 整个实例的所有库 GRANT SELECT ON *.* TO 'appuser'@'localhost'; -- 某个库的所有表 GRANT SELECT ON mydb.* TO 'appuser'@'localhost'; -- 某张表 GRANT SELECT ON mydb.orders TO 'appuser'@'localhost'; -- 某张表的某些列(比如只能看订单编号和金额,不能看手机号) GRANT SELECT (order_id, amount) ON mydb.orders TO 'appuser'@'localhost';绝大多数业务环境把粒度控制到“库”这一级就够了,表和字段级授权用得少,因为管理成本和误操作风险都会上升。但有一个场景是例外:报表账号。给第三方出数、给 BI 工具拉数据时,经常遇到“一个账号要连几个库、但又不能碰某些敏感列”的诉求。字段级授权虽然烦,但比另建一张脱敏视图省事。
关于 ALL PRIVILEGES 要单独提醒一句:GRANT ALL ON mydb.*只是把“该库内”的所有权限都给了,它并不包含 GRANT OPTION。也就是说,这个账号仍然没有资格把权限转授给别人。如果你想让他当该库的管理员,可以额外加WITH GRANT OPTION,但一般情况下不建议,因为一旦账号沦陷,影响范围会被放大。给到 ALL 并且带 GRANT OPTION,某种程度上已经等同于这个库的 root 了。
2.3 授权之后怎么查、怎么收
授权完之后,很多人随手敲一个FLUSH PRIVILEGES;,这个动作其实不是必需的。用 GRANT 或 REVOKE 修改权限时,MySQL 会自动重新加载权限表,不需要手动刷新。FLUSH PRIVILEGES 只有在直接用 INSERT、UPDATE、DELETE 操作了mysql.user、mysql.db这些底层权限表时才是必要的。
查一个账号当前有什么权限,用:
SHOW GRANTS FOR 'appuser'@'localhost';输出结果里能看到以GRANT ... TO形式展示的权限列表。注意这里必须带上 host 部分,和创建账号时的写法保持一致,否则可能查出来的是另一个同名账号。
想收回权限,用 REVOKE:
REVOKE UPDATE ON mydb.* FROM 'appuser'@'localhost';想彻底删除账号:
DROP USER 'appuser'@'localhost';这两个操作在生产环境执行前,我强烈建议先看一遍SHOW GRANTS和相关库的连接情况,尤其是“这个账号是不是还在被生产应用使用”。我踩过一次很蠢的坑:某个后台管理系统用的账号,第一个人离职前清理“无用账号”,直接 DROP 了,结果第二天线上系统连不上库,全家老小一起排查才发现是账号没了。任何账号删除前,先查information_schema.processlist里有没有活跃连接,这个习惯能救命。
3. 授权之后真正难缠的地方:远程连接与 SSL
3.1 从应用服务器连接要怎么限定来源
场景一升级,事情就变复杂了。开发环境只在本机操作,'appuser'@'localhost'够用;但线上应用一般部署在单独的机器上,应用服务器要通过网络连 MySQL,这时候账号就得改成允许指定主机访问。通常是这么写:
CREATE USER 'appuser'@'192.168.1.100' IDENTIFIED BY 'App@123456'; GRANT SELECT, INSERT, UPDATE, DELETE ON mydb.* TO 'appuser'@'192.168.1.100';这里的 IP 就是应用服务器的内网地址。只允许这一个来源,比写成%稳妥得多。%是通配符,表示“任意主机都能试这个账号密码”,配合强密码也许还能扛一阵,但只要密码泄露或爆破成功,攻击者想从哪连都能连。按最小化原则,主机限定也应该收窄。
如果应用服务器的 IP 经常变(比如动态分配的云主机、容器调度环境),可以把来源写成网段:
CREATE USER 'appuser'@'192.168.1.%' IDENTIFIED BY 'App@123456';这个%只能匹配 IP 段里的最后一部分,意思是“192.168.1.x 这个网段的机器都能连”。MySQL 的主机匹配并不是直接按字符串相等来做的,而是按精确度从高到低排序:带具体 IP 的最优先,其次是主机名,然后是网段,最后才是%。这意味着如果同时存在'appuser'@'192.168.1.100'和'appuser'@'%',那个具体 IP 永远会命中前者,两个账号密码可以分别设置,互不影响。这也是排查账号问题时要特别注意的点:你改的可能是%这条记录,但应用服务器实际命中的却是具体 IP 那条。
3.2 bind-address 和 skip-name-resolve 的坑
不少人在配置远程连接的过程中,明明账号、授权、防火墙都弄好了,客户端还是报Can't connect to MySQL server on 'x.x.x.x'。这时候十有八九是服务端配置文件的问题。默认安装的 MySQL,bind-address可能被设置为127.0.0.1,意思是只监听本机回环地址,外部请求根本到不了 MySQL 进程。改法是在my.cnf或my.ini的[mysqld]段里设置:
bind-address = 0.0.0.00.0.0.0表示监听所有网卡,生产环境建议直接写内网 IP,比如bind-address = 192.168.1.10,这样外网网卡不监听,少暴露一层。
还有skip-name-resolve这个参数,它默认在部分发行版的配置里是关闭的。如果打开,MySQL 就不会反查客户端 IP 对应的主机名。此时在授权语句里写'appuser'@'localhost'可能没问题,但写'appuser'@'webserver.hostname'就会失效,因为服务端根本不做主机名反解了。反过来,如果服务端开启了反解而 DNS 又有问题,客户端连接时会出现很长时间的延迟,最后报Access denied或解析超时。遇到这类奇怪问题,先看一眼my.cnf里有没有skip-name-resolve,再决定账号 host 是写 IP 还是主机名。最省心的做法是:账号一律用 IP 或网段,服务端保持 skip-name-resolve 开启状态,不依赖 DNS。
3.3 caching_sha2_password 与 SSL 连接报错
MySQL 8.0 默认的认证插件是caching_sha2_password,这比 5.7 时代的mysql_native_password安全很多。但它有个连锁反应:一些老版本的客户端驱动、旧版 JDBC 驱动、部分第三方工具(比如很老版本的 Navicat 或 PHP 的 mysql 扩展)不认识这个插件,连接时会直接报错:
Authentication plugin 'caching_sha2_password' cannot be loaded还有一类报错更隐蔽:初次连接时提示需要 SSL 或 RSA 公钥交换,比如:
Public Key Retrieval is not allowed这是因为我这边的客户端驱动在做 caching_sha2_password 认证时,需要向服务端获取 RSA 公钥来加密密码传输。新版 MySQL 客户端默认允许这种行为,但一些 JDBC 连接串并没有默认开这个开关。常见处理办法是在 JDBC URL 末尾加allowPublicKeyRetrieval=true&useSSL=false,不过关闭 SSL 会降低传输安全性。更合理的做法是升级驱动,或者给这类老客户端单独创建一个使用旧认证插件的账号:
CREATE USER 'legacy_app'@'192.168.1.%' IDENTIFIED WITH mysql_native_password BY 'Legacy@123456';说真的,如果业务允许,我更倾向升级驱动而不是开mysql_native_password,因为 caching_sha2_password 本身对密码传输有强保护,MySQL 8.x 后续版本对旧插件的支持也只会越来越边缘化。创建用户时如果看到认证插件相关的报错,先分清是驱动太老还是服务端配置太激进,再决定改哪一头。
3.4 Docker 部署 MySQL 时的授权前置步骤
现在很多人用 Docker 跑 MySQL,这也会影响“创建用户和授权”的流程。一个典型的启动命令会把数据目录和配置文件都挂载出去:
docker run -d \ --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=Root@123456 \ -e MYSQL_DATABASE=mydb \ -e MYSQL_USER=appuser \ -e MYSQL_PASSWORD=App@123456 \ -v /opt/mysql/data:/var/lib/mysql \ mysql:8.0注意这里的环境变量MYSQL_USER只会在数据目录是“首次初始化”时生效,它创建的账号默认是'appuser'@'%',而且会授予这个账号MYSQL_DATABASE指定库的全部权限。如果你已经挂载了一个旧的数据目录,或者镜像已经初始化过,这些环境变量就不会再生效了。很多人犯的错是:项目里写了MYSQL_USER环境变量,但容器用的数据卷是之前的老数据,里面根本不存在这个账号,启动后应用自然连接失败。这时候需要进入容器手动补授权,或者用docker exec执行 SQL:
docker exec -it mysql8 mysql -uroot -p进入后执行常规的CREATE USER和GRANT即可。还要记住,容器内 MySQL 的默认配置通常监听3306端口,但容器外能否连接取决于-p端口映射和宿主机防火墙,这三层里任何一层没通,远程连接都会失败。
4. 常见问题排查与避坑清单
4.1 Access denied 到底是在哪一层被拦的
Access denied for user 'appuser'@'x.x.x.x' (using password: YES)是 DBA 们最熟悉也最烦的报错之一。这个报错出现在认证阶段,与网络连通性无关。看到它,第一反应不是去折腾防火墙,而是先猜这么几种可能:
- 账号不存在,尤其要注意 host 部分是不是匹配不上。你建的可能是
'appuser'@'192.168.1.100',应用却从192.168.1.101连过来,匹配不上就报拒绝。 - 密码错了。这个最简单也最容易在事态混乱时被忽略。
- 账号和密码都没问题,但
skip_name_resolve开着,客户端 IP 反解失败导致 host 匹配不上。 - 插件不兼容。比如服务端默认 caching_sha2_password,老客户端不支持。
排查这类问题,我一般从两个信息入手:第一,SELECT user, host, plugin FROM mysql.user;看账号信息全貌;第二,看 MySQL 错误日志。日志里通常会记录具体是哪个账号、从哪个 IP 被拒绝,能直接定位到“账号不存在”还是“密码错误”。还有一个小技巧:在应用服务器上用命令先测一遍,排除客户端驱动层面的干扰:
mysql -uappuser -p -h 192.168.1.10 mydb这一步能连上,就说明服务和账号都正常,问题出在应用配置或驱动上;连不上,再按上面的顺序逐步缩小范围。
4.2 为什么改了权限还是没生效
很多人跑了 GRANT 之后,客户端依然报没有权限。最常见的原因不是没刷新,而是你改错了账号。因为 MySQL 账号由 user + host 双重决定,如果程序连接时命中的是'appuser'@'192.168.1.%',你却在改'appuser'@'localhost'的权限,那当然不生效。
排查思路很直接:在应用服务器上用CURRENT_USER()看一下实际登录身份:
SELECT CURRENT_USER();返回的结果就会告诉你这次的会话到底是以哪个 user@host 身份进来的。比如返回appuser@192.168.1.101,那就去改'appuser'@'192.168.1.%'的权限。这一步能省掉大量瞎猜时间。
另一个可能的原因是账号在连接时用的库不属于授权范围内。比如你只授权了mydb.*,但客户端连接串里默认的数据库写的是information_schema或者空库,某些查询用到未授权的库时自然报SELECT command denied。这不是授权没生效,而是连接参数的库名和授权范围不匹配。
4.3 localhost 和 % 的微妙关系
前面提到过 host 匹配的优先级问题,这里单独展开讲。MySQL 在匹配账号来源时,会先按 host 的精确度排序,而不是按插入顺序。匹配优先级从高到低大致是:具体 IP > 主机名 > 网段(比如 192.168.1.%)> 空字符串(表示本机匿名)>%。
举例说明。执行下面两条语句:
CREATE USER 'test'@'%' IDENTIFIED BY 'pass1'; CREATE USER 'test'@'localhost' IDENTIFIED BY 'pass2';在 MySQL 服务器本机执行mysql -utest -p时,命中test@localhost。因为在优先级排序里,localhost精确度最高,不会落到%。也就是说,你用pass2才能在本机登录,用pass1反而连不上。这种情况经常让人以为是自己密码打错了,其实是 MySQL 暗地里帮你做了账号分流。
所以,当你发现“同一个用户名,不同地方登录行为不一致”的时候,一定要去mysql.user表里把所有同名账号列出来,看看 host 字段各是什么,再判断是不是 host 匹配在捣乱。
4.4 忘了 root 密码怎么把权限找回来
说句实话,再谨慎的人也有过忘记 root 密码的时刻。这个问题的应急方案是:在配置文件中临时加一条skip-grant-tables,然后重启 MySQL,这样无需密码就能进入数据库,再自行修改密码。
具体流程是:编辑my.cnf或my.ini,在[mysqld]段下加:
skip-grant-tables重启 MySQL 服务:
service mysql restart然后直接无密码进入:
mysql -uroot进入后先执行FLUSH PRIVILEGES;让权限系统重新加载,再改密码:
ALTER USER 'root'@'localhost' IDENTIFIED BY 'NewRoot@123456';改完后,一定要把配置文件里的skip-grant-tables注释掉,再重启一次服务。因为这条参数会让 MySQL 跳过所有权限检查,任何知道端口的人都能连进来读写数据,这是极其危险的临时状态。生产环境操作这个流程时,我建议选在流量低峰期,并且把防火墙临时收紧到只允许本地管理 IP 访问,不然等于大门敞开。
4.5 权限回收与账号生命周期管理
授权的另一边是回收。一个项目的账号不可能永远不变,人离职、系统下线、权限收敛,都会涉及“把权限拿回来”这件事。我建议平时就建立一套简单的账号管理清单,至少包含三个维度:账号对应的应用、最后一次活跃时间、授权范围。定期清点mysql.user表,找到那些“看起来还在但已经三个月没有连接记录”的账号。
查询活跃连接可以用:
SELECT user, host, db, command, time, state FROM information_schema.processlist;这个视图会显示当前所有会话。如果一个账号不在任何 processlist 里,同时又没有被任何配置引用,那它就应该被仔细审查。删除前先SHOW GRANTS留底,再用 DROP USER 清理,避免像我在前面提到的那样误删核心账号。
权限回收还有一个容易被忽略的操作:当你调整权限时,已建立的连接不会立即失效,会话会继续保留原来的权限直到断开。所以线上压权限时,除了执行 REVOKE,有时候还要配合KILL掉对应会话才能真正生效。这与“授权后不需要 FLUSH PRIVILEGES”并不冲突,因为刷新权限缓存和断开已有会话是两个不同维度的问题。
5. 一些实战中沉淀的小心得
CREATE USER和GRANT这套 SQL 本身不难,难点在于“了解这个动作会带来什么影响”。我经历了从开发到运维再到自己管理线上库的转变,最大的体会是:权限永远要按最小化去给,能只读就别给写,能只限具体 IP 就别用%,能不用 root 就别用 root。密码策略再繁琐,也比被拖库之后处理事故轻松百倍。
还有一个和标题无关但常常一起出现的小建议:尽量把所有账号的创建和授权语句放进一个 SQL 版本管理目录里,和代码一起走评审。很多故障不是语法不会,而是“忘了当时给这个账号配了什么权限”。把授权语句当成代码一样管理,至少事后能查账,而不是对着mysql.user表发呆。
如果正在看这篇文章的你还处于刚接触 MySQL 的阶段,建议这会儿就打开终端,把文中的 CREATE USER、GRANT、SHOW GRANTS、REVOKE、DROP USER 全部操作一遍。建错了没关系,权限给大了也没关系,开发环境本来就是用来试错的。真正重要的是,你要把整套流程的操作手感练出来,知道每敲一条命令会产生什么后果,并且养成授权之后立刻查SHOW GRANTS的习惯。相信我,这个习惯会给未来的线上事故排查省下大把时间。