☰
MySQL创建用户与授权全攻略:账号存储、权限层级与踩坑指南
2026/10/15 4:59:55 网站建设 项目流程

做MySQL的人,几乎没有不跟账号打交道的,但真能把"创建用户并授权"这件事一次讲透的内容其实不多。原因很简单:这套东西平时用得少,命令又分散,网上资料大多是几条语句的简单罗列,很少把背后的逻辑讲清楚。结果就是你照着抄了一遍,换台服务器、换个MySQL版本、换个客户端驱动,照样报错。我最早接手权限管理的时候,也是靠着一行行试错才把整个体系弄明白的。这篇文章就把MySQL创建用户和授权从头到尾拆开讲,覆盖账号存储逻辑、CREATE USER语法、GRANT授权、权限查看回收,以及我实际排障中总结出来的坑。适合刚接触MySQL、想规范账号管理的开发者,也适合被各种权限报错折磨过、想彻底搞清楚原理的运维同学。

1. 用户账号不是"用户名+密码"那么简单

1.1 账号的真实身份由"用户名@主机"共同决定

很多人第一次查mysql.user表会愣一下:为什么我明明只创建了一个用户,表里却出现两行?因为MySQL的账号身份是由User和Host两个字段共同确定的。'zhang'@'localhost'和'zhang'@'192.168.1.%'是两个完全独立的账号,虽然登录名都叫zhang,但密码、权限、甚至能否登录都是分开管理的。

Host字段决定了这个账号可以从哪里连过来。'localhost'表示只能通过本机连接;'192.168.1.%'表示允许该网段内任意主机;'%'表示不限制来源。这里有个非常实际的坑:很多云服务器上,'localhost'和'127.0.0.1'在某些连接方式下表现还不一样,因为服务端对socket连接和TCP连接的处理逻辑有区别。工作中最常见的报错场景是:程序在服务器本机用localhost能连上,一改成公网IP就连不上,大概率就是账号的Host只授权了localhost,没授权%。

所以创建账号前,先想清楚一个问题:这个账号是谁在用、从哪里用。部署在应用服务器上的程序,Host写应用服务器的内网IP或网段,而不是偷懒写%;只有确实无法固定来源的场景,才考虑用通配符。

1.2 认证和授权是两套独立的体系

MySQL对账号的校验分两步。第一步是连接阶段的身份认证:检查用户是否存在、密码是否正确、来源主机是否在允许列表里,这三项全部通过才允许建立连接。第二步是操作阶段的权限检查:连接建立后,你执行的每一条SQL都会拿当前账号去匹配权限表,看看这个账号到底有没有对应的操作权限。

理解这两步分离非常重要。很多人看到Access denied for user 'xxx'@'xxx' (using password: YES)就认定是密码错了,实际上这个报错既可能是密码错,也可能是Host不匹配,也可能是账号根本不存在。不同的失败原因,排查方向完全不同。判断思路我放到后面"实战踩坑"章节专门讲。

1.3 权限数据到底存在哪几张表

MySQL的权限不是一堆散落的配置,而是规范地存在几个系统表里。最核心的是mysql.user,它保存账号的全局权限,也就是不限定具体库的操作权限。其次是mysql.db,保存数据库级别的权限。更细一级是mysql.tables_priv,保存表级别的权限;再往下还有mysql.columns_priv,控制到列的权限;存储过程的权限在mysql.procs_priv。

权限检查的时候,MySQL会把这几层的权限做叠加。也就是说,一个账号在mysql.user里有全局SELECT权限,在mysql.db里又有对某个库的INSERT权限,那么这个账号在看这个库时,既能看到数据,也能插入数据。理解这个叠加逻辑,对后面排查"为什么我授权了还是没权限""为什么我回收了权限还是能操作"这类问题很有帮助——问题往往出在你以为改对了层级,实际改的是另一张表的内容。

2. 创建用户:语法、密码与常见选择

2.1 CREATE USER 的基本语法与参数

创建用户的标准写法是:

