装完MySQL之后第一件事应该做什么?不是把root密码一改就万事大吉,而是立刻把用户体系梳理清楚。我见过太多项目把root账号直接丢给业务代码用,也见过不少刚入行的同学卡在CREATE USER这条命令上,死活创建不出能连上的账号——要么报错语法不对,要么授权完还是Access denied。这个"基础"里其实藏着不少坑,今天就把MySQL创建用户的完整逻辑、实操命令和排错思路一次讲透,适合刚装完MySQL不知道下一步干什么的运维,也适合写代码时总被数据库连不上折腾的后端。
1. 先搞懂用户怎么被认出来的:user@host双层身份
很多人对MySQL用户的理解停留在"用户名+密码",所以创建用户时只想着CREATE USER 'u1' IDENTIFIED BY 'pwd',却忽略了MySQL判断用户身份用的其实是两个维度:user和host。MySQL官方文档里把用户写成账户名@主机名,这个格式在创建授权和排错时是绕不开的。
1.1 host字段为什么这么重要
host字段表示"允许从哪个客户端IP或主机名登录"。同样的用户名,配不同的host,完全就是两个独立账号。比如:
CREATE USER 'demo'@'localhost' IDENTIFIED BY 'pwd123'; CREATE USER 'demo'@'192.168.1.%' IDENTIFIED BY 'pwd456';这两条命令创建的是两个不同的账号,虽然用户名都叫demo,但密码、权限、登录来源彼此完全独立。localhost那个只能从MySQL服务器本机连进来,192.168.1.%那个允许从192.168.1网段的机器远程连。如果你在本机登录时只匹配到localhost账号,就算远程账号密码再怎么对,用错端口或跳过匹配规则照样连不上。
生活里可以这样理解:用户名相当于你的名字,host相当于你登记的住址。银行开户时"身份证号+姓名"才是唯一标识,MySQL里"user+host"才是唯一标识。
1.2 权限表的前世今生:user、db、tables_priv、columns_priv
MySQL的账号权限是分层次存储的,创建了用户,接下来要搞清楚它会被哪些表约束。系统库里最核心的几张权限表是:
| 权限表 | 权限级别 | 说明 |
|---|---|---|
mysql.user | 全局权限 | 只要在表里出现的权限,对所有库生效 |
mysql.db | 库级权限 | 限定某用户对某个库的操作权限 |
mysql.tables_priv | 表级权限 | 限定某用户对某张表的权限 |
mysql.columns_priv | 列级权限 | 精确到字段级别的权限 |
mysql.procs_priv | 存储过程权限 | 对存储过程、函数的执行权限 |
判断一个请求能不能执行时,MySQL会先从mysql.user看有没有全局权限,然后查mysql.db,再到mysql.tables_priv,最后才是mysql.columns_priv。权限越靠前优先级越高,全局授权后其他表里的限制对它作用就非常小了。
这个机制直接解释了开发过程中常见的诡异现象:你在全局把UPDATE权限给了用户,但某张表还是报权限不足;或者反过来,你只给了单表权限,结果用户能看到的数据库列表依然空空如也。美团这类大厂里通常按库和表精确授权,避免全局撒网。
1.3 host匹配的隐藏排序规则
MySQL在客户端发起连接后,会按一定顺序去mysql.user表里匹配记录。localhost、127.0.0.1、::1、主机名、无通配符的IP地址,这些"精确记录"会被优先匹配;之后才轮到包涵通配符的记录,比如'192.168.1.%'、'%'。
这个匹配顺序会带来一个很经典的坑:同一用户名下同时存在'demo'@'localhost'和'demo'@'%'两个账号时,在本机通过socket或localhost连接会命中前者,通过IP连接会命中后者。如果你不小心给前者设置了错误密码,后者密码正确,反过来连127.0.0.1就成功,连localhost就失败,特别容易把人绕晕。
理解了这一层,后面排查Access denied时你就会知道:先看匹配的是哪个host记录,再核对密码,而不是盯着CREATE USER语句反复发呆。
2. 一条CREATE USER命令能做的事
真正理解语法逻辑之后,CREATE USER其实非常简单,难点在于写对选项和夺回细节控制权。
CREATE USER [IF NOT EXISTS] '用户名'@'host' IDENTIFIED BY '初始密码' [WITH mysql_native_password / caching_sha2_password] [REQUIRE SSL] [PASSWORD EXPIRE INTERVAL 90 DAY];IF NOT EXISTS会在用户已存在时给出告警而不是直接报错,脚本里重跑很实用。IDENTIFIED BY指定认证密码,密码会以哈希形式存到mysql.user表的authentication_string字段,没人能反向看到明文。
WITH子句可以指定认证插件。MySQL 8.0默认使用caching_sha2_password,安全性更高,但某些老客户端和旧版中间件不认识它。如果被老工具困扰,需要在创建时单独指定mysql_native_password。这个细节我已经数不清帮多少人解决过"Navicat突然连不上"的问题。
关于host的写法,常见的模板是:
-- 本机维护账号 CREATE USER 'admin'@'localhost' IDENTIFIED BY 'Tp@2024#Xy'; -- 指定网段的应用账号 CREATE USER 'app'@'192.168.1.%' IDENTIFIED BY 'App@2024#Secure'; -- 所有来源可登录(能不用就尽量别用) CREATE USER 'ops'@'%' IDENTIFIED BY 'Ops@2024#Secure';生产环境里%我建议少用甚至禁用,业务账号一律按服务器网段限制,运维账号按办公室出口IP限制。host写得太宽,等于是把数据库大门钥匙复制了好几把,出问题时攻击面也大。允许范围越小,登录出问题时反而越好排查——不然你连日志里那一串来源IP都不知道该信谁。
如果你想创建的用户需要跨多个来源登录,可以创建多个同用户名账号,分别配不同host,比如'backup'@'10.0.0.%'和'backup'@'localhost'。两张记录互不干扰,一个用于远程备份服务,一个用于本机脚本。
MySQL 8.0.16起还支持CREATE USER ... PASSWORD EXPIRE,强制用户在指定天数后改密:
CREATE USER 'temp'@'localhost' IDENTIFIED BY 'Temp@2024#Xy' PASSWORD EXPIRE INTERVAL 30 DAY;这个在做临时账号和外包协作场景时特别有用。30天一到,不换密码就登录不了,到期后帮你省掉一堆"怎么还不删除临时账号"的催收电话。
3. GRANT授权的新手盲区
用户创建出来只是一个空壳,不给权限什么都干不了。授权最常见的命令是GRANT,但也是踩坑最密集的地方。
3.1 授权语法和权限级别
GRANT 权限列表 ON 权限级别对象 TO '用户'@'host';权限列表可以是一个权限,也可以是逗号分隔的多个权限,比如SELECT, INSERT, UPDATE, DELETE。最常用的几种写法:
-- 全局级别:能管理所有库 GRANT ALL PRIVILEGES ON *.* TO 'admin'@'localhost'; -- 库级别:管理某个库 GRANT ALL PRIVILEGES ON mydb.* TO 'app'@'192.168.1.%'; -- 表级别:只读某张表 GRANT SELECT ON mydb.orders TO 'analyst'@'%'; -- 列级别:只能看某些列 GRANT SELECT (name, email) ON mydb.customers TO 'analyst'@'%';3.2 最小权限原则怎么落地
讲道理谁都会,实际授权时新手最常见的操作是给ALL PRIVILEGES一键到位。我自己的建议是:业务账号永远不给ALL,根据实际工作拆开给。
| 账号类型 | 推荐授权 | 使用场景 |
|---|---|---|
| 只读报表账号 | SELECT | BI报表、数据分析、定时导出 |
| 业务读写账号 | SELECT, INSERT, UPDATE, DELETE | 后端业务服务 |
| 管理维护账号 | ALL PRIVILEGES | DBA / 本机运维操作 |
| 备份账号 | SELECT, LOCK TABLES, SHOW VIEW | 逻辑备份、物理备份前的锁表 |
有人会问:后端服务要跑事务和联表查询,只给CRUD够吗?够。如果你在服务里执行CREATE TABLE、DROP TABLE之类的DDL,那说明设计架构有问题——建表应该走发布流程,而不是业务代码顺手执行。
有个我踩过的坑,刚接手项目时图省事给读写账号加了GRANT ALL ON mydb.*,结果有一次线上排查误删了一张日志表,万幸有备份。从那以后所有业务账号我都严格收敛为CRUD,DDL权限走专门的高权限账号。
3.3 5.7到8.0的隐式创建行为变化
这是很多旧教程挖的坑,在MySQL 5.7及之前版本,你可以直接用:
GRANT SELECT ON mydb.* TO 'xiaoming'@'%' IDENTIFIED BY 'somepassword';它允许GRANT连带创建用户,还不限制必须有CREATE USER权限。但在MySQL 8.0之后这个用法直接被禁用——必须先CREATE USER再GRANT,否则直接语法报错。
所以如果你搜到的是老资料或者习惯了5.7的写法,迁移到8.0后就会卡在这:明明一模一样的两条命令,在5.7跑得好好的,在8.0却报错。解决办法很简单,就是补一条CREATE USER:
CREATE USER 'xiaoming'@'%' IDENTIFIED BY 'somepassword'; GRANT SELECT ON mydb.* TO 'xiaoming'@'%';3.4 FLUSH PRIVILEGES到底什么时候要执行
可能你在很多教程里见过授权后要加一句FLUSH PRIVILEGES;,但实际上这句命令不是每次都要执行。它存在的意义是让MySQL重新读取授权表,仅在直接操作mysql.user、mysql.db这些系统表时才有必要。通过标准的CREATE USER、GRANT、REVOKE语句修改权限,MySQL会自动生效,不需要FLUSH。
老运维的习惯来自于早期一些版本直接改表后必须刷新的操作经验,沿用至今。日常授权流程里加上它没有什么危害,却容易让你误以为授权有问题时全靠FLUSH解决,反而忽略了真正的授权语句错误。
3.5 REVOKE回收权限的正确姿势
权限给出去,改完后一门心思收藏,结果回收时又出错。REVOKE的纯粹和GRANT对应:
REVOKE INSERT, UPDATE ON mydb.* FROM 'app'@'192.168.1.%'; REVOKE ALL PRIVILEGES ON mydb.* FROM 'app'@'192.168.1.%';但要讲清楚:REVOKE删除的是授权权限,不会删除用户本身。想连用户带权限一起清理,要用DROP USER。权限回收后已建立的连接仍然持有之前的权限,直到连接断开或重启;对于在线回收连接权限的需求,需要KILL相关连接进程,这在实际生产操作时经常被忽略。
我当时处理过一个报表账号需求,先给了它临时ALL PRIVILEGES,几天后改成了SELECT,结果报表服务一直没重连,还把UPDATE跑通了,看起来像是权限没撤回。重启进程后一切恢复正常。
4. 连接不上的时候,九个排查方向
创建了用户、授权也做了,然后拿Navicat一连接——啊,又报错了。这类问题占了数据库日常排错的半壁江山,我把高频坑全部列出,方便你直接按图索骥。
4.1 Access denied for user
这个错误最能说明host匹配问题。完整报错通常长这样:
ERROR 1045 (28000): Access denied for user 'demo'@'192.168.1.50' (using password: YES)关键信息是报错里显示的来源host是你客户端的真实IP,而不是你写错的那个host通配符。如果报错里出现的IP不在你创建用户的host范围内,那就说明MySQL根本没有匹配到该账号。
按三步排查:
# 1. 在MySQL服务器上查看账号和host SELECT user, host, authentication_string FROM mysql.user WHERE user='demo'; # 2. 确认当前客户端实际来源IP -- 登录后执行 SELECT CURRENT_USER(); SELECT SUBSTRING_INDEX(HOST, ':', 1) AS client_ip FROM information_schema.processlist WHERE ID=CONNECTION_ID();如果CURRENT_USER()返回的host和你建立的账号host对不上,就是匹配问题。干脆创建两条覆盖所需来源的账号,或者收敛所有业务连接到一个统一的host段。
4.2 caching_sha2_password带来的客户端兼容问题
MySQL 8.0把默认认证插件改成了caching_sha2_password。如果你的Navicat版本较旧、驱动太老或连接池中间件不支持这个插件,即使密码完全正确也会报认证失败。
解决方案有三种:
- 升级客户端驱动到支持caching_sha2_password的版本,长期最优。
- 临时把账号认证方式改回
mysql_native_password,兼容旧客户端:
ALTER USER 'demo'@'%' IDENTIFIED WITH mysql_native_password BY 'Demo@2024#Xy';- 创建用户时就指定老认证插件:
CREATE USER 'demo'@'%' IDENTIFIED WITH mysql_native_password BY 'Demo@2024#Xy';只提醒一点:不要把整个mysqld的默认认证插件都改掉,不然新账号默认都走老插件,安全性会拉低。
4.3 密码里的特殊字符和大小写
密码含!、@、#、%这类字符时,如果写在脚本或连接串里没做转义,会出现一个让你怀疑人生的结果:MySQL里存的密码对,程序里连不上。用命令行连接时和程序连接时,对特殊字符的解释规则不同。Shell命令行里单引号包住连接串能规避大半问题,但程序代码里还会涉及URL编码。
经验做法:业务系统连接账号的密码尽量避免使用需要转义的高风险字符,否则光在配置文件里折腾转义就能耗掉半天。比如Demo2024#Xy这种中等强度的密码就够用了,不必非得上!@#$%^&*全家桶。
4.4 建了账号但是授权还没生效
很多新手在一个会话里建用户、授权,然后在另一个会话里测试连接,发现没有权限。这不是授权没生效,而是你测试用的会话在用户创建之前就已经完成了身份验证。重新建立连接后再试即可。
还有一种类似情况是改了账号host,比如从'demo'@'localhost'改成'demo'@'%',已建立的旧连接依然按老host权限运行,需要重启连接或杀掉对应线程。
4.5 bind-address限制导致远程连不上
这个和用户创建关系不大,但实在太多人栽在这:MySQL默认监听地址可能是127.0.0.1,只允许本机连接。你的账号host写了'%'也没用,因为服务器从TCP层就把外部连接拒绝了。
-- 查看监听地址 SHOW VARIABLES LIKE 'bind_address';如果值是127.0.0.1,需要改配置文件my.cnf的bind-address = 0.0.0.0(或者指定内网IP),然后重启MySQL。改了监听后记得同步防火墙放行3306端口,iptables/安全组/ufw三层都要检查,我见过只改了配置文件但被云安全组挡着连不上的。
4.6 skip-name-resolve带来的host匹配异常
skip_name_resolve开启时,MySQL不反向解析客户端域名,这个机制本身无害,但会连带影响权限匹配——你创建用户时写的host如果用的是主机名而非IP,匹配就失效。开了这个参数后,host字段里的'myhost'这种写法就形同虚设,必须写IP地址或通配符。
如果你遇到过"本机localhost能连,局域网IP连不上,但账号host明明写了%",查一查skip_name_resolve和mysql.user里的host写法,大概率是主机名和IP两种风格混着用了。
4.7 连接串把host写成本机名而非IP
连接时用的是socket还是TCP/IP,很多初学者分不清。MySQL客户端连接localhost时默认走Unix socket(Linux下为/tmp/mysql.sock),连接127.0.0.1时才走TCP/IP 3306端口。用户host只授权给'demo'@'localhost'时,你用mysql -h 127.0.0.1连接,会命中另一条匹配规则。
一个比较省心的做法:应用和MySQL在同一台机器时就统一用127.0.0.1连接,配合授权'app'@'127.0.0.1';注意不要用localhost和127.0.0.1混着配,分分钟怀疑是密码错了。
4.8 SSL连接和useSSL参数
MySQL 8.0默认也开启了REQUIRE SSL的选项,但实际生产环境大量使用非SSL连接。如果创建用户时使用了REQUIRE SSL语句,应用连接串里必须显式开启SSL,否则连接直接被拒绝。
Java的JDBC连接串里有一个高频坑:useSSL=false和sslmode=DISABLED各自代表不同版本的参数体系。MySQL Connector/J 8.x版本的sslmode换成DISABLED,老写法是useSSL=false;如果两边参数不一致,会出现"服务端是8.0要求SSL,客户端老代码没正确配置SSL参数"这种连环炸。
我维护过一个Java项目,升级驱动版本后突然报SSL握手失败,改一行sslmode=DISABLED就恢复了。这类问题表面上是"数据库连接失败",根子上其实是MySQL 8.0安全策略升级导致的兼容性问题。
4.9 授权范围过窄导致"数据库列表为空"
有时候用户能连上MySQL,但Navicat左侧看不到任何数据库,或只能看到一个系统库。这不是连接失败,纯属MySQL对SHOW DATABASES返回内容的过滤:用户对某库没有任何权限时,该库根本不出现在列表中。
如果是你刚授权的库没显示,检查授权是否漏了该库,或者二度连接一下(有些GUI工具缓存了数据库列表)。如果确实没权限,就该补授权:
GRANT USAGE ON mydb.* TO 'demo'@'%';USAGE这个权限本身不带任何操作能力,但能让用户"看得到"这个库,这对需要允许用户浏览库结构但不给改动权限的场景很有用。
5. 后续管理:改密、锁号、删号,一个都不能少
创建用户只是开始。MySQL用户的生命周期管理里,改密码、锁定异常账号、精准删除,这三件事是运维和开发都要会的。
5.1 ALTER USER改密码与改认证方式
改密码最常用的语句是:
ALTER USER 'demo'@'%' IDENTIFIED BY 'NewPwd@2024#Secure';5.7.6之前的旧版本写法是SET PASSWORD FOR,现在ALTER USER是标准姿势。批量改密时可以配合脚本循环处理多个账号,但注意密码策略——MySQL默认会校验密码强度,如果密码太弱会被validate_password插件弹回。
如果一个账号的认证方式不对,也可以直接改:
ALTER USER 'demo'@'%' IDENTIFIED WITH caching_sha2_password BY 'NewPwd@2024#Secure';5.2 锁定与解锁账号
员工离职、外包项目结束、疑似账号被盗,临时锁号是好选择,而不是立刻删除。
-- 锁定 ALTER USER 'demo'@'%' ACCOUNT LOCK; -- 解锁 ALTER USER 'demo'@'%' ACCOUNT UNLOCK;锁定的账号尝试登录时返回错误:
ERROR 3118 (HY000): Access denied for user 'demo'@'%'. Account is locked.已经建立的连接不受锁定影响,需要额外KILL断开敏感会话。
5.3 RENAME USER的适用场景
用户名变更保持权限不变时:
RENAME USER 'oldname'@'%' TO 'newname'@'%';注意host也可以一起改。这个命令在账号迁移时非常顺滑——先建新账号迁移数据,等业务切换后删除旧账号,比直接改账号风险低很多。
5.4 DROP USER删除账号
DROP USER 'demo'@'%'; DROP USER IF EXISTS 'demo'@'%';MySQL 8.0后DROP USER不会自动回收该账号对其他库对象的授权,而是把它归到匿名用户。所以删除前最好先查一遍SHOW GRANTS,确定没有残留对象依赖。这里我踩过一次坑:删了一个报表账号,结果一张存储过程里还绑定它的DEFINER,后面调用全报错,只能临时重建账号才恢复。
5.5 SHOW GRANTS查权限一击必中
SHOW GRANTS FOR 'demo'@'%';返回结果里每个授权逐条列出,权限排查第一步就靠它。想查看当前登录用户自己的权限就用SHOW GRANTS FOR CURRENT_USER();,避免猜错host写错查看对象。
5.6 安全基线:给用户体系定规矩
最后给一份我个人在团队里强制执行的用户安全清单,供你参考:
- 所有业务账号不给全局
ALL,只给所属库的CRUD。 - 远程管理账号一律
REQUIRE SSL,从明文网络上掐死密码泄露面。 - 每90天强制轮换密码,业务账号用发布流程统一改密并重启服务。
- 任何账号权限变更后复核一遍
SHOW GRANTS,并在变更记录里更新。 - 临时账号明确到期时间,用
PASSWORD EXPIRE做硬约束。 - 离职/转岗人员涉及的账号锁定后,保留15天再删除,方便追溯历史操作。
- 账号名和用途挂勾:
app_前缀、ro_只读、etl_数据抽取,一看名字就知道该账号的权限边界。
我的经验是,把创建用户的流程录成自动化脚本,所有账号生成的SQL都走Git版本管理——谁申请、为什么申请、授权范围是什么,全部留痕。数据库出问题被追责时,这套流程能帮你省掉很多口水。
从CREATE USER一条语法聊到权限体系、排错思路,说白了,MySQL用户管理就是两件事:让该进来的人顺畅干活,让不该进来的人彻底挡在门外。基础的东西扎扎实实搞明白,后面做高可用、分库分表、审计这些进阶事务时,才不会在权限地基上翻车。