☰
MySQL 1130报错排查:Host not allowed远程连接被拒的完整解决方案
2026/10/1 18:00:17 网站建设 项目流程

1. 先搞懂这条报错卡在了连接流程的第几步

1.1 握手过程中MySQL为什么把你拒之门外

多数人第一次在Navicat、DBeaver或者命令行里看到"Host is not allowed to connect to this MySQL server"时,第一反应是检查网络、检查防火墙、检查端口通不通。但我要先把结论放在前面:看到这条报错,TCP连接已经通了,MySQL服务器已经收到你的握手请求,甚至已经识别出你的来源IP了。它是在授权表里找不到匹配记录,才把你拒掉的。

MySQL远程连接的完整流程大致是这样:客户端发起TCP连接,服务器accept之后开始MySQL协议握手,紧接着做两件事——先用Host和User去mysql.user表里找匹配记录,找不到就直接返回错误;找到记录之后才校验密码。你看到的这条报错,错误码是1130,对应的就是第一阶段被拒,即"主机白名单里没有你"。

所以当你看到这个报错时,请先放下防火墙排查手册。TCP三次握手已经完成,服务器已经在应用层和你对话了。问题出在MySQL自身的访问控制上——mysql.user表里没有一条记录能在Host列上匹配你当前连接所用的IP或主机名。

1.2 最容易踩中这个报错的几个真实场景

我最近几年处理这种报错,几乎都集中在下面几类场景上:

  • 新装MySQL后直接远程连root。这是最典型的。无论是Linux用rpm装,还是Windows的msi安装包,默认情况下MySQL只会为root@localhost创建账号,目的就是防远程暴力破解。你拿着root去远程连,只要没有手动授权过,必然报1130。
  • Docker容器里的MySQL,创建用户时Host写死了容器IP。很多人用docker run启动MySQL之后,再进容器里创建一个用户,Host写成了类似172.17.0.2这种容器地址。可你在宿主机上连接时,来源IP并不是容器的IP,而是经Docker NAT转换后的地址,于是同样报Host not allowed。
  • 从生产库导出数据到本地开发库,账号体系没同步。生产库里授权了'appuser'@'10.20.30.%',本地环境却是从另一台机器连过来的,本地库的授权表里自然没有这个来源,报错也跟着来了。
  • 用了内部域名做Host授权,但客户端实际通过IP连接。比如你授权了'user'@'db-client.internal',可客户端配置连接的是192.168.1.50,MySQL在解析时没有匹配到对应关系,也会拒你。

这些场景背后有一个共通的判断方法:先去查mysql.user表,看看目标用户有哪些Host范围,再对比你实际连接来源,基本就能锁定问题。

1.3 与"Access denied"这类报错的分界线

新手很容易把几个报错混在一起,我在这上面接过不少"半个小时的排查最后发现是自己密码错了"的求助。这里做一个简单的区分:

报错特征错误码含义
Host is not allowed to connect to this MySQL server1130授权表里没有匹配的Host记录,密码环节还没走到
Access denied for user 'xxx'@'xxx' (using password: YES)1045Host匹配上了,但密码不对,或者密码插件不兼容
Can't connect to MySQL server on 'xxx' (timed out)2003TCP层都没通,通常是防火墙、bind-address或端口映射问题
Unknown MySQL server host2005DNS解析失败,直接把域名解析绕开更省事

分清这四条线之后,你面对这条报错时心里就有底了:这不是网络故障,也不是密码问题,就是授权表的问题。接下来要做的就是沿着授权这条线一步步排查,把"到底是谁在什么主机上、被拒绝了"这件事看清楚。

2. 排查链路:从网络层一路确认到授权表

2.1 第一步:确认MySQL真的在端口上等你

虽然我前面说"看到这条报错说明TCP已经通了",但严谨起见,还是先做一次连通性确认。毕竟有些时候你看到的报错是应用层包装过的中文提示,可能把别的错误混进来。

在客户端机器上执行:

telnet mysql-server-ip 3306

或者用更直观的方式:

nc -vz mysql-server-ip 3306

如果端口通,你会看到类似Connected to mysql-server-ip的反馈。如果超时,那说明连接根本没到MySQL进程,这时候才需要回头看防火墙、安全组、Docker端口映射和bind-address。

MySQL监听地址也是个常见坑。用下面这条命令看服务器监听的到底是0.0.0.0还是只有本机回环地址:

SHOW VARIABLES LIKE 'bind_address';

如果是127.0.0.1,那无论授权表怎么写,远程都连不进来。需要修改配置文件为0.0.0.0并重启MySQL。不过你要注意,如果已经看到了1130报错,说明bind-address这块大概率没问题,因为服务器已经响应你了。

2.2 第二步:直接查mysql.user表里到底有没有你的位置