CREATE USER 'app_user'@'localhost' IDENTIFIED BY 'Strong@Passw0rd';

这条语句做的事情是:在mysql.user表里插入一条记录,User字段为app_user,Host字段为localhost,密码经过加密后存放在authentication_string字段(5.7及之前是password字段)。

这里有个细节值得说:MySQL 8.0之后,CREATE USER还支持IF NOT EXISTS:

CREATE USER IF NOT EXISTS 'app_user'@'localhost' IDENTIFIED BY 'Strong@Passw0rd';

这在写初始化脚本时非常有用。如果你在自动化脚本里重复执行用户创建,没有IF NOT EXISTS会直接报错中断,有了它则能保证幂等。

密码这个事多说一句。MySQL默认开启了密码复杂度校验组件(validate_password)时,太简单的密码会直接被拒绝,比如纯数字、纯字母、过短的密码都会报错。这不是bug,是安全策略。如果你在测试环境确实想临时用简单密码,可以通过调整validate_password.policy参数来放宽,但生产环境强烈建议用强密码,且密码里不要带上业务名、公司名这类容易猜到的内容。

2.2 密码认证插件:MySQL 8 与 5.7 的差异

这是创建用户时最隐蔽、也最容易导致"连接失败"的差异点。MySQL 8.0默认的认证插件是caching_sha2_password,而MySQL 5.7及更早版本默认是mysql_native_password。如果你用比较旧的语言驱动或者客户端工具去连接MySQL 8.0,很可能会报类似Authentication plugin 'caching_sha2_password' cannot be loaded的错误。

原因是旧客户端不认识这个新插件。解决办法有两个方向:要么升级客户端驱动到支持caching_sha2的版本,要么把账号的认证插件改回旧版,例如:

ALTER USER 'app_user'@'localhost' IDENTIFIED WITH mysql_native_password BY 'Strong@Passw0rd';

我的建议是:新项目优先用新驱动,直接默认caching_sha2_password;老项目如果驱动版本动不了,再改认证插件。因为caching_sha2_password的加密验证机制更安全,而且MySQL官方后续版本对旧插件的支持只会越来越少。改插件属于"治标",升级驱动才是"治本"。

还有一个细节:caching_sha2_password在首次认证、缓存未命中的情况下,需要安全连接(SSL)或者RSA公钥交换来传输密码。有些老客户端配置不当,会在第一次连接时报错,但第二次连接又莫名其妙成功了,这就是缓存命中与未命中的区别。遇到这种情况,优先排查连接是否支持安全传输,而不是无脑改认证插件。

2.3 创建用户的三种典型场景

创建用户不是一句话的事,而是要结合场景选参数。我实际工作中最常碰到的就三类:

本地管理账号:

CREATE USER 'admin_local'@'localhost' IDENTIFIED BY 'Admin@Passw0rd';

这种账号只允许在服务器本机登录,用于DBA或日常运维,不允许远程访问。Host限制是这类型账号最重要的安全边界。

内网应用账号:

CREATE USER 'app'@'192.168.10.%' IDENTIFIED BY 'App@Passw0rd';

限制来源网段到应用服务器所在的内网段。即使密码泄露,外部网络也无法直接连。

远程只读账号:

CREATE USER 'readonly'@'%' IDENTIFIED BY 'Read@Only1';

这里Host用了%,但要留意:%不代表就是安全的,它意味着任意来源都可尝试连接,所以这类账号的密码强度必须更高,权限必须严格控制,通常只给SELECT。能用具体IP就尽量别用%,这是我在一次线上事故后才深刻理解的教训。

3. 授权:从全局到单表的精确控制

3.1 GRANT 语句的结构与常用权限

创建用户之后,新账号默认是没有权限的。你用它登录可以,但它什么都干不了。授权的基本语句是:

GRANT SELECT, INSERT, UPDATE, DELETE ON appdb.* TO 'app_user'@'localhost';

