只改create user的host字段远远不够,必须同步配置bind-address为内网ip或0.0.0.0、防火墙放行对应网段、启用skip_name_resolve=on,四者缺一不可,否则ip限制形同虚设。

直接说结论:只改 CREATE USER 的 host 字段(比如 'user'@'192.168.5.%')远远不够,必须同步收紧 bind-address、防火墙和 DNS 解析行为,三者缺一不可。
CREATE USER 的 host 必须写成网段通配形式,不能用 %
MySQL 不支持 CIDR(如 '192.168.5.0/24'),只能靠 % 做前缀字符串匹配。写错格式会静默失败或权限不生效:
-
'user'@'192.168.5.%'✅ 匹配 192.168.5.1–192.168.5.255(注意点号是字面量,不能省略) -
'user'@'192.168.5.*'❌*不是 MySQL 通配符,语法报错 -
'user'@'192.168.5.0/24'❌ 报ERROR 1290或直接拒绝解析 -
'user'@'%'❌ 允许任意 IP 连接,包括外网,等同于没限制
创建时必须显式带引号、带点号、带 %,且 GRANT 中的 TO 后也要完全一致——漏一个字符,就新建一条 @'%' 记录,后患无穷。
FLUSH PRIVILEGES 不是万能的,DROP 冗余账号才是关键
常见现象:明明写了 'app'@'192.168.5.%',但从外网还能连上。根本原因不是权限没刷,而是残留了更宽泛的账号:
- 执行
SELECT User, Host FROM mysql.user WHERE User = 'app';,如果返回两行:'app'@'192.168.5.%'和'app'@'%',那客户端大概率会命中后者(MySQL 权限匹配按Host字符串长度由长到短排序,但实际行为受插入顺序和缓存影响,最稳解法是删掉宽泛项) - 必须手动清理:
DROP USER 'app'@'%'; -
FLUSH PRIVILEGES;只在修改mysql.user表直连后才必要;用CREATE USER/GRANT语句创建,它只是保险动作,不执行有时也生效,但建议保留
bind-address 和防火墙不配齐,MySQL 层限制等于摆设
只设 'user'@'192.168.5.%',但 bind-address = 0.0.0.0 且防火墙全开,外网照样能发 TCP 包进来,然后被 MySQL 拒绝——这暴露了错误信息(Access denied for user ...@'x.x.x.x'),反而帮攻击者确认账号存在。
- 查当前监听:
SELECT @@bind_address;,值应为'192.168.5.100'(具体内网 IP)或'0.0.0.0'(配合防火墙);若为'127.0.0.1',则所有远程连接都失败,哪怕 IP 匹配 - Linux 下确认监听状态:
ss -tlnp | grep :3306,输出中应含你期望的内网 IP - 防火墙必须同步放行:
sudo ufw allow from 192.168.5.0/24 to any port 3306(ufw)或firewall-cmd --add-rich-rule='rule family="ipv4" source address="192.168.5.0/24" port port="3306" protocol="tcp" accept'(firewalld)
skip_name_resolve=ON 是防止 DNS 反解失配的隐性开关
默认 skip_name_resolve = OFF,MySQL 会对客户端 IP 做反向 DNS 查询。一旦 DNS 配置异常(比如内网没配反向解析记录),IP 可能被解析成 unknown-host-192-168-5-100.example.com,结果匹配不到你定义的 'user'@'192.168.5.%',转而尝试 'user'@'%' 或直接拒绝。
- 在
[mysqld]段加配置:skip_name_resolve = ON - 重启 MySQL 生效(
systemctl restart mysqld) - 加完之后,
host字段将严格按原始 IP 字符串匹配,不再依赖 DNS —— 这是让网段限制真正可靠的关键一环
真正起作用的从来不是单条 SQL,而是 CREATE USER + bind-address + 防火墙规则 + skip_name_resolve 四者严丝合缝。少一个,就可能在某次网络变更、DNS 故障或安全扫描中露出破绽。











