RSS
菜单
全部文章快讯开发科技深度热点

第 15 章 用户与权限管理

内容摘要

MySQL 是一个多用户数据库系统,谁能连进来、能看哪些库、能改哪些表,全靠用户与权限体系控制。本篇假设你已经能用 root 登录 MySQL、会写基本 DDL/DML。学完之后,你将能按业务需要创建最小权限账号、用 GRANT/REVOKE 精确授权、回收权限,并说出生产环境有哪些安全红线。本示例基于 MySQL 8.0。

📖 本篇简介:MySQL 是一个多用户数据库系统,谁能连进来、能看哪些库、能改哪些表,全靠用户与权限体系控制。本篇假设你已经能用 root 登录 MySQL、会写基本 DDL/DML。学完之后,你将能按业务需要创建最小权限账号、用 GRANT/REVOKE 精确授权、回收权限,并说出生产环境有哪些安全红线。本示例基于 MySQL 8.0。

目录

  • 1 MySQL 账号体系概述
  • 2 创建用户 CREATE USER
  • 3 查看 修改 删除用户
  • 4 GRANT 授权详解
  • 5 REVOKE 撤权
  • 6 权限粒度层级
  • 7 权限速查表
  • 8 安全实践建议
  • 9 常见坑与注意事项
  • 10 练习题

1 MySQL 账号体系概述

MySQL 的账号不是一个单纯的"用户名",而是 用户名 + 来源主机 的组合。也就是说,'app'@'localhost' 和 'app'@'192.168.1.%' 是两个完全不同的账号,可以配不同的密码和权限。

所有账号信息存在系统库 mysql 的 user 表里。登录时 MySQL 会按"最匹配主机"的原则找到对应账号,再校验密码。

-- 本示例基于 MySQL 8.0
-- 查看当前有哪些用户(需要 root 或有 mysql.user 权限的账号)
SELECT user, host, account_locked FROM mysql.user ORDER BY user;

-- MySQL 8.0 默认使用 caching_sha2_password 认证插件
-- 查看某个用户用的认证插件
SELECT user, host, plugin FROM mysql.user WHERE user = 'root';

一个生产库通常至少有这几类账号:

| 账号类型 | 用途 | 权限特点 |

|---|---|---|

| root | 超级管理员 | 仅本机登录,绝不对外开放 |

| 应用账号 | 业务程序连接 | 只给业务库的增删改查 |

| 只读账号 | 报表/分析 | 只有 SELECT |

| 运维账号 | DBA 部署/迁移 | 有限的 ALL,可建库 |

1.1 连接时的账号匹配规则

MySQL 收到连接请求后,会按"主机匹配度最高"的原则在 mysql.user 里挑一条记录来校验密码。比如同时存在 'app'@'localhost' 和 'app'@'%',从本机连进来时优先用前者,从其他机器连进来才用后者。这就是为什么同一个用户名在不同密码下都能登录成功——它其实命中的是不同的账号记录。

-- 排查"为什么我这个密码能/不能登录"时,先确认命中了哪条记录
SELECT user, host, plugin FROM mysql.user WHERE user = 'app';

2 创建用户 CREATE USER

MySQL 8.0 推荐先用 CREATE USER 建账号,再用 GRANT 授权(早期版本"授权即建号"的写法在 8.0 已不推荐)。

-- 创建一个只能从本机登录的应用账号
CREATE USER 'app'@'localhost'
    IDENTIFIED BY 'App@Strong#2026';   -- 密码尽量复杂,包含大小写数字符号

-- 创建一个允许从指定网段登录的账号(% 是通配符)
CREATE USER 'app_rw'@'10.0.0.%'
    IDENTIFIED BY 'Rw@Strong#2026';

-- 创建一个只读账号,限制登录过期时间为 90 天
CREATE USER 'report'@'10.0.0.%'
    IDENTIFIED BY 'Report@2026'
    PASSWORD EXPIRE INTERVAL 90 DAY;

-- 创建一个锁定状态的账号(建好先锁着,审批通过再解锁)
CREATE USER 'temp'@'%' IDENTIFIED BY 'Tmp@2026' ACCOUNT LOCK;

主机部分的写法规则:

| 主机写法 | 含义 |

|---|---|

| localhost | 只能本机通过 socket/127.0.0.1 登录 |

| 127.0.0.1 | 只能从 127.0.0.1 登录 |

| 10.0.0.% | 10.0.0.0/24 网段可登录 |

| 10.0.0.0/255.255.255.0 | 子网掩码形式 |