这段语句的意思是:给app_user@localhost这个账号授予对appdb库下所有表的增删改查权限。ON后面的部分叫权限作用域,TO后面是账号。

MySQL的权限种类很多,常用的就是下面这些:

  • SELECT:查询数据
  • INSERT:插入数据
  • UPDATE:更新数据
  • DELETE:删除数据
  • CREATE:创建库、表、索引
  • DROP:删除库、表
  • ALTER:修改表结构
  • INDEX:创建或删除索引
  • EXECUTE:执行存储过程或函数
  • GRANT OPTION:允许把自己的权限转授给别的账号

权限授予的原则我建议始终是"最小够用"。只读业务就只给SELECT,写业务再考虑加INSERT、UPDATE、DELETE,表结构变更权限(CREATE、ALTER、DROP)绝对不要给到业务应用账号。很多线上事故,本质上就是应用账号权限太大,一条误操作SQL直接把表删了。这个原则不是保守,是保命。

3.2 权限层级:全局、库、表、列怎么选

GRANT语句的ON部分决定了权限的生效范围,按层级从大到小是这样的:

权限层级写法示例存储位置典型用途
全局权限ON *.*mysql.user管理员账号
数据库权限ON appdb.*mysql.db应用账号
表权限ON appdb.ordersmysql.tables_priv只给某张表
列权限ON appdb.orders(amount)mysql.columns_priv敏感字段隔离
存储过程权限ON appdb.proc_namemysql.procs_priv只允许调用特定存储过程

全局权限*.*要特别慎重,因为它的改动记录在mysql.user表里,影响的是整个MySQL实例。日常工作中,给业务账号授权,最多到库名.*就够了。把粒度细化到表可是有先例的:比如某个统计报表账号,只给它访问orders表,不给它看customer_info表,这样即使账号泄露,敏感客户数据也不会暴露。

3.3 几种常用授权组合的实战写法

只读报表账号:

GRANT SELECT ON appdb.* TO 'report'@'192.168.10.%';

应用读写账号,但只允许它操作appdb这个库:

GRANT SELECT, INSERT, UPDATE, DELETE ON appdb.* TO 'app'@'192.168.10.%';

管理员账号,允许管理所有库,并且可以把权限转授给别人:

GRANT ALL PRIVILEGES ON *.* TO 'admin'@'localhost' WITH GRANT OPTION;

这里必须提醒:ALL PRIVILEGES不等于WITH GRANT OPTION。很多人以为赋了ALL就等于拥有一切,其实GRANT OPTION需要单独写。反过来,WITH GRANT OPTION是个双刃剑,被授权的账号可以把权限再授给其他账号,一旦这个账号被攻击,攻击者就能无限横向扩展权限。所以我给应用账号、报表账号从来不加GRANT OPTION,只有DBA账号才需要。

业务运行一段时间后,需求变化想追加权限,直接用GRANT追加即可,GRANT本身有"幂等追加"的效果,重复执行不会重复叠加产生副作用:

GRANT EXECUTE ON appdb.* TO 'app'@'192.168.10.%';

这句在原有权限基础上,追加了存储过程的执行权限,原来的增删改查不受影响。

4. 查看、刷新、回收与删除

4.1 用 SHOW GRANTS 确认权限现状

排查权限问题,第一步永远是看当前账号实际拥有什么权限:

SHOW GRANTS FOR 'app'@'192.168.10.%';

如果你不知道自己当前登录的是哪个账号、拥有什么权限,可以直接:

SELECT CURRENT_USER(); SHOW GRANTS FOR CURRENT_USER();

注意CURRENT_USER()返回的是MySQL实际匹配到的账号,和客户端输入的用户名可能不一样。比如你用root从远程连,但MySQL只匹配到了'root'@'localhost'这个账号,CURRENT_USER()返回的就是root@localhost。这个洞察在排查"为什么我有权限却不生效"时至关重要:你以为自己是以A身份登录,实际MySQL匹配到的是B身份,权限当然对不上。

查询所有账号列表也很简单:

