MySQL将'user'@'localhost'和'user'@'%'视为两个独立用户,因权限系统以user+host为复合主键,二者认证、权限、密码完全隔离;查用SELECT USER(), CURRENT_USER()确认实际匹配项,删用户须显式指定host,GRANT时host精度决定匹配优先级。

MySQL 为什么认为 'u'@'localhost' 和 'u'@'%' 是两个用户
MySQL 的权限系统把 user 和 host 当作一个不可分割的复合主键,不是“用户名相同就共享权限”。'u'@'localhost' 只能从本地 socket 或 -h 127.0.0.1 连入,'u'@'%' 才响应远程 IP;两者密码、认证插件、权限列表完全独立。很多“本地能连、远程连不上”或“远程能连、本地报 1045”的问题,根源就是误以为它们是同一个账号。
查清当前到底匹配了哪个 user@host
别猜,直接看 MySQL 实际用了谁:
-
SELECT USER(), CURRENT_USER();——USER()是你“说”的身份(含客户端传的 host),CURRENT_USER()才是 MySQL 真正查表匹配并用于鉴权的那个user@host -
SELECT User, Host FROM mysql.user WHERE User = 'u';—— 看所有残留记录,尤其注意localhost、127.0.0.1、%、host.docker.internal这类值是否都存在 - 如果
CURRENT_USER()返回'u'@'localhost',但你是用公网 IP 连的,说明客户端解析或连接参数触发了回环路径,不是权限没开,而是根本没走到%那条规则
删错一条就卡住:DROP USER 必须显式带 host
DROP USER 'u'; 在 MySQL 8.0+ 中只删 'u'@'localhost'(历史兼容行为),其他 host 下同名账号全留着。再执行 CREATE USER 'u'@'%'... 就会报 ERROR 1396,因为系统发现 'u' 这个名字已在别的 host 上注册过。
- 先查全量:
SELECT User, Host FROM mysql.user WHERE User = 'u'; - 再逐个删:
DROP USER 'u'@'localhost', 'u'@'%', 'u'@'192.168.1.100';(把上一步结果全部列进去) - 验证清空:
SELECT User, Host FROM mysql.user WHERE User = 'u';应返回空集 - 严禁
DELETE FROM mysql.user—— 绕过权限系统清理逻辑,后续可能认证失败或权限不加载
GRANT 时 host 写法直接影响匹配优先级
MySQL 匹配 user@host 是按最长前缀精确匹配,不是模糊搜索。顺序是:具体 IP > 网段(如 192.168.1.%) > % > 空字符串。哪怕你只建了 'u'@'%',只要客户端连上来时被解析成 'u'@'host.docker.internal',而表里没有这条记录,就会拒绝,不会自动 fallback 到 %。
- 生产环境避免无脑用
'u'@'%',优先考虑'u'@'10.0.2.%'或'u'@'192.168.1.100' - 用
GRANT创建比INSERT INTO mysql.user安全——它自动处理plugin、authentication_string字段,且在 binlog 中可复制(主从同步权限必需) - MySQL 8.0+ 默认插件是
caching_sha2_password,老客户端(如 PyMySQL ≤1.0、Navicat ≤15.0)不支持,建户后立刻加一句:ALTER USER 'u'@'%' IDENTIFIED WITH mysql_native_password BY 'pwd';
user 和 host 是联合键,以及没检查 host 维度的残留记录。多执行一次 SELECT User, Host FROM mysql.user WHERE User = 'u';,比反复试错快得多。


















