MySQL 使用者身份由 User + Host + Password + Privileges 四元組唯一確定,而非單純的用戶名。本文揭示分步建立用戶再授權導致無法登入的根因,並給出正確姿勢。
-- 步驟 1:建立用戶(指定密碼)
CREATE USER 'app'@'localhost' IDENTIFIED BY 'secret123';
-- 步驟 2:授權(未指定密碼)
GRANT ALL ON `mydb`.* TO 'app'@'localhost';
-- 結果:用 app/secret123 登入失敗!
-- ERROR 1045 (28000): Access denied for user 'app'@'localhost'
MySQL mysql.user 表主鍵為 (Host, User),但**認證資訊(Password)與權限是分開儲存**的:
CREATE USER 在 mysql.user 插入一行,寫入 User、Host、authentication_string(密碼雜湊)。GRANT ... TO 'user'@'host' **若不帶 IDENTIFIED BY**,會在權限表 (mysql.db, mysql.tables_priv 等) 寫入權限記錄,**但不更新 mysql.user 的密碼欄位**。mysql.user 已存在,**再次 GRANT 不帶密碼不會報錯,也不會更新密碼**,導致權限表有記錄但認證表密碼為舊值或空,登入失敗。-- 建議:GRANT 同時建立用戶、設密碼、授權
GRANT ALL ON `mydb`.* TO 'app'@'localhost' IDENTIFIED BY 'secret123';
-- MySQL 5.7+ 等價於:
-- CREATE USER 'app'@'localhost' IDENTIFIED BY 'secret123';
-- GRANT ALL ON `mydb`.* TO 'app'@'localhost';
-- FLUSH PRIVILEGES;
-- 8.0+ 語法分離(推薦)
CREATE USER 'app'@'localhost' IDENTIFIED BY 'secret123';
GRANT ALL ON `mydb`.* TO 'app'@'localhost';
-- 查看認證資訊
SELECT `User`, `Host`, `plugin`, `authentication_string`
FROM `mysql`.`user` WHERE `User` = 'app';
-- 查看權限
SHOW GRANTS FOR 'app'@'localhost';
-- 實際登入測試
mysql -u app -p'secret123' -h 127.0.0.1 -P 3306
| Host 值 | 匹配來源 | 備註 |
|---|---|---|
'localhost' | Unix Socket / Named Pipe | 本機連線,不走 TCP |
'127.0.0.1' | TCP 回環 | 本機 TCP 連線 |
'::1' | IPv6 回環 | IPv6 環境需求 |
'%' | 任意遠端 IP | 含本機 TCP(但不含 localhost Socket) |
'192.168.1.%' | 特定網段 | 支援通配符 |
💡 **最佳實踐**:為同一應用建立三組帳號 'app'@'localhost'、'app'@'127.0.0.1'、'app'@'%'(或僅必要網段),密碼一致,權限一致,覆蓋所有連線方式。
| 誤區 | 正確認知 |
|---|---|
| 用戶名唯一 | (User, Host) 組合唯一 |
| 密碼屬於用戶 | 密碼屬於 (User, Host) 組合 |
| GRANT 會自動建用戶 | 5.7+ 預設 NO_AUTO_CREATE_USER,GRANT 不建用戶 |
| 修改密碼用 UPDATE mysql.user | ❌ 危險,請用 ALTER USER 或 SET PASSWORD |
| FLUSH PRIVILEGES 必須手動執行 | 5.7+ GRANT/ALTER USER 隱含刷新,但手動執行無害 |