SELECT User, Host, plugin FROM mysql.user;

不建议直接SELECT authentication_string并到处贴,密码hash属于敏感信息。

4.2 FLUSH PRIVILEGES 什么时候必须用

关于FLUSH PRIVILEGES,网上的说法非常混乱。有人说完美执行完GRANT必须执行,不然不生效。这里我负责任地讲:用CREATE USER、ALTER USER、GRANT、REVOKE、DROP USER这些官方语句管理权限时,MySQL会自动更新权限相关的内存缓存,不需要执行FLUSH PRIVILEGES。

那什么时候必须用?只有当你直接操作了系统表,比如用INSERT、UPDATE、DELETE改了mysql.user、mysql.db这些表的内容。这种情况我不建议任何人做——直接改权限表风险极高,稍不留神就会把自己锁在门外。如果确实遇到权限表漂移、内存缓存和表数据不一致之类的异常,再执行FLUSH PRIVILEGES强制重建缓存。日常操作,别依赖这条命令,也别没事就刷。

4.3 回收权限、改密码、删账号的正确姿势

回收权限用REVOKE,和GRANT是对称的:

REVOKE DELETE ON appdb.* FROM 'app'@'192.168.10.%';

这句收回DELETE权限,其他权限不受影响。要注意REVOKE的ON和FROM要写完整,漏了作用域会直接报错。

修改密码用ALTER USER:

ALTER USER 'app'@'192.168.10.%' IDENTIFIED BY 'NewPass@2024';

这是MySQL 8推荐的方式,比老版本里的SET PASSWORD FOR ... = PASSWORD('...')更规范。另外,当你担心账号被爆破时,可以临时禁用账号,而不是删除:

ALTER USER 'temp_user'@'%' ACCOUNT LOCK;

需要恢复时再执行:

ALTER USER 'temp_user'@'%' ACCOUNT UNLOCK;

ACCOUNT LOCK这个功能在MySQL 8里很好用,很多运维不知道,遇上异常告警时宁可先锁账号再排查,也不要直接DROP,因为DROP是不可逆的。

删除账号:

DROP USER 'app'@'192.168.10.%';

DROP USER会连同这个账号在各权限表里的授权记录一起清理,不需要先REVOKE再DROP。但要注意:如果业务正在使用这个账号,DROP的瞬间连接会全部断开,应用侧报错是必然的。所以删除账号前,先确认没有活动连接,可以用SHOW PROCESSLIST检查一下。

5. 实战踩坑记录与排查思路

5.1 "Access denied" 报错到底在说什么

这个报错大概是MySQL世界里出现频率最高的错误之一。很多人一到这一步就慌,其实只要冷静拆解,原因无非下面几种:

报错表现可能原因排查方向
Access denied (using password: YES)密码错误检查密码是否被转义、是否有隐藏字符
Access denied (using password: YES)Host不匹配查看mysql.user里该用户允许的来源主机
Access denied (using password: YES)账号不存在检查User和Host是否和连接参数完全一致
Access denied (using password: NO)客户端没发密码确认连接命令里是否有密码参数
Plugin cannot be loaded认证插件版本不匹配按2.2方向处理

排查的第一步永远是在服务器本机,用mysql命令行直接连:

mysql -u app_user -p -h 127.0.0.1

如果本机能连,说明账号和权限配置没问题,问题多半在网络、驱动、或者Host匹配上。如果本机也报Access denied,就直接查mysql.user表,确认你要连的账号是否真的存在:

SELECT User, Host FROM mysql.user WHERE User = 'app_user';

这一步能快速区分"账号不存在"和"密码错误"。需要注意的是,如果你连接的客户端指定了host(比如远程IP),MySQL匹配账号时会按Host字段的优先级来找,具体匹配顺序是先精确匹配,再按通配符的精确程度排序,localhost比%优先,IP精确值比网段通配优先。所以可能出现一种情况:你建了'app'@'%',但服务器上还存在一个'app'@'localhost',从本机连接时命中的是localhost账号,密码对不上,报错。这种问题不看CURRENT_USER()是永远找不到原因的。