这是整个排查链路里最关键的一步,比什么工具都管用。先在MySQL服务器本机登录:

mysql -uroot -p

然后执行:

SELECT user, host, plugin FROM mysql.user;

你会看到类似下面的输出:

+------------------+-----------+-----------------------+ | user | host | plugin | +------------------+-----------+-----------------------+ | root | localhost | caching_sha2_password | | mysql.session | localhost | caching_sha2_password | | myapp | 10.0.0.% | caching_sha2_password | +------------------+-----------+-----------------------+

这里的关键就是看你想用的那个账号,在Host列上有没有能覆盖你源IP的记录。假设你从192.168.1.100连过来,账号是root,表里只有root@localhost,那就毫无悬念地会收到1130。主机名匹配规则不只是精确匹配,MySQL还支持通配符,比如%表示任意主机,192.168.1.%表示匹配网段,web01.example.com表示匹配特定主机名。

还有一个常用的确认手段,在服务器本机执行SELECT CURRENT_USER();和SELECT USER();,分别查看当前连接实际生效的账号,以及你当前来源IP对应的账号视角。这能帮你判断,"我明明授权了%,为什么还是连不上"这类诡异问题。

2.3 第三步:别忽略bind-address和DNS反向解析

如果user表里已经有合适的授权,但还是报1130,那问题多半出在host匹配的参数上。下面两个变量需要重点看:

SHOW VARIABLES LIKE 'skip_name_resolve'; SHOW VARIABLES LIKE 'bind_address';

skip-name-resolve这个变量很有意思。默认情况下它是OFF,意味着MySQL会对每个连接做DNS反向解析:把客户端IP反向解析成主机名,再拿主机名去和授权表的Host列比对。如果你的授权表里写的是'user'@'localhost'或者'user'@'some-hostname',但客户端通过IP连接,且反向解析失败或解析出的主机名对不上,那就会拒绝连接。

解决思路有两个方向:一是把授权表里的Host改成IP或IP段;二是在配置里加上skip-name-resolve=ON,让MySQL跳过反解,直接拿IP地址做匹配。后者在互联网环境里能显著降低每次连接的握手延迟,但代价是授权表里就只能用IP写Host了,写主机名会失效。

这一层的坑比较隐蔽,因为表面上你确实授权了,甚至授权的是%,但配合DNS反解,实际匹配逻辑可能完全不是你想象的那样。我建议生产环境直接开启skip-name-resolve,把授权全部收敛到IP维度,少一个变量就少一类事故。

3. 三种修复方法,按场景选而不是瞎试

3.1 用GRANT授权:最推荐,但要按版本区分写法

绝大多数情况下,解决1130的正确做法是给指定用户补一条Host授权。注意MySQL 5.7和8.0在语法上有明显差异。

MySQL 5.7及更早版本,可以用简洁的单条语句,创建用户的同时授权:

GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY 'YourPassword' WITH GRANT OPTION; FLUSH PRIVILEGES;

MySQL 8.0之后,GRANT语句不再支持隐式建用户。你需要分两步:

CREATE USER 'root'@'%' IDENTIFIED BY 'YourPassword'; GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' WITH GRANT OPTION; FLUSH PRIVILEGES;

如果你只是想给特定库授权,而不是放开全部权限,可以把*.*替换成mydb.*,这样更安全。实际工作中我强烈建议不要直接放开root远程,而是创建一个专用账号,只授予需要访问的库:

CREATE USER 'devuser'@'192.168.1.%' IDENTIFIED BY 'StrongPassword'; GRANT SELECT, INSERT, UPDATE, DELETE ON myapp.* TO 'devuser'@'192.168.1.%'; FLUSH PRIVILEGES;

FLUSH PRIVILEGES这条很多人会纠结有没有必要。我的理解是:执行GRANT/CREATE USER时,MySQL会直接更新授权表并同步刷新内存里的授权缓存,理论上不执行FLUSH也能生效;但如果你是通过UPDATE mysql.user这种方式手工改表,那就必须执行FLUSH PRIVILEGES,否则新连接不会重新加载授权数据。稳妥起见,改完授权我都会习惯性执行一次,代价几乎为零。

3.2 手动改mysql.user表:老办法的边界在哪里

有些老资料会教你直接改表:

UPDATE mysql.user SET host = '%' WHERE user = 'root'; FLUSH PRIVILEGES;

这个做法在5.x时代很常见,但在8.0里我不建议作为首选。原因有两个:一是mysql.user表的字段在不同版本有差异,直接改表绕过了MySQL自身的权限变更日志,容易漏掉一些关联表的同步;二是升级到8.0后,直接改mysql.user表如果有语法或字段理解错误,可能导致整个认证体系出问题。

