最近有几个朋友问我同一个问题:“MySQL 里怎么创建一个新用户,然后只给他某一个库的权限?”一开始我以为是新手才问这个,后来发现有不少写过一两年 SQL 的人,也会在这个地方翻车——要么是GRANT写错了语法,要么是建完用户之后用 Navicat 连不上,再要么是权限给得太宽,连生产库的DROP权限都交出去了。说实话,MySQL 的权限系统设计得很完整,但正因为它完整,很多人才会在“创建新用户 + 授予权限”这个最基础的操作上栽跟头。这篇内容我就从实际运维和开发的角度,把整个流程拆开揉碎了讲一遍,包括 MySQL 5.7 和 MySQL 8.0 的语法差异、认证插件怎么选、精确到库/表/字段的授权怎么玩、权限回收和删除用户时容易踩的坑,以及几个我用真实生产环境验证过的账号分配方案。无论你是在本地搭环境,还是管理线上数据库,这篇文章都能当个参考手册用。
1. 为什么推荐单独建用户,而不是一直用 root 操作
先聊聊大多数教程不会告诉你的理念问题:为什么要单独建用户?直接用 root 不香吗?
在开发环境里用 root 确实没什么感觉,反正就你一个人折腾。但是一旦涉及多人协作、项目上线、或者公司安全审计,root 账号的管理就非常头疼了。root 是 MySQL 的超级管理员账号,它拥有所有库的所有权限,包括DROP DATABASE、GRANT OPTION、SHUTDOWN这类危险操作。你用 root 去启动一个 JavaWeb 项目的数据库连接池,等于把整库的生死权限都交给了应用层——万一应用被注入了或者代码有 bug,数据库连后悔的机会都没有。
我记得看过一个真实案例:某团队为了图省事,项目里所有人共享一个 root 密码。后来一个同事在测试环境执行DROP DATABASE时手滑连上了生产库,直接把核心业务表全删了。事后排查发现,删库的人甚至连操作日志都没留下——因为大家都用同一个 root 账号,根本没法追溯是谁干的。如果当时按项目隔离、按人建账号,至少能把事故范围缩小到一个库,还能通过mysql.general_log精确定位到人。
所以单独建用户的核心价值有三个:
- 最小权限原则:每个应用连接数据库时,只使用自己业务域内需要的权限,比如某个库的
SELECT/INSERT/UPDATE/DELETE,超出范围的操作全部拒绝。 - 责任可追溯:每个开发者一个账号,配合如
init_connect参数或审计插件,能知道谁在什么时间做了什么操作。 - 故障隔离:某个账号密码泄露,最多影响它权限范围内的库,而不是整个 MySQL 实例。
这就是为什么”创建新用户 + 授予权限“这件事,看起来简简单单,却是所有 MySQL 管理操作里最应该认真对待的一环。
2. 动手前的环境确认:版本差异与认证插件决定你怎么建账号
很多人在网上搜到一段CREATE USER语法,复制到自己机器上却报错,或者建完之后客户端连不上。原因往往不是语法本身错了,而是没搞清楚自己用的 MySQL 版本和认证插件。
2.1 先确认你的 MySQL 版本
不同的 MySQL 版本,CREATE USER和GRANT的语法、默认行为、甚至密码认证方式都不一样。尤其是 8.0 版本,改动非常大。你可以用下面这条命令确认版本:
mysql -uroot -p # 登录后执行 SELECT VERSION(); -- 8.0.36 这样的输出,就是 MySQL 8.0 系列如果是5.7 及以下版本,CREATE USER后一般建议加IDENTIFIED BY '密码',并且默认的认证插件是mysql_native_password,任何客户端都能顺利连接。
如果是8.0 及以上版本,默认认证插件变成了caching_sha2_password,这个插件安全性更高,但一些老旧的客户端(比如很老版本的 Navicat、PHP 的mysql扩展、部分 Python 驱动)默认不支持,就会抛出不认识认证插件的错误。常见的报错长这样:
Authentication plugin 'caching_sha2_password' cannot be loaded: The specified module could not be found.遇到这种情况,你就需要在创建用户时显式指定认证插件为mysql_native_password,或者在 MySQL 8.0 里用ALTER USER修改已有用户的插件类型。不过我个人建议,如果是新项目,尽量升级客户端驱动来支持caching_sha2_password,因为mysql_native_password在 MySQL 9.x 里已经被移除了,长期来看还得跟上版本。
2.2 了解 MySQL 8.0 的默认安全策略
另外,MySQL 8.0 默认开启了密码复杂度校验插件validate_password,对用户密码有强度要求,至少要包含大写字母、小写字母、数字和特殊字符中的三类,并且长度不低于 8 位。如果你用123456这种弱密码建用户,很可能会直接报错:
ERROR 1819 (HY000): Your password does not satisfy the current policy requirements所以建用户之前,建议先看一下当前环境的密码策略:
SHOW VARIABLES LIKE 'validate_password%';如果是在自己本地测试,觉得这个策略比较烦,可以临时调低等级(生产环境不建议这么干):
SET GLOBAL validate_password.policy = LOW; SET GLOBAL validate_password.length = 6;注意:validate_password相关的参数在不同版本里名字稍有区别,MySQL 8.0 系列用的是带点号的validate_password.policy,5.7 用的是下划线validate_password_policy。这块是我经常见到的误区之一,命令抄错版本就很容易报错。
2.3 认证插件差异对比
为了让你更直观地理解认证插件的重要性,我把两种插件做了一个对比:
| 项目 | mysql_native_password | caching_sha2_password |
|---|---|---|
| 默认性 | MySQL 5.7 默认 | MySQL 8.0 默认 |
| 加密强度 | SHA1 散列,安全性一般 | SHA2 算法,加密强度更高 |
| 连接性能 | 每次做简单校验,略快 | 首次连接需要额外握手,之后有缓存,性能影响不大 |
| 兼容性 | 几乎所有老客户端兼容 | 新客户端驱动兼容,老客户端不支持 |
| 建议 | 仅为了兼容老客户端时使用 | 新环境优先用这个 |
3. CREATE USER 语法拆解:从零开始建立一个完整的账号
环境确认完,就到正题了。先给出一段最简单的创建用户语句,然后逐行解释。
CREATE USER 'test_user'@'localhost' IDENTIFIED BY 'YourStrongPass123!';3.1 用户名和主机名的含义
这里的'test_user'@'localhost'是两个关键要素的组合:用户名test_user和允许登录的主机名localhost。
主机名决定这个账号能从哪些 IP 或主机上连接数据库。最常见的取值是:
localhost:只能从本机连接,常用于本地应用、定时任务、运维脚本。%:通配符,允许从任意主机连接,适合应用服务器 IP 不固定或者需要多个远程机器访问的场景。192.168.1.%:只允许特定网段,比如同一个局域网内的应用服务器。具体IP:如'192.168.1.101',只允许这个 IP 连接。
我强烈建议你不要上来就写'test_user'@'%',除非你的应用服务器 IP 真的会动态变化。限定主机范围是权限控制的第一道门,很多人把密码泄露归咎于数据库被扫描爆破,但其实如果主机范围限定好了,外网机器根本连不上你的 MySQL 端口,爆破也就无从谈起。
3.2 IDENTIFIED BY 后面的密码细节
密码这里还有几个小细节:
- 密码需要满足当前环境的密码策略,否则报错。
- 密码字符串要加引号,建议用单引号包起来;如果密码里有特殊字符(比如
$、@、#),注意 shell 层面的转义。 - 生产环境不建议把密码明文写在命令行历史里,可以用
mysql_config_editor或者环境变量来管理。个人本地测试倒是无所谓。
3.3 创建不设置密码的账号(不推荐)
语法上可以不写IDENTIFIED BY,但这种用户除非是特殊内部用途,否则任何人都能无密码登录:
CREATE USER 'anonymous_test'@'localhost';这个操作只适合临时测试,正式环境一定不要出现无密码账号。因为 MySQL 默认允许空密码用户存在,安全隐患极大。
3.4 修改已有用户密码
如果你要改一个已有用户名的密码,在 MySQL 5.7 里可以用:
SET PASSWORD FOR 'test_user'@'localhost' = PASSWORD('NewPass123!');在 MySQL 8.0 里,推荐用ALTER USER语法:
ALTER USER 'test_user'@'localhost' IDENTIFIED BY 'NewPass123!';如果同时想改认证插件,可以这样写:
ALTER USER 'test_user'@'localhost' IDENTIFIED WITH mysql_native_password BY 'NewPass123!';我在实战中遇到过一次很典型的场景:应用部署在 CentOS 上,连接 MySQL 8.0 一直报Authentication plugin cannot be loaded,最后就是用这条ALTER USER把账号改成mysql_native_password,问题立刻解决。所以这一步其实才是很多远程连接故障的“隐藏解法”。
4. 授权绝不是一条 GRANT 就完事:权限级别与精确授权的取舍
用户创建好了,接下来是重头戏——授予权限。
4.1 权限级别怎么理解
MySQL 的权限系统其实是分层的,从大到小依次是:
- 全局级别:作用于整个 MySQL 实例,比如
CREATE DATABASE、DROP DATABASE、SHUTDOWN、SUPER等,存在mysql.user表里。 - 数据库级别:作用于某个具体的库,比如
SELECT、INSERT、UPDATE、DELETE、CREATE TABLE等,存在mysql.db表里。 - 表级别:作用于某张具体的表,比如
SELECT一张表、INSERT一张表。 - 列级别:作用于某张表的某些列,比如只允许读
name列而不允许读salary列。 - 例程级别:作用于存储过程和函数,比如
EXECUTE。
所以用户在发起一条 SQL 时,MySQL 会把这台机器、这个库、这张表、这些列层层叠加判断,任何一个环节没有权限,SQL 就会被拒绝。
4.2 最适合 90% 场景的授权语法
假如你的应用只需要访问名为app_db的库,下面这一行是最常用的授权写法:
GRANT SELECT, INSERT, UPDATE, DELETE ON app_db.* TO 'test_user'@'localhost';这里面的逻辑是:
SELECT, INSERT, UPDATE, DELETE:这四个权限是 CRUD(增删改查)的标准配置,权限最小且满足日常业务。ON app_db.*:作用范围是app_db库下的所有表。如果你连app_db库的建表、改表权限都不想给,就只给这四个。- 授权后是否刷新权限:在 MySQL 8.0 里,
CREATE USER和GRANT之后不需要执行FLUSH PRIVILEGES,因为账号和权限表是自动落盘的。但是在 5.7 及更早版本里,如果你直接修改了mysql.user、mysql.db这些系统表,或者有些教程让你手写INSERT INTO USER,那就必须FLUSH PRIVILEGES;让权限缓存重新加载。
我见过不少老教程让人在GRANT后加一句FLUSH PRIVILEGES;,严格来说这并不是错误,但在 MySQL 5.7+ 和 8.0 里属于多余操作。如果你在编写初始化脚本,写了也无妨,多刷一次不影响结果,只是概念上要知道它不是必须的。
4.3 更细粒度的授权:表级别、字段级别和存储过程
有些场景权限可以给得更细。比如业务上只允许某个账号查询order表的id、status、create_time三个字段,不允许读取客户手机号字段,可以这样写:
GRANT SELECT (id, status, create_time) ON app_db.order TO 'readonly_user'@'localhost';这样readonly_user就只能 select 指定的列,连SELECT *他都会失败,因为他没有权限读取order表上所有列。这个特性在实际做数据脱敏、敏感权限分级的时候非常有用。
再比如,如果应用需要调用某个存储过程,但要禁止它直接操作基表,可以这样:
GRANT EXECUTE ON PROCEDURE app_db.update_order_status TO 'app_user'@'localhost';这样应用只能通过这个存储过程去改数据,不能绕过逻辑直接改表。很多金融类项目、订单系统都是这么设计的,算是一种“强制走接口”的思路。
4.4 什么是 GRANT OPTION,别乱给
GRANT OPTION是 MySQL 权限体系里非常重要的一个权限。它代表用户可以将自己拥有的权限再授予其他用户,相当于“代理授权”。如果你给普通用户加了GRANT OPTION,那么它理论上可以创建一个新用户,然后把它自己拥有的权限复制过去。这有点像给了一个人你自己房卡的复制权。
我在真实生产环境里见过一个事故:管理员为了方便,给开发账号加了WITH GRANT OPTION,开发自己建了一堆临时账号用于联调,半年后项目结束,这些临时账号既没人回收、也没人记得,等于数据库里多了一批权限外泄的入口。所以除非你确实需要一个“数据库管理员”的辅助账号,否则不要给普通业务账号加GRANT OPTION。
正确写法是直接省略:
-- 不要这样 GRANT ALL PRIVILEGES ON app_db.* TO 'test_user'@'localhost' WITH GRANT OPTION; -- 推荐这样 GRANT SELECT, INSERT, UPDATE, DELETE ON app_db.* TO 'test_user'@'localhost';4.5 ALL PRIVILEGES 到底该不该用
ALL PRIVILEGES表示某个范围内的所有权限,常见写法是GRANT ALL PRIVILEGES ON app_db.* TO ...。如果你只是本地搭环境,怎么爽怎么来都行;但在生产环境,我建议尽量少用。原因很简单:ALL PRIVILEGES里包含DROP、ALTER、CREATE、INDEX这些“重量级”权限,一旦应用被 SQL 注入拿到这些权限,基本等于控制整个库。
如果某个账号只是给开发人员自己在测试库用的,那ALL PRIVILEGES ON test_db.*无可厚非,灵活方便。但给应用 API 用的连接账号,最好还是最小化到业务真正需要的权限上去。
5. 权限的查看、回收与删除:别让不再使用的账号留在生产库
很多团队建用户、授权很积极,但忘了做生命周期管理。权限的查看、回收、删除同样属于日常运维的重要内容。
5.1 查看用户权限的几种方式
创建完用户后,你可能想确认到底给了哪些权限。MySQL 提供了专门的命令:
-- 查看某个用户的权限 SHOW GRANTS FOR 'test_user'@'localhost';输出类似这样:
+-----------------------------------------------------------+ | Grants for test_user@localhost | +-----------------------------------------------------------+ | GRANT USAGE ON *.* TO `test_user`@`localhost` | | GRANT SELECT, INSERT, UPDATE, DELETE ON `app_db`.* | +-----------------------------------------------------------+GRANT USAGE ON *.*是一个初始状态,表示这个用户没有任何实际权限,仅仅是一个合法账号。真正的权限在第二行。
如果你想看当前登录用户的权限,直接写:
SHOW GRANTS FOR CURRENT_USER();如果你想全局梳理整个实例里有哪些账号、他们的密码过期时间、认证插件等,可以查mysql.user表:
SELECT User, Host, plugin, password_expired, account_locked FROM mysql.user;这个表在 8.0 里字段比 5.7 多一些,但User、Host这两个核心字段永远在。注意,mysql.user表只存全局层级的权限信息,库层级的授权在mysql.db表里,表层级在mysql.tables_priv表里。
5.2 权限回收的正确姿势
权限回收用REVOKE,语法和GRANT基本对应。比如我把刚才给的DELETE权限收回来:
REVOKE DELETE ON app_db.* FROM 'test_user'@'localhost';如果想把整个库的所有权限都收走,可以这样:
REVOKE ALL PRIVILEGES ON app_db.* FROM 'test_user'@'localhost';注意:REVOKE ALL PRIVILEGES并不会删除账号本身。这个用户仍然存在,只是变成了一个什么也干不了的“空壳”账号。如果你希望彻底移除账号,需要再执行DROP USER。
5.3 删除用户前先查清引用关系
删除用户是一件高风险操作,尤其当你删的是应用正在使用的数据库账号时,会导致整个应用报连接错误。我在实操中的习惯是:
- 先
SHOW GRANTS FOR '目标用户'@'host';确认这是哪个用途的账号。 - 去项目的配置中心或环境变量里查连接串是否引用了这个账号。
- 确认没有引用之后再执行删除:
DROP USER 'test_user'@'localhost';如果一次性要删多个账号,可以用逗号分隔:
DROP USER 'user1'@'localhost', 'user2'@'%';这里有个坑:DROP USER的主机名要和创建时完全一致。你创建时写的是'test_user'@'localhost',删除时写'test_user'@'%'就删不掉,因为 MySQL 把这两者视为两个不同的账号。
5.4 关于账号锁定的使用
生产环境经常遇到一种场景:发现某个账号异常,但不能立刻删除,担心是旧项目在跑。这时候可以先锁定账号,而不是直接删除:
-- 锁定账号 ALTER USER 'test_user'@'localhost' ACCOUNT LOCK; -- 解锁账号 ALTER USER 'test_user'@'localhost' ACCOUNT UNLOCK;锁定后的账号无法登录,但已建立的连接不受影响,而且可以随时解锁恢复,比直接删除安全得多。这是我处理可疑账号的标准操作:先锁,观察一周,确认没有告警后再 DROP。
6. 我用实际生产脚本验证过的权限分配方案
理论讲完了,分享三个我在实际项目中验证过的分配方案。这些方案适用于不同团队规模和业务阶段,你可以直接复制改造。
6.1 方案一:单人本地开发环境
这是最简单的情况,典型场景是你自己本地装了 MySQL,给项目建个专用账号。
-- 本地开发环境专用账号 CREATE USER 'dev'@'localhost' IDENTIFIED BY 'Dev@2024local'; GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, ALTER, INDEX ON dev_db.* TO 'dev'@'localhost';这里比生产多了CREATE、ALTER、INDEX,因为本地开发经常要改表结构、加索引。本地账号权限给宽一点问题不大,方便调试。
6.2 方案二:JavaWeb 项目的标准数据库账号
一个典型 JavaWeb 项目跑在服务器上,应用通过 JDBC 连接池连接数据库。这时数据库账号应该最少化到业务操作本身。
-- 应用专用账号,限定在应用服务器IP段 CREATE USER 'app_user'@'192.168.1.%' IDENTIFIED BY 'App@2024Prod'; -- 只给CRUD权限 GRANT SELECT, INSERT, UPDATE, DELETE ON app_db.* TO 'app_user'@'192.168.1.%'; -- 可选:如果应用需要导入导出数据或存储过程,再单独加 GRANT FILE ON *.* TO 'app_user'@'192.168.1.%'; GRANT EXECUTE ON PROCEDURE app_db.sp_update_status TO 'app_user'@'192.168.1.%';注意FILE权限我单独列出来,因为它涉及服务器文件读取,权限太重,能不给就不给。如果需要导入导出,最好由 DBA 完成,而不是放在应用账号里。
6.3 方案三:只读账号用于数据分析、报表和备份
数据分析师或定时报表任务通常只需要读权限。可以建一个统一的只读账号,方便复用:
CREATE USER 'readonly'@'%' IDENTIFIED BY 'Read@Only2024'; GRANT SELECT ON app_db.* TO 'readonly'@'%';如果你希望只读账号能看到但不能改,这个写法就够了。更严格一点,还能加上字段级过滤,把敏感列(例如用户手机号、密码哈希)排除掉。
但这里我要重点提醒:如果只读账号的 host 是'%',意味着任何远程 IP 都能用这个账号登录。这种账号一旦密码泄露,攻击者可以随便连上你的 MySQL 实例,把库内数据捞走。所以哪怕是只读账号,最好也限定 host。
6.4 生产环境初始化脚本的完整示例
如果是自动化部署,可以用一个 SQL 脚本统一执行。下面是我常用的初始化脚本模板:
-- create_user_and_grant.sql CREATE DATABASE IF NOT EXISTS app_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- 应用账号 CREATE USER 'app_user'@'10.0.0.%' IDENTIFIED BY 'App@2024Prod'; GRANT SELECT, INSERT, UPDATE, DELETE ON app_db.* TO 'app_user'@'10.0.0.%'; -- 只读账号 CREATE USER 'readonly'@'10.0.0.%' IDENTIFIED BY 'Read@Only2024'; GRANT SELECT ON app_db.* TO 'readonly'@'10.0.0.%'; -- 操作完毕再查看 SHOW GRANTS FOR 'app_user'@'10.0.0.%'; SHOW GRANTS FOR 'readonly'@'10.0.0.%';这个脚本跑完之后,你既有了干净的库,也有了规范化的应用账号和只读账号,后续项目要用直接复制改账号名和密码就行。
7. 高频问题排查与踩坑记录
最后一个部分,把我在实操和社区帮人看问题时常见的坑列一遍。每一行都对应一个真实事故现场。
7.1 连接报错“Access denied for user”
这是最常见的错误。遇到这个,先按顺序查三件事:
- 用户名或密码是否拼写正确:特别注意复制粘贴时有没有多空格、大小写是否一致。
- 主机名是否匹配:你创建的是
'test_user'@'localhost',但应用在远程用test_user连接,MySQL 会认为这是'test_user'@'你的应用IP',找不到匹配账号,直接拒绝。 - 账号是否被锁定:有时
ALTER USER ... ACCOUNT LOCK之后忘了解锁,也会报这个错。
这里有个容易被忽略的细节:MySQL 在匹配账号时,是按user和host一起精确匹配的。'test_user'@'localhost'和'test_user'@'%'是两个完全不同的账号,即便密码一样,也是两个身份。所以如果你改了其中一个的密码,另外一个不会跟着变。
7.2 已经授权但还是无法访问某个库
这种情况通常是搞混了权限的作用范围。例如你只授权了app_db.*,却在访问另一个库时发现没有权限,这不是 bug,而是你本来就没授权。还有一种情况是在 MySQL 8.0 里,如果你给的是app_db.*而你的连接串里写的是jdbc:mysql://localhost:3306/app_db,但查询语句里又跨库引用了other_table,就会因为其他库的SELECT权限缺失而报错。
7.3 用 Navicat 或老客户端连接 MySQL 8.0 失败
这个我前面提过,核心是认证插件问题。如果确认CREATE USER没有指定插件,MySQL 8.0 默认是caching_sha2_password,老版本 Navicat 不支持。解决方案有两种:
- 登录到 MySQL,用
ALTER USER把认证插件改成mysql_native_password; - 干脆升级 Navicat 到较新的版本。
但要是 MySQL 版本本身就特别老(比如 5.5),有些新驱动又不兼容,反而需要安装mysql_native_password插件。总之先确认版本再动手。
7.4 脚本里批量创建用户时,错误信息不明确
写初始化脚本批量创建用户时,经常遇到一条语句报错导致整个脚本中断。这时推荐在 MySQL 命令行执行时,加一个--force参数,让错误时继续执行:
mysql -uroot -p < init_script.sql --force也可以逐条排查,SQL 脚本里每条建议用分号结尾,并加上必要的USE mysql等上下文。
7.5 密码策略太强或太弱怎么办
很多本地测试用户被密码策略卡住,最简单的调整方式我前面写了,用SET GLOBAL修改策略等级。但要注意,这个修改在 MySQL 重启后会恢复默认值。如果想永久生效,需要写到my.cnf或my.ini配置文件的[mysqld]段下面:
[mysqld] validate_password.policy=LOW validate_password.length=6修改后重启 MySQL 服务生效。生产环境建议保留默认高强度策略,毕竟密码是数据库的第一道防线。
7.6 改密码后应用仍然能连接,要等多久生效
在 MySQL 5.7 和 8.0 中,ALTER USER和SET PASSWORD会立即写入授权表,新连接立刻用新密码。已经存在的长连接不会感知到密码变更,它们会继续工作直到断开重连。所以生产环境改密码后,如果你希望立即踢掉所有旧连接,可以执行KILL相关线程,否则慢慢等连接池老化即可。
写在最后的小建议
关于 MySQL 创建新用户和授予权限这件事,不同项目阶段的做法确实不一样。本地学习时怎么方便怎么来,真正上了生产,账号隔离、权限最小化、定期回收这些习惯要尽早养成。我在实际使用中的体会是:权限管理这种事,前期越省事,出问题时就越痛苦,尤其是账号权限已经扩散到多个环境时,收尾非常麻烦。建议你不管项目多小,都固定一套创建用户的脚本,账号名、host 范围、权限集都写清楚,以后审计和排查会轻松很多。如果是刚入手 MySQL,也不怕,把这一套流程在本地装上 MySQL 之后完整跑一遍,基本就理解权限体系是怎么回事了。