5.2 授权成功却仍然没权限

还有一种让人抓狂的情况:GRANT执行成功,SHOW GRANTS也显示权限都给了,但业务执行SQL还是被拒绝。

我遇到过一个典型案例:某公司有个报表程序,连接参数写的是app@127.0.0.1,但账号授权时只授给了'app'@'localhost'。看起来都是本机,但客户端用TCP方式连127.0.0.1,服务端匹配账号时,Host是按来源IP来匹配的,最后匹配到了另一个隐形的账号或者根本匹配不到。这种问题的排查办法就是前面说的,先查CURRENT_USER(),看清楚MySQL到底把你识别成了哪个账号。

另外要注意权限叠加。我在1.3提过,MySQL的权限是多层叠加的。如果你在mysql.user里给某账号授了全局DELETE权限,又在mysql.db里对它REVOKE了某个库的DELETE权限,那后者并不能覆盖前者——全局权限依然生效。权限回收是个精细活,一定要确认你回收的是哪一层。最稳妥的验证方式,是拿这个账号实际登录执行一次目标SQL,不要只看SHOW GRANTS的输出。

5.3 密码正确但客户端连接失败

这个是MySQL 8升级后最常见的迁移事故。老系统换到MySQL 8,应用连接突然大量报错,报错内容提到auth plugin或者caching_sha2。原因就是2.2说的认证插件变了。

处理顺序我建议这样:先看驱动是不是太久远,升级MySQL JDBC或对应语言驱动到新版本;如果升级成本高,再考虑把账号认证插件改为mysql_native_password。但要注意,改插件只是绕过了问题,不是真正解决了安全降级。如果这是长期运行的系统,还是应该推动客户端升级。

还有一种情况是SSL/RSA相关:客户端连接时,如果你的连接被要求安全传输,而客户端没有配置服务器公钥,首次连接可能卡在密码验证环节。此时要么在连接参数里指定allowPublicKeyRetrieval=true(这需要你确认网络环境可信),要么配置好SSL证书。这不是MySQL的bug,而是安全传输机制的一部分,理解了原理就不会对着配置文件乱试了。

5.4 权限管理的小技巧与体检清单

我每次上线新系统,都会按下面这个清单快速过一遍:

  • 所有账号的Host是否最小化?有没有不必要的大范围通配?
  • 应用账号是否只有SELECT/INSERT/UPDATE/DELETE?有没有意外的DROP、ALL?
  • 有没有账号带GRANT OPTION但不是DBA在用?
  • 临时账号、离职人员账号是否已经清理?
  • 密码是否定期轮换?密码强度是否符合策略?

把这些清单固化到日常运维流程里,比出一次事故再补救要划算得多。另外一个习惯也很有用:每个季度用SHOW GRANTS把所有账号的权限导出归档,作为权限审计的基线。哪天出问题,对比基线就知道谁改过什么。

6. 一点个人体会

权限管理这件事,我这些年最大的体会就一句话:权限是给业务用的,但也是给风险留的门,门越少越好。

我自己曾经因为偷懒,把一个业务账号直接授了ALL PRIVILEGES ON *.*,结果测试同事误操作,一个UPDATE语句没带WHERE条件,直接把整张业务表更新了。虽然最后有备份恢复,但那次折腾让我彻底改了习惯:账号权限永远按最小原则给,授权之前先想清楚它要做的事,宁可在需求明确后多执行一次GRANT追加,也不要在权限上提前"大方"。

最后再分享一个实用技巧:如果你要批量复制某个账号的权限,可以先执行SHOW GRANTS FOR '模板账号'@'host',把输出整理成GRANT语句,把账号部分替换成新账号再执行。这样能避免手工一条条授权时漏项,同时保留一份可审计的记录。MySQL的权限管理说到底不复杂,但它藏着的坑特别多,把底层规则吃透了,后面的路就顺了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询