| % | 任意主机都能登录(极不安全,生产慎用) |

3 查看 修改 删除用户

-- 修改密码(MySQL 8.0 推荐用 ALTER USER)
ALTER USER 'app'@'localhost' IDENTIFIED BY 'NewApp@2026';

-- 修改当前登录账号自己的密码(不需要知道旧密码以外的权限)
ALTER USER USER() IDENTIFIED BY 'MyNewPass@2026';

-- 解锁被锁定的账号
ALTER USER 'temp'@'%' ACCOUNT UNLOCK;

-- 查看某个账号已经被授予了哪些权限
SHOW GRANTS FOR 'app'@'localhost';

-- 删除用户(同时会自动回收它的所有权限)
DROP USER 'temp'@'%';

注意:改密码、删用户后,该账号已经建立的连接不会立刻断开,新密码只对之后新建立的连接生效。要踢掉旧连接,需要用 KILL <进程ID>。

3.1 查看当前登录身份

排障时经常需要确认"我现在是以哪个账号、从哪台机器连进来的":

-- 当前登录用户和来源 IP
SELECT CURRENT_USER(), USER();
-- CURRENT_USER() 表示实际命中的账号记录,USER() 表示客户端实际连接信息

-- 当前会话拥有的全部权限
SHOW GRANTS;

CURRENT_USER() 和 USER() 不一样:前者是 MySQL 根据主机匹配选中的账号,后者是客户端发起连接时声明的账号名。当 'app'@'localhost' 和 'app'@'%' 同时存在时,这个区别特别有用。

4 GRANT 授权详解

GRANT 用来把权限授予账号。语法骨架:

GRANT 权限列表 ON 库.表 TO '用户'@'主机' [WITH GRANT OPTION];
-- 给应用账号授予 shop 库所有表的增删改查权限
GRANT SELECT, INSERT, UPDATE, DELETE
    ON shop.*
    TO 'app'@'localhost';

-- 给只读账号授予整个 shop 库的只读权限
GRANT SELECT ON shop.* TO 'report'@'10.0.0.%';

-- 授予某一张表的全部权限(含索引、建约束等)
GRANT ALL PRIVILEGES ON shop.orders TO 'app_rw'@'10.0.0.%';

-- 列级权限:只允许修改 user 表的 nickname 一列
GRANT UPDATE (nickname) ON shop.user TO 'app'@'localhost';

-- WITH GRANT OPTION:允许该账号把自己拥有的权限再转授给别人(慎用)
GRANT SELECT ON shop.* TO 'analyst'@'10.0.0.%' WITH GRANT OPTION;

-- 授权后刷新权限(一般 GRANT 会自动生效,FLUSH 用于手动改过 mysql.user 后)
FLUSH PRIVILEGES;

ON 库.表 的通配规则:

| 写法 | 含义 |

|---|---|

| *.* | 所有库所有表(超级权限) |

| shop.* | shop 库下所有表 |

| shop.orders | 只针对 orders 这一张表 |

| shop.tmp_% | shop 库下以 tmp_ 开头的表 |

4.1 常用业务角色模板

实际项目里,下面这几套账号模板可以直接套用:

| 角色 | 权限组合 | 用途 |

|---|---|---|

| 应用读写 | SELECT,INSERT,UPDATE,DELETE ON 业务库.* | 后端服务连接 |

| 报表只读 | SELECT ON 业务库.* | 数据分析跑报表 |

| 迁移运维 | ALL ON 业务库.* 但不含 GRANT | DBA 部署数据 |

| 开发测试 | ALL ON 测试库.* | 开发环境随便造 |

-- 一键生成"应用读写"账号的模板
CREATE USER 'app_rw'@'10.0.0.%' IDENTIFIED BY 'Rw@Strong#2026';
GRANT SELECT, INSERT, UPDATE, DELETE ON shop.* TO 'app_rw'@'10.0.0.%';

-- 一键生成"报表只读"账号的模板
CREATE USER 'report'@'10.0.0.%' IDENTIFIED BY 'Report@2026';
GRANT SELECT ON shop.* TO 'report'@'10.0.0.%';

4.2 权限的传递与回收细节

WITH GRANT OPTION 让账号能把自己的权限转授出去,形成授权链。回收时要注意:先收 GRANT OPTION,再收具体权限,否则会报错或收不干净。

-- 正确的撤权顺序
REVOKE GRANT OPTION ON shop.* FROM 'analyst'@'10.0.0.%';
REVOKE SELECT ON shop.* FROM 'analyst'@'10.0.0.%';