不过它有一个适用场景:当你需要批量调整某个用户的所有Host记录时,UPDATE确实比逐条GRANT高效。比如你要把某个账号从只允许某个IP改成允许整个网段,可以写:

UPDATE mysql.user SET host = '192.168.1.%' WHERE user = 'myapp'; FLUSH PRIVILEGES;

改完之后务必检查一下结果,确认没有把别的账号误伤:

SELECT user, host FROM mysql.user WHERE user = 'myapp';

另外提醒一句,如果表里同一个用户同时存在'myapp'@'localhost'和'myapp'@'%'两条记录,这是很正常的。MySQL在匹配连接时会按精确度排序,localhost或具体IP会优先于%匹配,并不会冲突。你不用特意删掉某一条。

3.3 全部放开root的"%"授权:能救急但有代价

时间紧任务重,很多人的第一反应是把root直接授权成%。我理解这种救急操作,但我要负责任地说清楚:这等于把你的数据库大门钥匙挂在了门口。

'root'@'%'意味着任何来源IP,只要能猜对密码,就能以最高权限登录。一旦数据库放在了有公网IP的机器上,扫描工具分分钟就能探测到3306端口,然后就是持续的暴力破解尝试。我见过太多因为这种救急操作导致的数据安全事故,很多还是在客户环境里。

如果确实需要远程管理,正确姿势是:

CREATE USER 'admin'@'办公网固定IP' IDENTIFIED BY '复杂密码'; GRANT ALL PRIVILEGES ON *.* TO 'admin'@'办公网固定IP' WITH GRANT OPTION; FLUSH PRIVILEGES;

把来源IP限定到你的办公网出口IP,或至少限定到某个可信网段,比如10.0.0.0/8。这样即使密码泄露,攻击者也必须来自可信网络,风险等级完全不一样。

如果是云服务器环境,还有一个必须同步检查的点:云平台的安全组规则是否允许了0.0.0.0/0访问3306端口。安全组和MySQL授权表是两道独立的门,两道门都应该只对可信IP开放,哪个都不能图省事。

4. 修复之后更容易翻车的几个地方,我替你们先踩了

4.1 Docker端口映射后,来源IP却不是你以为的那个IP

使用Docker部署MySQL的场景,1130报错的成因往往比裸机部署更绕。很多人进入容器创建用户时,看到自己用的是172.17.0.3,就顺手把Host写成了这个IP。结果在宿主机上用localhost或宿主机IP去连接,还是被拒。

原因在于Docker的网络模型。当你用-p 3306:3306映射端口时,宿主机上的客户端连接经过Docker的iptables转发,到达容器内的MySQL时,来源IP已经变成了Docker网桥的网关地址(通常是172.17.0.1),而不是客户端真正的IP。所以你在容器里授权'user'@'172.17.0.3',永远等不到这个来源。

解决方式有两类:

  • 在授权时把来源写成Docker给宿主机分配的网关地址,也就是'user'@'172.17.0.1'。
  • 更推荐的做法是:在宿主机上使用--network host方式启动MySQL容器,或者通过环境变量、初始化SQL从一开始就把账号授权好。初始化SQL可以放在容器的/docker-entrypoint-initdb.d/目录下,MySQL首次启动时会自动执行,这样账号体系从出生就是对的。

Docker场景有个核心原则:授权表里的Host,永远以MySQL进程实际看到的来源IP为准,而不是你的直觉。拿不准的时候,可以在授权表里先建一条'user'@'%',接一次连接,执行SELECT USER();看返回结果,再收敛成具体IP。

4.2 skip-name-resolve会对授权表产生连锁影响

我在前面提过skip-name-resolve,这里单独展开一下,因为它是一个非常容易"改一处、炸一片"的参数。

当skip_name_resolve=OFF(默认)时,MySQL会对每个TCP连接做反向DNS解析。如果反向解析超时,连接建立耗时可能高达好几秒,这种情况下你会观察到"能连但非常慢"的怪现象。而当skip_name_resolve=ON时,MySQL不再反解主机名,这时授权表里凡是Host列写主机名的记录全部失效,只有IP和%有效。

我遇到过一起现场排障,开发反馈"我明明授权了但连不上",排查半天发现是运维在优化配置时开了skip_name_resolve,而授权表里清一色写的是内网机器名。这种问题从报错上完全看不出区别,唯一办法就是对比配置变更记录和授权表内容。

如果你决定要开skip_name_resolve,我的建议是规范化授权表,把所有的Host列统一改成IP地址或CIDR网段,彻底放弃主机名匹配。这一步做完之后,连接速度和排障清晰度都会有明显提升。

4.3 MySQL 8认证插件让一些老客户端莫名失败

