本地敲mysql -uroot -p一切正常,换到另一台机器上用客户端连,立马翻脸不认人——报错还五花八门:Host '192.168.1.20' is not allowed to connect to this MySQL server、Can't connect to MySQL server on '10.0.0.5' (10061)、由于目标计算机积极拒绝,无法连接、Authentication plugin 'caching_sha2_password' cannot be loaded。这几个错误长得像亲戚,其实分属完全不同的层次,处理顺序搞反了就会在一件事上耗掉整个下午。MySQL 8.0 远程连接之所以年年有人问,就是因为它同时压在四层上:网络监听、主机防火墙、账号授权(root@localhost和root@%的区别就在这一层)、认证插件。任何一层没打通,现象都是"连不上",但修法完全不同。
这篇东西写给三类人:刚在云服务器或虚拟机里装完 MySQL 8.0、想让本地客户端连上去的开发者;用 Docker 起 MySQL 8.0 之后发现端口映射怎么都不通的运维新手;以及被Access denied和10061反复折磨、只想快速定位到底是哪一层出问题的人。下面按"先分类、再逐层拆、最后给完整排查链路"的顺序来,每一层都会告诉你为什么这么设计、命令为什么这么敲,而不是丢一堆 SQL 让你照抄。
1. 先把"连不上"拆成两类:网络根本不通 vs 账号权限不匹配
1.1 判别两类问题的分界线在哪
连接被拒绝和Host is not allowed to connect这两句话,看起来都是"失败",但含义差得远。前者的意思是客户端发出的 TCP SYN 包被目标主机明确拒绝了,或者压根没到达 MySQL 进程——握手都没开始,MySQL 连你是谁都不知道。后者恰恰相反,说明TCP 连接已经建立成功,MySQL 服务端已经完成了握手、拿到了你的来源 IP,然后在权限表里查了一圈,发现这个来源不在允许列表里,主动把连接踢掉了。
判断方法其实很简单,一句话:看报错里有没有出现你客户端的 IP。
- 报错里带
'172.16.3.88' is not allowed、Access denied for user 'root'@'172.16.3.88'—— 网络层是通的,问题在账号授权,直接跳到第 3 章。 - 报错是
10061、积极拒绝、timed out、2003—— 网络层就不通,别去翻权限表,先查监听和防火墙,看第 2 章和第 5 章。
我见过太多人在第二种情况下疯狂改mysql.user表,加了三个root@'%'还是连不上,最后发现是安全组没放行 3306。方向错了,做多少都是无用功。
1.2 常见报错文本对应的故障层
把常见报错和它真正指向的层次列出来,遇到问题先查表,能省掉一半时间:
| 报错文本 | 含义 | 优先排查层 |
|---|---|---|
Host 'x.x.x.x' is not allowed to connect | TCP 已通,来源主机被拒 | 账号授权层(root@localhost/root@%) |
Can't connect to MySQL server on 'ip' (10061) | Windows 下典型的 TCP 拒绝 | 监听地址、防火墙、容器端口映射 |
由于目标计算机积极拒绝,无法连接。(10061) | 同上,中文系统表述 | 同上,重点看端口有没有真的在听 |
Can't connect ... (110) timed out | 包被静默丢弃 | 安全组、防火墙 DROP 规则 |
Access denied for user 'root'@'ip' (using password: YES) | 账号在、密码错或来源不匹配 | 授权表、密码、认证插件 |
Access denied ... (using password: NO) | 客户端没带密码 | 客户端连接配置 |
Authentication plugin 'caching_sha2_password' cannot be loaded | 客户端版本太老 | 认证插件层(第 4 章) |
Host 'ip' is blocked because of many connection errors | 被max_connect_errors拉黑 | 执行FLUSH HOSTS解封 |
Lost connection ... at 'reading initial communication packet' | 中间设备干扰或超时 | 中间网络、SSH 转发链路 |
1.3 为什么 MySQL 8.0 让远程连接更容易失败
有几个设计变化,是老版本 MySQL 用户转到 8.0 之后最容易吃亏的地方,需要提前知道:
- 默认只监听回环地址。Debian/Ubuntu 的 MySQL 8.0 包安装后,
bind-address默认是127.0.0.1,也就是只有本机能连。这是出于安全考虑的保守默认值,但对想远程连的人来说就是第一个坑。 - 默认认证插件换成了
caching_sha2_password。5.7 时代是mysql_native_password,很多老客户端、老驱动对这个新插件不支持,握手阶段就断了,报错还特别绕。 - 密码强度策略默认开启。
validate_password组件在 8.0 里是默认装的,你想给远程账号设个123456会直接被拒绝,报ERROR 1819,很多人以为是权限不够,其实是密码太弱。 - 默认不允许 root 从任意主机登录。安装时创建的只有
'root'@'localhost',root@'%'根本不存在,所以"我从远程用 root 连"这个动作,在 8.0 默认配置下必然失败。
把这四点记住,后面的排查基本就是填空题。
2. bind-address 与监听地址:第一个必须过的关卡
2.1 MySQL 8.0 默认只监听 127.0.0.1 的原因
MySQL 装完之后,服务进程在 3306 端口上"听"的范围是由bind-address决定的。默认值是127.0.0.1,意味着内核只会把发往本机回环地址的 3306 流量交给 mysqld,外部网卡上来的包直接没人接,内核回一个 RST —— 这就是客户端看到的10061 积极拒绝。
为什么发行版要这么设?因为数据库是最不该默认暴露在公网上的服务之一。历史上大量服务器因为 3306 开放 + 弱密码被拖库,所以官方包的默认策略是"宁可你用不了,也不默认开"。理解了这一点,你就知道这个值不是 bug,是特性,改它的时候心里要有数。
2.2 检查当前监听状态的两条命令
在动手改配置之前,先确认现状,别凭猜:
# 看 3306 到底听在哪个地址上 ss -lntp | grep 3306 # 输出示例(未开放远程) # LISTEN 0 151 127.0.0.1:3306 0.0.0.0:* users:(("mysqld",pid=1234,fd=23)) # 输出示例(已开放远程) # LISTEN 0 151 *:3306 0.0.0.0:* users:(("mysqld",pid=1234,fd=23))如果看到的是127.0.0.1:3306,那不用往下猜了,先改配置。如果看到的已经是*:3306或0.0.0.0:3306,说明监听没问题,去查防火墙和账号。
ss比netstat好用的地方在于它不依赖net-tools包,新系统上默认就有。另一个必须确认的点是:配置里绝对不能有skip-networking,这个参数一开,mysqld 完全不监听 TCP,只走 socket,任何远程连接都不可能成功,而且日志里不一定有明显提示。
2.3 修改 bind-address 的完整操作
配置文件的路径在不同发行版上不一样,先搞清楚你的 MySQL 到底读的是哪个文件,否则改了半天不生效:
# 让 mysqld 自己告诉你它读了哪些配置 mysqld --verbose --help | grep -A1 "Default options are read from" # 或者更直接 my_print_defaults mysqld | head -30常见路径对照:
| 发行版 / 安装方式 | 主配置文件 | 通常放 bind-address 的位置 |
|---|---|---|
| Debian / Ubuntu apt | /etc/mysql/my.cnf | /etc/mysql/mysql.conf.d/mysqld.cnf |
| CentOS / RHEL / Rocky | /etc/my.cnf | /etc/my.cnf.d/mysql-server.cnf |
| 通用二进制包 | 解压目录下my.cnf | 自己决定 |
| Docker 官方镜像 | /etc/my.cnf | /etc/mysql/conf.d/*.cnf |
找到文件后,在[mysqld]段里改:
[mysqld] # 允许所有网卡监听(生产环境建议写具体的内网 IP) bind-address = 0.0.0.0 # 端口保持默认,如果改过端口这里要对应 port = 3306注意:
bind-address是静态参数,必须重启 mysqld 才生效,systemctl reload或mysqladmin reload都改不动它。重启命令用systemctl restart mysqld(Debian 系是mysql),重启后用ss -lntp | grep 3306再确认一次。
从 MySQL 8.0.13 开始,bind-address允许写多个地址,用逗号分隔,比如bind-address = 127.0.0.1,10.0.0.5,这样可以同时保留本地 socket 连接和内网监听。如果你的版本低于 8.0.13,只能写一个地址,写了多个会启动失败。这个细节在官方文档里不显眼,但我在线上见过因为多地址配置导致 mysqld 起不来的情况。
还有一个容易被忽略的点:!includedir是按文件名顺序加载的,后面加载的文件会覆盖前面的同名参数。如果你的/etc/my.cnf.d/里有两个文件都写了bind-address,生效的是排序靠后的那个。改之前先用grep -rn "bind-address" /etc/mysql/ /etc/my.cnf*全局搜一遍,避免改了 A 文件、被 B 文件覆盖。
3. 'root'@'localhost' 和 'root'@'%' 到底差在哪
3.1 MySQL 的账号是"用户名 + 来源主机"两个字段
这是整个问题的核心认知,也是最容易被忽略的一点:MySQL 里的用户标识不是一个字符串,而是用户名@主机的组合。'root'@'localhost'和'root'@'%'在系统表里是两条完全独立的记录,可以有不同的密码、不同的认证插件、不同的权限。你在本地改了 root 的密码,改的是'root'@'localhost'那条,远程那条纹丝不动。
查看当前有哪些账号:
SELECT user, host, plugin FROM mysql.user ORDER BY user, host;典型的默认输出是这样的:
+------------------+-----------+-----------------------+ | user | host | plugin | +------------------+-----------+-----------------------+ | mysql.infoschema | localhost | caching_sha2_password | | mysql.session | localhost | caching_sha2_password | | mysql.sys | localhost | caching_sha2_password | | root | localhost | caching_sha2_password | +------------------+-----------+-----------------------+看到没有?只有一个root@localhost。你从远程用 root 连,来源 IP 是192.168.1.50,MySQL 在表里找不到能匹配这个来源的 root 记录,直接回Host '192.168.1.50' is not allowed to connect。这不是密码问题,是账号根本不存在。
3.2 主机匹配的优先级规则,以及匿名用户那个大坑
理解了"两条独立记录",下一个问题就是:如果同时存在root@localhost、root@'192.168.1.%'、root@'%',MySQL 用哪一条?
规则是按主机匹配的具体程度排序,越具体的越优先。大致顺序是:字面主机名或完整 IP > 带通配符但前缀更长的(如192.168.1.%)> 纯%。所以从192.168.1.50连过来,会命中192.168.1.%那条而不是%那条。
这里有个历史上非常著名的坑:空字符串主机''。某些安装方式(尤其是通过打包脚本或旧版升级上来的)会在mysql.user里留下''@'localhost'、''@'%'这样的匿名账号。空主机名在匹配规则里的位置排在%之前,所以从远程连过来时,可能先被匿名账号匹配上,然后因为匿名账号没有权限而报Access denied for user ''@'192.168.1.50'——注意用户名部分是空的,这个特征非常明显。
处理办法很直接:
-- 先确认有没有匿名账号 SELECT user, host FROM mysql.user WHERE user = ''; -- 有就删掉 DROP USER IF EXISTS ''@'localhost'; DROP USER IF EXISTS ''@'%';另一个相关的坑是max_connect_errors。默认值不小,但如果你反复用错误配置试探,达到阈值后 MySQL 会把这个来源 IP 直接拉黑,之后即使配置全对也会报Host 'x.x.x.x' is blocked because of many connection errors。这时候执行一句FLUSH HOSTS;就能解封,不用重启服务。
3.3 正确创建远程账号的完整写法
想从远程连,规范做法是新建一个专用账号,而不是把root@'%'放出来。但如果你的场景确实需要 root 远程(比如内网测试环境),写法是这样:
-- 创建允许任意主机来源的 root 账号 CREATE USER 'root'@'%' IDENTIFIED BY 'YourStrongPwd_2024'; -- 授予全部权限,并且允许它继续给别人授权 GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' WITH GRANT OPTION; -- 查看结果 SELECT user, host, plugin FROM mysql.user WHERE user = 'root';这里有几个细节值得说明。
第一,CREATE USER和GRANT之后不需要FLUSH PRIVILEGES。网上大量教程会在末尾加一句FLUSH PRIVILEGES;,其实只有当你直接用INSERT/UPDATE改mysql.user表时才需要手动刷新权限缓存。用 DDL 语句操作,MySQL 会自动同步。加上去不会错,但会误导新人以为这是必须的。
第二,密码强度策略会拦你。8.0 默认装了validate_password组件,设弱密码会报:
ERROR 1819 (HY000): Your password does not satisfy the current policy requirements查看当前策略并临时放宽(仅限测试环境,生产别这么干):
SHOW VARIABLES LIKE 'validate_password%'; -- 8.0.4 之后是组件形式,变量名带点号 SET GLOBAL validate_password.policy = LOW; SET GLOBAL validate_password.length = 8;注意老教程里写的是validate_password_policy(下划线),那是 5.7 时代的写法,8.0 上执行会报变量不存在。这个差异坑过不少人。
第三,只想让某个网段连,就别写%。更精确的写法:
CREATE USER 'app_rw'@'192.168.1.%' IDENTIFIED BY 'AppPwd_2024#x'; GRANT SELECT, INSERT, UPDATE, DELETE ON app_db.* TO 'app_rw'@'192.168.1.%';这样即使密码泄露,攻击面也只在一个内网段里。
3.4 直接改 root@localhost 的主机字段行不行
有人会想:那我把'root'@'localhost'的 host 字段直接改成%不就行了?技术上可以:
UPDATE mysql.user SET host = '%' WHERE user = 'root' AND host = 'localhost'; FLUSH PRIVILEGES;但我强烈不建议。原因有两个:一是这么干之后本地 socket 连接可能反而出问题(因为root@localhost没了,本地连接也得走 TCP 匹配%),二是你失去了"本地一个高权限账号、远程一个受限账号"的分层结构。正确姿势是保留root@localhost不动,另建远程账号,这也是 MySQL 官方推荐的做法。
4. caching_sha2_password 认证插件:MySQL 8.0 最隐蔽的一道坎
4.1 认证插件换代的背景
MySQL 8.0.4 开始,默认认证插件从mysql_native_password换成了caching_sha2_password。换的原因不是心血来潮:老插件的密码哈希算法强度已经不够看了,新插件基于 SHA-256,安全性和抗暴力破解能力都高一截。
但它带来了兼容性问题。caching_sha2_password的握手流程更复杂,第一次连接时如果没有缓存,需要走一次完整的密钥交换(RSA 公钥加密密码,或者走 TLS 通道)。老版本客户端不认识这套流程,就会出现各种奇怪报错:
Authentication plugin 'caching_sha2_password' cannot be loadedPublic Key Retrieval is not allowed(Java 驱动常见)Client does not support authentication protocol requested by server
这个错误的特点是:账号、密码、权限全对,网络也通,就是连不上。很多人在这里卡住,反复重设密码也没用,因为问题不在密码本身。
4.2 三种解决思路的取舍
| 方案 | 做法 | 优点 | 代价 |
|---|---|---|---|
| 升级客户端 | 换新版 Navicat / DBeaver / JDBC 驱动 / PHP 扩展 | 安全性最好,不改服务端 | 老系统可能升不动 |
| 改账号认证插件 | ALTER USER ... IDENTIFIED WITH mysql_native_password | 见效最快,一行 SQL | 安全性下降,8.4 起该插件默认禁用 |
| 强制走 TLS | 服务端配置证书,客户端启用 SSL | 安全且兼容,绕开 RSA 交换限制 | 配置证书有工作量 |
我的建议顺序是:先试升级客户端,升不动再考虑改插件,改插件只改必要的那个业务账号,别动 root。因为mysql_native_password在 8.0.34 已经标记为废弃,8.4 版本默认不再加载,现在图省事改过去的账号,将来升级大版本时还得再改一遍。
4.3 具体命令与验证方式
改单个账号的认证插件:
ALTER USER 'app_rw'@'192.168.1.%' IDENTIFIED WITH mysql_native_password BY 'AppPwd_2024#x'; -- 确认改动生效 SELECT user, host, plugin FROM mysql.user WHERE user = 'app_rw';Java 应用如果用的是 8.0 之前的 JDBC 驱动,连接串里加上这两个参数通常能解决Public Key Retrieval is not allowed:
jdbc:mysql://10.0.0.5:3306/app_db?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai提示:
allowPublicKeyRetrieval=true的作用是允许客户端从服务端获取 RSA 公钥来加密密码。它意味着密码加密走的是明文信道上的非对称加密,安全性弱于 TLS,只建议在内网使用。公网环境请老老实实配 TLS。
如果你不想降级插件,也不想自签证书,还有一个折中办法:用 8.0 自带的方式生成自签证书并开启require_secure_transport,让所有远程连接强制加密。这个工作量比想象中小,mysql_ssl_rsa_setup会自动生成一套。但要注意,一旦开了require_secure_transport,本地都不允许明文连接,得确保所有客户端都支持 SSL,否则会把自己锁在外面。
5. 防火墙、安全组与 Docker 端口映射:网络层最后三关
5.1 主机防火墙放行 3306
监听改好了,账号也建了,还是连不上,接下来查主机防火墙。不同系统的操作差别不小:
# firewalld(CentOS / RHEL / Rocky) firewall-cmd --permanent --add-port=3306/tcp firewall-cmd --reload firewall-cmd --list-ports # 只放行指定网段更安全(rich rule 写法) firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="192.168.1.0/24" port protocol="tcp" port="3306" accept' firewall-cmd --reload # ufw(Ubuntu) ufw allow from 192.168.1.0/24 to any port 3306 proto tcp ufw status verbose用iptables直接写规则也行,但要记得持久化(iptables-save),否则重启就丢。另外注意,如果服务器上装了fail2ban之类的工具,它可能在你反复试错之后把客户端 IP 加进黑名单,报错表现为突然连不上、之前明明能用。排查时先看iptables -L -n完整链,别只看某个链。
5.2 云主机安全组:最容易漏的一层
如果机器是云服务器,除了系统防火墙,还有一层云平台的安全组,两者是独立的。很多人改完系统防火墙就以为完事,结果还是不通,原因就在安全组没放行。
安全组排查要点:
- 入方向规则里要有
TCP 3306,源地址写你的固定公网 IP 或内网网段,不要图省事写0.0.0.0/0。 - 出方向一般默认全放行,但有些严格环境下出方向也被限制,需要一并检查。
- 同一账号下的多台机器如果互相访问,走内网 IP 通常不受公网安全组限制,但受内网 ACL 限制。
判断是不是安全组问题,有个快速验证法:从云主机的浏览器 webshell 或跳板机上执行mysql -h 内网IP -P 3306。内网通、公网不通,基本就是安全组或公网映射的问题;内网也不通,就是监听或账号的问题。
5.3 Docker 部署 MySQL 8.0 时的三个典型错误
现在越来越多人用容器跑 MySQL 8.0,这一层的坑和裸机不太一样:
错误一:端口映射只绑定了回环地址。有些教程为了让数据库更安全,会写-p 127.0.0.1:3306:3306。这个写法的意思是宿主机上只有回环地址监听 3306,从外面当然连不上。想远程连必须写成-p 3306:3306或-p 0.0.0.0:3306:3306。
错误二:不知道容器里账号的默认状态。MySQL 官方镜像的启动脚本里,MYSQL_ROOT_HOST的默认值就是%,也就是说容器启动时会自动建一个root@'%'。所以用容器跑的时候,从宿主机之外连不上,大概率不是账号问题,而是端口映射或防火墙。想限制来源可以显式设置-e MYSQL_ROOT_HOST=172.17.0.1。
错误三:配置文件挂载覆盖了默认监听。有人把宿主机的my.cnf整个挂到/etc/mysql/my.cnf,而那份文件里写着bind-address = 127.0.0.1,结果容器内部只监听回环,端口映射出去也是死的。正确做法是只挂一个conf.d目录下的小文件:
docker run -d --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=RootPwd_2024 \ -v /data/mysql8/conf:/etc/mysql/conf.d \ -v /data/mysql8/data:/var/lib/mysql \ mysql:8.0还有一点必须提醒:Docker 会向 iptables 的DOCKER链里插规则,这些规则优先于 ufw 的规则。所以经常出现"ufw status显示 3306 是 deny,但外部偏偏能连上"的怪现象。要真正限制容器端口,得往DOCKER-USER链里加规则,光靠 ufw 是拦不住的。
6. 一次完整的排查实录:从 10061 到连接成功
6.1 现场信息收集:五条命令锁定层次
假设场景是这样的:一台内网 CentOS 服务器,MySQL 8.0 刚装好,客户端在 Windows 上,用 Navicat 连报由于目标计算机积极拒绝,无法连接。(10061)。我的固定排查顺序是这样:
第一步,客户端侧确认端口。先把 DNS、代理之类的干扰因素排除掉:
# Windows PowerShell Test-NetConnection 10.0.0.5 -Port 3306如果TcpTestSucceeded: False,说明 TCP 层就不通,继续往下;如果是True,直接跳到第 3 章的账号排查。
第二步,服务端确认监听。上服务器敲:
ss -lntp | grep 3306 mysql -h 127.0.0.1 -uroot -p -e "SELECT 1;"如果ss显示的是127.0.0.1:3306,问题定位完成——监听范围不对。如果本地命令行都连不上,那问题更基础,先看 mysqld 是不是还活着(systemctl status mysqld、tail -50 /var/log/mysqld.log)。
第三步,确认监听参数来源。找到实际生效的配置文件:
grep -rn "bind-address\|skip-networking" /etc/my.cnf /etc/my.cnf.d/第四步,看防火墙。
firewall-cmd --list-all第五步,账号授权表。这一步在监听修好之后再查:
SELECT user, host, plugin FROM mysql.user; SHOW VARIABLES LIKE 'bind_address';6.2 修复过程与验证
这个场景的根因通常就一个:bind-address = 127.0.0.1。修复动作是三步:
# 1. 改配置 sed -i 's/^bind-address.*/bind-address = 0.0.0.0/' /etc/my.cnf.d/mysql-server.cnf # 2. 重启(bind-address 不支持热加载) systemctl restart mysqld # 3. 确认监听范围变了 ss -lntp | grep 3306看到*:3306之后,再从客户端试一次。如果这时候报错从10061变成了Host '192.168.1.50' is not allowed to connect,恭喜你,说明网络层已经打通,问题收敛到了账号层,这是好事——从"什么都可能"变成了"只剩一件事"。
接着建账号:
CREATE USER 'app_rw'@'192.168.1.%' IDENTIFIED BY 'AppPwd_2024#x'; GRANT SELECT, INSERT, UPDATE, DELETE ON app_db.* TO 'app_rw'@'192.168.1.%';再从客户端连,如果又冒出Authentication plugin 'caching_sha2_password' cannot be loaded,说明客户端版本老,按第 4 章处理。
整个链路走完,从 10061 到连接成功,实际上可能只改了两行配置、执行了两条 SQL。真正花时间的是判断现在卡在哪一层。
6.3 修好之后仍然偶发失败的两类原因
有些情况是"明明配好了,偶尔又连不上",这类问题比一次性配错更烦人:
一是连接数被打满。报错变成Too many connections。查SHOW VARIABLES LIKE 'max_connections';和SHOW STATUS LIKE 'Threads_connected';。如果是连接池配置不当导致连接泄漏,改服务端参数只是治标,得回头查应用侧的连接池。
二是wait_timeout导致的空闲断连。默认wait_timeout是 28800 秒,但如果被人改小,连接池里的空闲连接会被服务端单方面关闭,应用拿到一个已死的连接去执行 SQL,报MySQL server has gone away或者Communications link failure。这类问题的特征是"第一次请求必失败、重试就成功"。解决办法是客户端连接池配置心跳检测(如 HikariCP 的keepaliveTime要小于服务端wait_timeout)。
7. 长期做法:别用 root 远程,账号与安全的最小必要配置
7.1 按业务拆账号的授权模板
把root@'%'放出去,等于把保险柜钥匙挂在门把手上。生产环境的账号应该按"谁用、从哪来、干什么"三个维度拆开。我常用的模板是这样:
-- 只读账号,给报表和数据分析用 CREATE USER 'report_ro'@'192.168.1.%' IDENTIFIED BY 'RoPwd_2024#a'; GRANT SELECT ON app_db.* TO 'report_ro'@'192.168.1.%'; -- 应用读写账号,给服务端程序用 CREATE USER 'app_rw'@'192.168.1.%' IDENTIFIED BY 'RwPwd_2024#b'; GRANT SELECT, INSERT, UPDATE, DELETE ON app_db.* TO 'app_rw'@'192.168.1.%'; -- 运维账号,只在跳板机段可用 CREATE USER 'ops_admin'@'192.168.1.10' IDENTIFIED BY 'OpsPwd_2024#c'; GRANT ALL PRIVILEGES ON *.* TO 'ops_admin'@'192.168.1.10' WITH GRANT OPTION;配置完用SHOW GRANTS FOR 'app_rw'@'192.168.1.%';逐个复核一遍。这里有个实操心得:授权时优先授库级权限,不要授全局权限。GRANT SELECT ON app_db.*和GRANT SELECT ON *.*看起来只差一个字符,出事时的爆炸半径差好几个数量级。特别是FILE权限,有了它能读写服务器上任意文件,非必要绝对不能给。
7.2 加固清单
如果这台库要长期对外提供服务,这几件事建议一次性做完:
| 加固项 | 具体做法 | 说明 |
|---|---|---|
| 换端口 | port = 13306 | 降低被批量扫描命中的概率,不是安全措施,是降噪 |
| 限制来源 | 账号 host 写具体网段 | 比%精确得多 |
| 强制加密 | require_secure_transport = ON | 所有连接走 TLS |
| 关闭本地文件导入 | local_infile = OFF | 防LOAD DATA LOCAL类攻击 |
| 开启慢查询日志 | slow_query_log = ON | 排查性能问题的基础 |
| 定期审计账号 | 脚本化对比mysql.user | 及时发现新增的高权限账号 |
关于端口,我得说句实话:改端口只能挡住自动化扫描,挡不住针对性的探测。真正有价值的是"限制来源 + 强制加密"这两条。
7.3 几个我实际踩过的细节
最后分享几个文档里不写、但实操中一定会碰到的点。
第一,用 SSH 端口转发比直接暴露 3306 省事得多。如果你能 SSH 上那台服务器,直接本地执行:
ssh -N -L 13306:127.0.0.1:3306 user@10.0.0.5然后客户端连127.0.0.1:13306就行了。这样 MySQL 完全不用对外开端口,bind-address保持127.0.0.1也没关系,安全性和便利性兼顾。用 VS Code 远程连服务器开发的同学,也可以在 SSH 配置里加LocalForward 13306 127.0.0.1:3306,连上服务器后本地自动就能连数据库了,比开安全组规则干净得多。
第二,注意区分127.0.0.1和localhost在客户端侧的含义。有些客户端里,localhost会走本地 socket(unix socket)而不是 TCP,尤其在 macOS 和 Linux 上。你想测试远程连接,连接主机一定要写明确的 IP,写localhost可能测的是本地另一个 MySQL,结果自己骗自己。
第三,云厂商的"数据库白名单"和服务器安全组是两回事。如果你用的是托管数据库服务,控制台里还有一个独立的访问白名单,加 IP 的地方可能藏得很深。服务器安全组放行了、数据库白名单没加,照样连不上,报错同样是超时。
第四,密码里的特殊字符会要命。命令行里mysql -uapp_rw -p'Pa$$w0rd'会因为 shell 展开$而变成另一个密码,导致Access denied。用单引号包起来可以避免大部分问题,但更稳妥的是交互式输入密码,或者用mysql_config_editor生成的.mylogin.cnf加密登录路径。我见过有人因为密码里有个!被 bash 历史展开成上一条命令,排查了半小时。