5 REVOKE 撤权

REVOKE 与 GRANT 对应,把权限收回来。

-- 收回 app_rw 对 orders 表的删除权限
REVOKE DELETE ON shop.orders FROM 'app_rw'@'10.0.0.%';

-- 收回只读账号对整个 shop 库的所有权限
REVOKE SELECT ON shop.* FROM 'report'@'10.0.0.%';

-- 收回"转授权限"
REVOKE GRANT OPTION ON shop.* FROM 'analyst'@'10.0.0.%';

-- 查看撤权后的结果
SHOW GRANTS FOR 'app_rw'@'10.0.0.%';

一个常见误区:REVOKE 的权限列表必须和当初 GRANT 的颗粒度一致,否则收不掉。比如当初给的是 ON shop.*,你用 ON shop.orders 去 REVOKE 是收不掉的。

6 权限粒度层级

MySQL 权限分四个层级,从大到小:

| 层级 | 写法 | 典型权限 |

|---|---|---|

| 全局层级 | ON *.* | FILE、PROCESS、RELOAD、REPLICATION SLAVE 等 |

| 数据库层级 | ON shop.* | CREATE、DROP、ALTER、INDEX |

| 表层级 | ON shop.orders | SELECT、INSERT、UPDATE、DELETE |

| 列/例行层级 | UPDATE (col) / EXECUTE ON PROCEDURE | 列级 UPDATE、存储过程执行权 |

设计原则是最小权限:应用账号只给它真正用到的库和表,绝不给 *.*,更不要随便给 SUPER、FILE、GRANT OPTION。

-- 反面教材:给应用账号超级权限,一旦程序被注入后果不堪设想
-- GRANT ALL PRIVILEGES ON *.* TO 'app'@'%';   -- 千万不要这样

-- 正确做法:精确到库、精确到常用 DML
GRANT SELECT, INSERT, UPDATE, DELETE ON shop.* TO 'app'@'10.0.0.%';

7 权限速查表

| 权限 | 作用 | 适用层级 |

|---|---|---|

| SELECT | 查询数据 | 库/表/列 |

| INSERT | 插入数据 | 库/表/列 |

| UPDATE | 更新数据 | 库/表/列 |

| DELETE | 删除数据 | 库/表 |

| CREATE | 建库/建表 | 库/表 |

| DROP | 删库/删表 | 库/表 |

| ALTER | 修改表结构 | 库/表 |

| INDEX | 建/删索引 | 库/表 |

| GRANT OPTION | 转授权限 | 库/表/过程 |

| ALL PRIVILEGES | 除 GRANT OPTION 外的全部 | 视层级 |

| 命令 | 作用 |

|---|---|

| CREATE USER u@h IDENTIFIED BY 'p'; | 建账号 |

| ALTER USER u@h IDENTIFIED BY 'p'; | 改密码 |

| GRANT ... ON db.* TO u@h; | 授权 |

| REVOKE ... ON db.* FROM u@h; | 撤权 |

| SHOW GRANTS FOR u@h; | 查看权限 |

| DROP USER u@h; | 删账号 |

| FLUSH PRIVILEGES; | 重载权限表 |

记忆口诀:先建号、再授权、撤权要对齐、删除用 DROP。日常运维里最常用的就是 SHOW GRANTS 和 GRANT/REVOKE 三件套。

8 安全实践建议

1. root 只允许本机登录:root'@'localhost',不要建 root'@'%'。

2. 禁用远程 root:任何能从外网连到 3306 还能用 root 的做法都是事故温床。

3. 密码强度:开启 validate_password 组件,强制长度、大小写、数字、特殊字符。

4. 账号按用途拆分:应用读写、报表只读、运维各用各的账号,互不混用。

5. 最小权限:能给列级不给表级,能给表级不给库级。

6. 定期审计:定期跑 SELECT user,host FROM mysql.user; 和 SHOW GRANTS,清理离职/废弃账号。

7. 不要在代码里硬编码 root 密码,用配置中心或环境变量。

8. 定期做账号审计:每季度导出一次 mysql.user 和 SHOW GRANTS,对照在职人员清单核对,清理离职账号和废弃临时账号。权限管理是个持续动作,不是一次性配置。

-- MySQL 8.0 安装 validate_password 组件并设置强度策略
INSTALL COMPONENT 'file://component_validate_password';
SET GLOBAL validate_password.policy = 'STRONG';   -- 强策略
SET GLOBAL validate_password.length = 12;        -- 最少 12 位

