Appearance
用户权限与安全
MySQL 权限管理做的是"谁能从哪来、能做什么"。账号不是只有一个用户名,还包括来源地址;权限范围从全局到单列都可以控制;密码、SSL 和审计各管一摊。
一、账号与来源地址
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 实际匹配到的授权账号。权限问题大多数时候出在后者和预期不一致——你以为连的是 app@10.0.%,实际匹配成了权限更宽的 app@%。
二、账号管理
创建应用账号,同时指定来源网段:
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 差不多。
几个常用权限要知道用途:SELECT/INSERT/UPDATE/DELETE 是基本 DML(应用账号);CREATE/ALTER/DROP 是表结构变更(DBA 或变更工具);INDEX 创建删除索引;PROCESS 查看所有连接线程(排查账号、监控工具);REPLICATION SLAVE 从库拉取 binlog(复制账号);REPLICATION CLIENT 查看主从状态(监控巡检);SHOW VIEW 查看视图定义(备份导出工具)。
生产应用账号一般不需要 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 里看到 DELETE FROM orders WHERE id=...,只能说明改了什么;再查连接账号是 app_user@10.0.2.15,应用发布时间线里同一分钟刚上线了清理任务,基本能判断是应用逻辑触发,不是 DBA 手工在库里删。账号信息、来源地址、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 |