mysql用户是'user'@'host'联合标识,非单一用户名;host匹配按最长前缀优先精确比对,如192.168.1.105优先匹配'192.168.1.105'而非'%';%仅替代单段,不支持跨段或cidr计算;'localhost'与'127.0.0.1'互不等价;current_user()返回实际匹配项,user()仅为客户端声明。

MySQL用户不是“用户名”,而是'user'@'host'这个完整字符串
很多人以为CREATE USER 'app'就建了个叫app的用户,其实它等价于'app'@'%'——MySQL 5.7+ 默认补全host为%,而%不匹配localhost(Unix socket连接场景)。这意味着你用mysql -u app -h 127.0.0.1能连上,但mysql -u app(默认走socket)会失败,因为后者实际匹配的是'app'@'localhost',而这张记录根本不存在。
真正起作用的是user和host两个字段组成的联合键,它们在mysql.user表里构成唯一索引。哪怕只改其中一个字段,比如把'app'@'192.168.1.100'改成'app'@'192.168.1.%',就是一条全新记录,旧权限不会继承过去。
Host字段匹配不是“模糊查找”,而是最长前缀优先的精确比对
MySQL查mysql.user表时,并不按SQL WHERE语句那样逐行扫描,而是先按host字段做“最长前缀匹配”:比如客户端IP是192.168.1.105,它会依次尝试匹配:
-
'app'@'192.168.1.105'(完全相等,最高优先级) -
'app'@'192.168.1.%'(%只占一个段,次之) -
'app'@'%'(最宽泛,最低优先级)
注意:'app'@'192.168.%'不会匹配192.168.1.105,因为%只能替代一个“段”,不能跨段;'app'@'192.168.1.0/24'在MySQL 8.0+虽支持CIDR语法创建,但它仍是完整字符串,不是子网计算,匹配时仍按字面值比对。
常见陷阱:
-
'app'@'localhost'和'app'@'127.0.0.1'完全独立,互不影响 -
'app'@'%.example.com'要求DNS可解析,且必须字面一致,不能靠反向DNS自动转换 -
CURRENT_USER()返回值才是真实匹配依据,USER()只是客户端声明的身份,两者常不一致
为什么直接UPDATE mysql.user表大概率出错
手动执行UPDATE mysql.user SET Host='10.244.1.%' WHERE User='app' AND Host='10.244.1.15';看似简单,但容易忽略三件事:
- 已有更高优先级记录(如
'root'@'localhost')可能拦截匹配,导致新记录根本没机会被选中 - MySQL 8.0+ 的
authentication_string字段依赖认证插件(如caching_sha2_password),直接UPDATE可能让密码字段与插件不兼容,引发ERROR 2059 - 改完必须执行
FLUSH PRIVILEGES,否则缓存未刷新,变更不生效
更稳妥的做法是用DROP USER 'app'@'10.244.1.15';再CREATE USER 'app'@'10.244.1.%' IDENTIFIED WITH caching_sha2_password BY 'xxx';重建,让MySQL内部自动维护字段一致性。
生产环境该用IP还是%?关键看网络是否确定
用%图省事,但风险极高:一旦应用被迁到公网或容器网络变动,就等于开放了所有入口。真正安全的做法是按部署拓扑选host值:
- Kubernetes Pod固定CIDR(如
10.244.0.0/16)→用'app'@'10.244.%.%'或分段写死(如'app'@'10.244.1.%') - Docker Compose →显式创建
'app'@'host.docker.internal',别指望%自动覆盖 - 云服务器内网IP稳定 →直接写
'app'@'10.0.1.23',比通配符更可控
最容易被忽略的一点:MySQL的host匹配发生在TCP握手之后、认证之前,它不依赖DNS解析结果,也不做任何网络层校验——只要客户端声称自己来自某个IP或主机名,MySQL就按那个字符串去查表。所以host字段本质是“信任声明”,不是“网络验证”。