8.1 账号与权限的存储结构

MySQL 把账号和权限存在 mysql 系统库的几张表里,理解这张图能帮你排查"为什么我授权了却不生效":

| 系统表 | 存什么 |

|---|---|

| mysql.user | 全局账号、密码、全局权限 |

| mysql.db | 数据库级权限 |

| mysql.tables_priv | 表级权限 |

| mysql.columns_priv | 列级权限 |

| mysql.procs_priv | 存储过程/函数权限 |

权限校验是按"全局 → 库 → 表 → 列"逐层叠加的,最终权限是各层的并集。

-- 直接查系统表看某个账号的库级权限(少用,优先用 SHOW GRANTS)
SELECT db, user, host, Select_priv, Insert_priv, Update_priv
FROM mysql.db WHERE user = 'app';

8.2 密码策略与密码过期

MySQL 8.0 提供了一整套密码管理能力,适合企业级合规要求:

-- 让某个账号的密码过期,下次登录强制改
ALTER USER 'app'@'localhost' PASSWORD EXPIRE;

-- 密码改了之后不能立刻重用最近 5 次
ALTER USER 'app'@'localhost' PASSWORD HISTORY 5;

-- 账号锁定:连续输错 3 次密码锁 1 天
CREATE USER 'ops'@'10.0.0.%' IDENTIFIED BY 'Ops@2026'
    FAILED_LOGIN_ATTEMPTS 3 PASSWORD_LOCK_TIME 1;

8.3 角色 Role(MySQL 8.0 新特性)

当账号多了,逐个 GRANT 很烦。MySQL 8.0 引入角色(Role):把一组权限打包成一个角色,再把角色授予账号。

-- 1. 创建角色
CREATE ROLE 'shop_read', 'shop_write';

-- 2. 给角色授权
GRANT SELECT ON shop.* TO 'shop_read';
GRANT SELECT, INSERT, UPDATE, DELETE ON shop.* TO 'shop_write';

-- 3. 把角色授予账号
GRANT 'shop_read' TO 'report'@'10.0.0.%';
GRANT 'shop_write' TO 'app'@'10.0.0.%';

-- 4. 账号登录后激活角色(或设为默认激活)
SET DEFAULT ROLE ALL TO 'report'@'10.0.0.%';

这样以后权限调整只要改角色,所有挂这个角色的账号自动生效,管理成本大幅下降。

8.4 审计账号的日常命令清单

| 操作 | 命令 |

|---|---|

| 列全部账号 | SELECT user,host FROM mysql.user; |

| 看某账号权限 | SHOW GRANTS FOR 'u'@'h'; |

| 看当前会话权限 | SHOW GRANTS; |

| 删账号 | DROP USER 'u'@'h'; |

| 改密码 | ALTER USER 'u'@'h' IDENTIFIED BY 'p'; |

| 踢连接 | KILL <id>; |

9 常见坑与注意事项

坑 1:建号时忘了写主机部分,结果变成 `'app'@'%'`。

CREATE USER 'app' IDENTIFIED BY '...' 等价于 'app'@'%',意味着全世界都能连。生产环境必须显式写明主机或网段。

坑 2:以为 GANK 后立即对已连接会话生效。

GRANT/REVOKE 只影响新建立的连接,已经连上的会话权限不变。要让对方立刻感受到,需要 KILL 掉旧连接。

坑 3:8.0 里还在用 GRANT 隐式建号。

MySQL 8.0 已经不支持"GRANT 不存在的用户自动创建"(需要 sql_mode 配合,且不推荐)。必须先 CREATE USER 再 GRANT。

-- 8.0 下这句会报错:You must have privileges to create user
-- GRANT SELECT ON shop.* TO 'nobody'@'localhost' IDENTIFIED BY 'x';
-- 正确分两步:
CREATE USER 'nobody'@'localhost' IDENTIFIED BY 'Xx@2026';
GRANT SELECT ON shop.* TO 'nobody'@'localhost';

坑 4:密码里有特殊字符导致命令行登录失败。

!、# 在 shell 里有特殊含义,命令行传密码时要么用 --password=xxx 加引号,要么干脆只敲 -p 让它交互式输入。

坑 5:把 DROP/ALTER 权限给了应用账号。

应用被 SQL 注入后,攻击者 DROP TABLE 都能执行。应用账号只给 SELECT/INSERT/UPDATE/DELETE,结构变更走 DBA 工单。

坑 6:用 `%` 通配主机却误以为很安全。