这条严格来说不会报1130,但它经常出现在同一个排障流程里,让人误以为还是授权问题。MySQL 8默认的认证插件是caching_sha2_password,而很多老版本的图形客户端,比如某些旧版Navicat、老版JDBC驱动,只支持mysql_native_password。你授权也对了、密码也对了,客户端却报"Authentication plugin 'caching_sha2_password' cannot be loaded"这类错误。

解决办法通常是把该用户的认证插件切回旧版:

ALTER USER 'myapp'@'192.168.1.%' IDENTIFIED WITH mysql_native_password BY 'YourPassword'; FLUSH PRIVILEGES;

不过这只是兼容性过渡方案。新项目我建议优先升级客户端驱动到支持caching_sha2_password的版本,因为新版插件在安全性上明显更强。实在要兼容老旧客户端,也可以用上面的命令单独处理某个账号,别把整库的默认认证方式改掉。

还有一种情况值得提一下:MySQL 8.0.34版本开始弃用了mysql_native_password插件,未来版本可能直接移除。如果你维护的是比较新的8.0版本,尽量别再用这个兼容插件兜底,能升级客户端就升级客户端。

4.4 授权表的"你以为是"和"实际生效"之间的差距

有时候你以为自己已经授权了,实际上MySQL选中的是另一条记录。授权表匹配有个优先级规则,越精确的Host反而越会被优先匹配到。比如你同时有'user'@'192.168.1.%'和'user'@'%'两条记录,来自192.168.1.50的连接会命中前者,而不是后者。如果前者的密码和后者的密码设置不一致,你拿后者的密码去连,就会莫名其妙被拒绝。

遇到这种情况,用SELECT CURRENT_USER();看实际生效的账号名,能直接看出到底落到了哪条记录上。另外要养成习惯:一个账号尽量只维护一条Host记录,要么具体IP,要么网段,要么%,避免多条记录互相干扰。

5. 授权策略的长期维护建议:少让后人替你背锅

5.1 设计授权时,把"最小够用"当成默认准则

排查过太多1130之后,我现在的习惯是:第一次创建账号时就想清楚这个账号将来会在哪里被使用。给应用服务器用的账号,Host就写那台应用服务器的IP;给开发同事用的账号,Host就写办公网出口IP;给自动化运维脚本用的,再单独开一个只读账号,Host限定到跳板机。

举例说明,一套典型的电商系统数据库授权看起来像这样:

| user | host | privileges | |---------------|-----------------|--------------------------------| | app_rw | 192.168.1.10 | SELECT, INSERT, UPDATE, DELETE | | app_readonly | 192.168.1.% | SELECT | | admin_op | 10.0.0.5 | ALL PRIVILEGES |

每个账号的用途和来源都清晰可查,后人接手的时候不需要猜。你在授权表里省下的每一分钟思考,都会在将来某次故障排查时十倍偿还。

5.2 定期review授权表,别让账号堆积成隐患

线上环境跑得久了,mysql.user表里很容易堆出一堆过期账号。比如某个外包同事走了,他的账号还在;某个项目下线了,它的数据库账号还在。这些账号本身就是风险敞口。

我的习惯是每季度做一次授权表检查,主要看三件事:

  1. 有没有Host列为%且权限是ALL PRIVILEGES的高危账号
  2. 有没有超过半年没用过但还活着的账号
  3. 有没有已经不属于当前业务架构的库和账号

检查方法也简单,直接查用户表,再对照业务资料逐个确认。确认无用的账号执行DROP USER 'xxx'@'xxx';,别只删一半。

5.3 把授权变更纳入规范化操作流程

最早我改授权都是业务方喊一声,我随手一条GRANT就发过去了,事后经常忘记录。后来吃过亏,现在所有授权变更都走统一的SQL脚本,留档备查。每个新增账号的脚本大概长这样:

-- 变更说明:电商应用新服务器加入,需要访问订单库 CREATE USER 'order_app'@'10.0.8.25' IDENTIFIED BY 'StrongPassword'; GRANT SELECT, INSERT, UPDATE, DELETE ON `order_db`.* TO 'order_app'@'10.0.8.25'; FLUSH PRIVILEGES;

脚本文件按日期和用途命名,放在专门的目录里。这样下次有人说"我这边连不上数据库",我能先翻脚本确认账号是否存在、来源IP对不对,比直接去线上瞎猜快得多。

另外再分享一个小技巧:写授权语句时,把Host字段里的IP段用引号括起来,虽然不括也不会报错,但标准化之后整个SQL脚本看起来整齐很多,出问题也更容易一眼看出来。

关于1130这个报错,我这些年最深的体会是:它对新手像一堵墙,对老手只是一个信号。信号背后的核心永远只有一条——MySQL的授权表里没有你的位置。搞清楚你在以什么身份从哪来、该被授予什么权限,然后按最小够用的原则补上授权,这个报错就会彻底变成你日常工具箱里最顺手的一个工具。

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

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

立即咨询