mysql登录时匹配排序后最前面的user表记录,按host从精确(localhost、ip)到模糊(%、空字符串)排序,同host下再按user从非空到空排序。

MySQL登录时到底匹配哪一行user表记录?
不是看有没有%,而是看排序后哪一行排最前面。MySQL把mysql.user表全读进内存,按Host字段从精确到模糊排序:localhost、127.0.0.1、192.168.1.100这类具体值优先;192.168.1.%次之;%排最后;空字符串''比%还靠后。
同Host值下再按User排序:非空用户名优先,空User(即匿名用户)排最后。
- 本地执行
mysql -ufoo默认走host=localhost,哪怕你只建了'foo'@'%',只要'foo'@'localhost'存在(哪怕密码为空),就会命中它 -
SELECT USER(), CURRENT_USER();结果不同——USER()返回你“声称”的身份,CURRENT_USER()返回实际匹配的user@host条目 - 用
SELECT host,user,authentication_string FROM mysql.user ORDER BY host ASC, user DESC;能直观看到排序顺序,但注意ASC对host是反直觉的:因为localhost 在字典序里成立,而MySQL内部排序逻辑是“越具体越靠前”,所以真实排序需用<code>ORDER BY LENGTH(host) DESC, host DESC近似模拟
为什么%在本地连不上,但远程可以?
根本原因不是%“不包括”localhost,而是localhost这一行在匹配排序中赢了%。常见现场:
- 你执行过
CREATE USER 'foo'@'localhost';(没设密码),又执行CREATE USER 'foo'@'%';(设了密码)——本地连时命中localhost行,因authentication_string为空,提示Access denied - 你只建了
'foo'@'%',但MySQL安装时自带的'root'@'localhost'或''@'localhost'(匿名用户)依然存在,它们会拦截所有未显式指定-h的本地连接 -
mysql -ufoo -p -h127.0.0.1能通,但mysql -ufoo -p失败——因为前者强制走TCP/IP协议匹配127.0.0.1或%,后者走socket协议,只认localhost
删掉localhost行就能一劳永逸?
不能直接删,容易锁死自己。MySQL要求至少有一个root或等效权限用户能通过localhost登录,否则重装都救不回来。
- 安全做法是:先用
mysql -uroot -p -h127.0.0.1连上,再执行DROP USER 'foo'@'localhost';,而不是用socket连 - 如果只剩
'foo'@'localhost'且密码为空,可用mysqld --skip-grant-tables启动跳过权限检查,再更新authentication_string字段 - 更稳妥的是保留
'foo'@'localhost',但确保它的authentication_string和'foo'@'%'一致:UPDATE mysql.user SET authentication_string=(SELECT authentication_string FROM mysql.user WHERE user='foo' AND host='%') WHERE user='foo' AND host='localhost'; FLUSH PRIVILEGES;
新建用户时怎么避开这个坑?
别依赖%覆盖所有场景,明确声明你需要的访问路径。
- 如果真需要本地+远程统一账号,建两个:
CREATE USER 'foo'@'localhost' IDENTIFIED BY 'xxx';和CREATE USER 'foo'@'%' IDENTIFIED BY 'xxx';,密码必须相同 - 开发环境慎用
'root'@'%'——它会被'root'@'localhost'压制,且暴露高危账户 - 用
host字段做最小化控制:比如内网服务就写'app'@'10.0.0.%',比%安全,也避免和localhost冲突 - 检查现有用户用:
SELECT user, host, account_locked FROM mysql.user WHERE user = 'foo';,一眼看出是否存在多行干扰
真正麻烦的从来不是%本身,而是它和localhost共存时那套隐式排序逻辑——看不见,摸不着,但每次连接都严格执行。











