MySQL 8.0 远程连接:网络、授权与认证插件全链路排查
2026/9/18 20:52:43 网站建设 项目流程

本地敲mysql -uroot -p一切正常,换到另一台机器上用客户端连,立马翻脸不认人——报错还五花八门:Host '192.168.1.20' is not allowed to connect to this MySQL serverCan't connect to MySQL server on '10.0.0.5' (10061)由于目标计算机积极拒绝,无法连接Authentication plugin 'caching_sha2_password' cannot be loaded。这几个错误长得像亲戚,其实分属完全不同的层次,处理顺序搞反了就会在一件事上耗掉整个下午。MySQL 8.0 远程连接之所以年年有人问,就是因为它同时压在四层上:网络监听、主机防火墙、账号授权(root@localhostroot@%的区别就在这一层)、认证插件。任何一层没打通,现象都是"连不上",但修法完全不同。

这篇东西写给三类人:刚在云服务器或虚拟机里装完 MySQL 8.0、想让本地客户端连上去的开发者;用 Docker 起 MySQL 8.0 之后发现端口映射怎么都不通的运维新手;以及被Access denied10061反复折磨、只想快速定位到底是哪一层出问题的人。下面按"先分类、再逐层拆、最后给完整排查链路"的顺序来,每一层都会告诉你为什么这么设计、命令为什么这么敲,而不是丢一堆 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 allowedAccess denied for user 'root'@'172.16.3.88'—— 网络层是通的,问题在账号授权,直接跳到第 3 章。
  • 报错是10061积极拒绝timed out2003—— 网络层就不通,别去翻权限表,先查监听和防火墙,看第 2 章和第 5 章。

我见过太多人在第二种情况下疯狂改mysql.user表,加了三个root@'%'还是连不上,最后发现是安全组没放行 3306。方向错了,做多少都是无用功。

1.2 常见报错文本对应的故障层

把常见报错和它真正指向的层次列出来,遇到问题先查表,能省掉一半时间:

报错文本含义优先排查层
Host 'x.x.x.x' is not allowed to connectTCP 已通,来源主机被拒账号授权层(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 errorsmax_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,那不用往下猜了,先改配置。如果看到的已经是*:33060.0.0.0:3306,说明监听没问题,去查防火墙和账号。

ssnetstat好用的地方在于它不依赖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 reloadmysqladmin 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@localhostroot@'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 USERGRANT之后不需要FLUSH PRIVILEGES网上大量教程会在末尾加一句FLUSH PRIVILEGES;,其实只有当你直接用INSERT/UPDATEmysql.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 loaded
  • Public 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 mysqldtail -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 = OFFLOAD 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.1localhost在客户端侧的含义。有些客户端里,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 历史展开成上一条命令,排查了半小时。

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

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

立即咨询