'app'@'%' 意味着任意 IP 都能尝试连接,哪怕有密码,也暴露在暴力破解风险下。必须限定到具体网段。

坑 7:忘记回收离职人员的临时账号。

开通容易收回难,临时账号往往被遗忘。建议每季度跑一次账号审计,对照在职名单清理。

10 练习题

练习 1:创建一个账号 crm_app,允许从 172.16.0.% 登录,密码 Crm@2026#Sec,并只给它 crm_db 库的 SELECT/INSERT/UPDATE 权限。

<details>

<summary>参考答案</summary>

CREATE USER 'crm_app'@'172.16.0.%' IDENTIFIED BY 'Crm@2026#Sec';
GRANT SELECT, INSERT, UPDATE ON crm_db.* TO 'crm_app'@'172.16.0.%';
SHOW GRANTS FOR 'crm_app'@'172.16.0.%';

</details>

练习 2:上面那个账号后来不需要 INSERT 权限了,怎么收回?

<details>

<summary>参考答案</summary>

REVOKE INSERT ON crm_db.* FROM 'crm_app'@'172.16.0.%';

注意 REVOKE 的粒度要和当初 GRANT 一致,都是 crm_db.*。

</details>

练习 3:'app'@'localhost' 和 'app'@'%' 是同一个账号吗?为什么?

<details>

<summary>参考答案</summary>

不是。MySQL 账号由"用户名+主机"共同决定,它们是两个独立账号,可以有不同密码和权限。登录时 MySQL 按主机匹配度选择用哪一个,localhost 比 % 更具体、优先级更高。

</details>

练习 4:为什么不建议给应用账号 GRANT ALL PRIVILEGES ON *.*?

<details>

<summary>参考答案</summary>

违背最小权限原则。一旦应用被 SQL 注入或代码泄露,攻击者可以 DROP DATABASE、读任意库、甚至用 FILE 权限读写服务器文件。应只给业务库的常用 DML 权限。

</details>

练习 5:改了某个账号的密码,为什么它还能连着旧密码继续操作?

<details>

<summary>参考答案</summary>

改密码只影响之后新建立的连接,已经建立的会话仍然有效。要立刻踢掉,需要 SHOW PROCESSLIST 找到连接 ID,再 KILL <id>。

</details>

练习 6:MySQL 8.0 的角色(Role)解决了什么问题?怎么用?

<details>

<summary>参考答案</summary>

账号多了以后逐个 GRANT 维护成本高。Role 把一组权限打包,再授予多个账号,改权限只改角色即可。用法:CREATE ROLE → GRANT 权限给角色 → GRANT 角色给账号。

CREATE ROLE 'rw';
GRANT SELECT,INSERT,UPDATE ON shop.* TO 'rw';
GRANT 'rw' TO 'app'@'10.0.0.%';

</details>

练习 7:为什么不建议给应用账号 FILE 权限?

<details>

<summary>参考答案</summary>

FILE 权限允许 MySQL 读写服务器本地文件(LOAD DATA INFILE / SELECT ... INTO OUTFILE)。一旦应用被注入,攻击者可以读服务器敏感文件、写 webshell,等于拿了服务器的文件系统控制权。应用账号绝不能给 FILE、PROCESS、SUPER 这类全局权限。

</details>

练习 8:GRANT SELECT ON shop.* TO 'a'@'%'; 之后为什么还要 FLUSH PRIVILEGES?什么时候不需要?

<details>

<summary>参考答案</summary>

用 GRANT/CREATE USER 等正规语句授权后,MySQL 会自动把权限加载到内存,不需要 FLUSH。FLUSH PRIVILEGES 只在你直接用 SQL 改了 mysql.user 这类系统表(绕过 GRANT)时才需要,让修改重新生效。养成用 GRANT 而不是直接改系统表的习惯,就基本用不到 FLUSH。

</details>

本篇小结:权限管理的精髓就两条——账号按用途拆分、权限按最小粒度授予。记住"用户名+主机"才是完整账号,授权后已建立的连接要 KILL 才生效;账号多了用 Role 统一管理。

>

安全红线:root 绝不远程登录,应用账号绝不给 FILE/SUPER 等全局权限。

下一篇预告:第 16 章 备份与恢复

— 全文完 —回到顶部 ↑
下载推广海报

文章推广海报

《第 15 章 用户与权限管理》完整推广海报
DISCUSSION

文章回复

0 条公开回复
未登录回复需要审核后公开
还没有回复,欢迎参与讨论。