Appearance
用户权限与安全
MySQL 权限管理做的是"谁能从哪来、能做什么"。账号不是只有一个用户名,还包括来源地址;权限范围从全局到单列都可以控制;密码、SSL 和审计各管一摊。
一、MySQL 账号 = 用户名 + 来源地址
MySQL 的账号由 user@host 两部分组成。同一个用户名可以对应多个来源:
sql
SELECT user, host FROM mysql.user;输出可能是:
text
'app'@'10.0.%'
'app'@'localhost'
'app'@'%'同一个 app 用户名,匹配哪一种,取决于客户端从哪里连过来。MySQL 的匹配规则会找最精确的那一条——'app'@'10.0.1.15' 比 'app'@'10.0.%' 优先,'app'@'10.0.%' 比 'app'@'%' 优先。
排查权限问题时要同时确认用户名和来源匹配结果:
sql
SELECT USER(), CURRENT_USER();USER() 是客户端说的"我是谁",CURRENT_USER() 是 MySQL 实际匹配到的授权账号。权限问题大多数时候出在后者和预期不一致。
二、创建和管理账号
创建应用账号,同时指定来源网段:
sql
CREATE USER 'app_user'@'10.0.%'
IDENTIFIED BY 'StrongPassword_123!';'10.0.%' 表示这个账号只能从 10.0.x.x 网段连接。比 '%'(允许任何来源)安全得多。
创建只读账号:
sql
CREATE USER 'readonly'@'10.0.%'
IDENTIFIED BY 'ReadonlyPassword_123!';修改密码、锁定和解锁:
sql
ALTER USER 'app_user'@'10.0.%' IDENTIFIED BY 'NewStrongPassword_123!';
ALTER USER 'app_user'@'10.0.%' ACCOUNT LOCK;
ALTER USER 'app_user'@'10.0.%' ACCOUNT UNLOCK;删除账号:
sql
DROP USER 'app_user'@'10.0.%';删除前确认这个账号还有没有活跃连接:
sql
SELECT user, host, db, command, time, state
FROM information_schema.processlist
WHERE user = 'app_user';直接删一个还有大量连接在用的账号,应用会立刻报错。
三、GRANT 和 REVOKE
授应用账号所需的最小权限——单库 DML:
sql
GRANT SELECT, INSERT, UPDATE, DELETE
ON app_db.*
TO 'app_user'@'10.0.%';只读账号只给 SELECT:
sql
GRANT SELECT ON app_db.* TO 'readonly'@'10.0.%';查看某个账号有哪些权限:
sql
SHOW GRANTS FOR 'app_user'@'10.0.%'\G回收权限:
sql
REVOKE DELETE ON app_db.* FROM 'app_user'@'10.0.%';走 CREATE USER、GRANT、REVOKE 这类标准语句时,不需要手动 FLUSH PRIVILEGES。直接修改 mysql.user 这种系统表才需要。生产上更稳的做法是走标准授权语句,不动系统表。
四、权限范围——从全局到单列
MySQL 的授权可以精细到列:
| 范围 | 示例 | 什么时候用 |
|---|---|---|
| 全局 | *.* | 管理员账号,少数情况 |
| 单库 | app_db.* | 应用账号最常见 |
| 单表 | app_db.orders | 某张表需要特殊权限 |
| 单列 | app_db.orders(amount) | 极少数场景 |
| 存储过程 | PROCEDURE app_db.proc_name | 只给执行权限不给数据权限 |
这种授权很危险,等于把整个 MySQL 交给这个应用:
sql
GRANT ALL PRIVILEGES ON *.* TO 'app_user'@'%';应用漏洞或密码泄露后,攻击者能做的事情和 root 差不多。
常用的 MySQL 权限:
| 权限 | 能做什么 | 谁需要 |
|---|---|---|
SELECT、INSERT、UPDATE、DELETE | 基本 DML | 应用账号 |
CREATE、ALTER、DROP | 表结构变更 | DBA 或变更工具 |
INDEX | 创建和删除索引 | DBA 和性能优化工具 |
PROCESS | 查看所有连接线程 | 排查账号、监控工具 |
REPLICATION SLAVE | 从库拉取 binlog | 复制账号 |
REPLICATION CLIENT | 查看主从状态 | 监控和巡检工具 |
SHOW VIEW | 查看视图定义 | 备份和导出工具 |
TRIGGER | 触发器相关 | 备份和特定应用 |
生产应用账号一般不需要 DROP、FILE、SUPER 这类高风险权限。排查用的 PROCESS 权限也单独建排查账号,不混在应用账号里。
五、角色——MySQL 8.0
角色可以把一组权限打包,方便统一授予多个账号:
sql
CREATE ROLE 'app_readonly';
GRANT SELECT ON app_db.* TO 'app_readonly';
GRANT 'app_readonly' TO 'readonly'@'10.0.%';
SET DEFAULT ROLE 'app_readonly' TO 'readonly'@'10.0.%';查看默认角色:
sql
SELECT * FROM mysql.default_roles;角色适合人和工具账号都多的环境——改一次角色权限,所有被授予这个角色的账号都生效。账号数量少时,直接授权更直观。两种方式没有绝对好坏,看环境规模。
六、密码策略和来源限制
查看密码验证组件的配置:
sql
SHOW VARIABLES LIKE 'validate_password%';MySQL 8.0 使用 validate_password 组件来强制密码复杂度。启用方式和参数名可能随版本和安装方式不同而变化。
密码过期和永不过期:
sql
ALTER USER 'app_user'@'10.0.%' PASSWORD EXPIRE;
ALTER USER 'app_user'@'10.0.%' PASSWORD EXPIRE NEVER;应用账号的密码轮换不能直接在库上改完就完——应用在那一瞬间就会因为密码不对而报错。更稳的做法是:先新建一个账号(新密码)、应用切过去、观察连接正常、再删除旧账号。
来源限制是防止横向移动的一层保护。与其用 'app'@'%',不如限制到应用服务器的网段或具体 IP:
sql
CREATE USER 'app_user'@'10.0.1.15' IDENTIFIED BY 'StrongPassword_123!';MySQL 层的来源限制配好,再结合防火墙和安全组,外网来源基本到不了 MySQL 端口。多层的防御让攻击者在任意一环都会被拦住。
七、SSL 连接
数据在网络上明文传输,中间任何节点都可以看到 SQL 内容。SSL 加密客户端和 MySQL 之间的通信。
确认 MySQL 是否编译了 SSL:
sql
SHOW VARIABLES LIKE 'have_ssl';
SHOW VARIABLES LIKE 'ssl_%';要求某个账号必须走 SSL 连接:
sql
ALTER USER 'app_user'@'10.0.%' REQUIRE SSL;客户端连接时指定使用 SSL:
bash
mysql -h mysql.example.com -u app_user -p --ssl-mode=REQUIRED更严格地校验证书——不仅加密,还要确认服务器身份:
bash
mysql -h mysql.example.com -u app_user -p \
--ssl-mode=VERIFY_IDENTITY \
--ssl-ca=/etc/mysql/ca.pemVERIFY_IDENTITY 会校验证书中的主机名和连接的目标主机名是否一致,适合跨网络、跨机房或不可信网络环境的连接。
八、审计——社区版能做到什么
社区版 MySQL 原生审计能力比较有限:
| 方式 | 能做什么 | 限制 |
|---|---|---|
| general log | 记录所有执行的语句 | 开销很大,所有连接的所有 SQL 都记,不适合长期开启 |
| slow log | 记录执行时间超过阈值的 SQL | 只能看慢的,看不到快的恶意操作 |
| binlog | 记录数据变更 | 记录的是行变更,原始 SQL 不一定保留完整 |
| 企业版审计插件 | MySQL Enterprise Audit | 需要企业版 |
| 第三方方案 | MariaDB Audit Plugin、代理层审计、堡垒机/数据库网关 | 需要额外部署 |
临时开启 general log 抓现场:
sql
SET GLOBAL general_log = ON;
SET GLOBAL general_log_file = '/data/mysql/logs/general.log';
-- 抓完后关掉
SET GLOBAL general_log = OFF;general log 会记录包括密码在内的完整 SQL(取决于客户端和连接方式),保存和分享日志前要先检查内容。临时排查可以开,长期审计还是用插件或网关方案更合适。
发生误操作时,单看一种日志通常还原不了完整过程。账号信息(谁登录)、来源地址(从哪里来)、binlog 事件(改了什么行)、应用发布时间线(是谁触发的),这几条线要合在一起看。
九、账号巡检
定期检查有没有异常权限:
查远程 root:
sql
SELECT user, host
FROM mysql.user
WHERE user = 'root'
AND host NOT IN ('localhost', '127.0.0.1', '::1');查有没有空密码账号(authentication_string 为空但 plugin 非 auth_socket 的需要留意):
sql
SELECT user, host, plugin, authentication_string
FROM mysql.user
WHERE authentication_string = '';查高权限账号:
sql
SELECT user, host, Super_priv, File_priv, Grant_priv
FROM mysql.user
WHERE Super_priv = 'Y'
OR File_priv = 'Y'
OR Grant_priv = 'Y';MySQL 8.0 权限拆得更细,SUPER 逐步被动态权限(如 CONNECTION_ADMIN、REPLICATION_APPLIER 等)替代。巡检脚本不能照搬 5.7 的规则,要按实际版本调整检查项。
日常管理的基本原则——每种账号各司其职:
| 账号类型 | 权限范围 | 来源限制 |
|---|---|---|
| 管理员账号 | 全局,但只给少数实名用户 | 特定管理网段或本机 |
| 应用账号 | 单库 DML,不给 DDL | 应用服务器网段 |
| 只读账号 | 只 SELECT | 需要拉数据的服务器 |
| 复制账号 | 只 REPLICATION SLAVE/CLIENT | 从库 IP |
| 备份账号 | SELECT + LOCK TABLES + 必要管理权限 | 备份服务器 |
| 临时账号 | 按需给,设置过期时间 | 限制到具体 IP |