这是《MySQL 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 数据库安全不是“设置一个复杂密码”,而是一套持续运行的体系:账号有边界,权限可回收,网络有入口控制,敏感数据有分级,操作有审计,变更可追溯。很多拖库、误授权、SQL 注入和内部滥用,都来自治理缺口,而不是单点漏洞。
本章以 MySQL 8.0 为主线,讲清楚账号与权限模型、角色管理、网络安全、传输加密、审计、敏感数据保护和生产权限治理。
32.1 安全模型总览
入口层
|-- 网络分区 / 安全组 / 防火墙
|-- 只暴露代理或网关
+-- VPN / 堡垒机
|
v
账号层
|-- 最小权限账号
|-- 按应用拆分账号
+-- 密码策略与轮换
|
v
权限层
|-- 库 / 表 / 列 / 存储例程权限
|-- 角色
+-- 定期审计与回收
|
v
数据层
|-- 敏感字段识别
|-- 加密与脱敏
+-- 备份同样受控
|
v
行为层
|-- SQL 审计
|-- 高危操作审批
+-- 异常访问告警
安全设计遵循三个原则:
- 最小权限:只授予当前任务必需的权限;
- 职责分离:开发、DBA、运维、数据分析不共用超级账号;
- 默认拒绝:先不能访问,再按需求精确授权。
32.2 账号与认证
32.2.1 查看账号
SELECT user, host, plugin, account_locked,
password_last_changed
FROM mysql.user
ORDER BY user, host;
MySQL 账号由 user 和 host 共同决定。同一个用户名来自不同主机可以是不同账号,权限也可能不同:
app_user@10.0.%.%
app_user@192.168.1.10
report_user@10.20.%.%
32.2.2 创建应用账号
CREATE USER 'shop_app'@'10.0.%'
IDENTIFIED BY 'StrongPassword_2026'
PASSWORD EXPIRE INTERVAL 90 DAY
FAILED_LOGIN_ATTEMPTS 5
PASSWORD_LOCK_TIME 1;
GRANT SELECT, INSERT, UPDATE, DELETE ON shop.*
TO 'shop_app'@'10.0.%';
应用账号通常不需要 DROP、ALTER、CREATE、GRANT OPTION 和 SUPER。发布系统或 DBA 工具可以使用单独的变更账号。
32.2.3 认证插件
MySQL 8.0 默认认证插件是 caching_sha2_password:
SELECT user, host, plugin FROM mysql.user;
兼容旧客户端或旧驱动时,可能需要临时使用 mysql_native_password:
ALTER USER 'legacy_app'@'10.0.%'
IDENTIFIED WITH mysql_native_password
BY 'StrongPassword_2026';
兼容方案应有退出时间。长期方案是升级驱动和客户端,而不是让数据库永久停留在旧认证体系。
32.2.4 禁止匿名账号与共享账号
检查匿名账号:
SELECT user, host FROM mysql.user WHERE user = '';
删除:
DROP USER ''@'localhost';
共享账号的问题:
- 无法判断真实操作人;
- 权限只会越来越大;
- 密码轮换困难;
- 审计日志失去价值;
- 离职和项目下线无法回收。
32.3 权限体系
32.3.1 权限层级
| 层级 | 示例 |
|---|---|
| 全局 | *.* |
| 数据库 | shop.* |
| 表 | shop.orders |
| 列 | shop.users(phone) |
| 存储例程 | PROCEDURE shop.close_order |
| 代理 | PROXY 权限 |
查看权限:
SHOW GRANTS FOR 'shop_app'@'10.0.%';
32.3.2 常见账号模板
普通业务写入账号:
CREATE USER 'shop_app'@'10.0.%'
IDENTIFIED BY 'StrongPassword_2026';
GRANT SELECT, INSERT, UPDATE, DELETE ON shop.*
TO 'shop_app'@'10.0.%';
只读服务账号:
CREATE USER 'shop_read'@'10.0.%'
IDENTIFIED BY 'StrongPassword_2026';
GRANT SELECT ON shop.* TO 'shop_read'@'10.0.%';
报表账号只给需要的表:
CREATE USER 'report_user'@'10.20.%'
IDENTIFIED BY 'StrongPassword_2026';
GRANT SELECT ON shop.orders TO 'report_user'@'10.20.%';
GRANT SELECT ON shop.order_items TO 'report_user'@'10.20.%';
备份账号:
CREATE USER 'backup_user'@'127.0.0.1'
IDENTIFIED BY 'StrongPassword_2026';
GRANT SELECT, LOCK TABLES, PROCESS, EVENT, TRIGGER
ON *.* TO 'backup_user'@'127.0.0.1';
GRANT RELOAD ON *.* TO 'backup_user'@'127.0.0.1';
不同 MySQL 版本和备份工具对权限要求略有差异,应按工具文档确认,但原则不变:不使用 root 做日常备份。
32.3.3 危险权限清单
| 权限 | 风险 |
|---|---|
ALL PRIVILEGES |
权限边界失控 |
SUPER |
终止会话、绕过只读、修改全局变量 |
DROP |
删除库表和数据 |
GRANT OPTION |
权限二次扩散 |
FILE |
读写服务器文件 |
CREATE USER |
创建后门账号 |
REPLICATION SLAVE |
可拉取数据变更 |
EVENT |
定时执行 SQL |
授权前先问三个问题:
- 这个权限解决什么问题?
- 能否用更小范围替代?
- 到期后如何回收?
32.3.4 角色管理
MySQL 8.0 支持角色,适合把权限按职责打包:
CREATE ROLE 'shop_readonly';
CREATE ROLE 'shop_app_rw';
CREATE ROLE 'shop_dba_change';
GRANT SELECT ON shop.* TO 'shop_readonly';
GRANT SELECT, INSERT, UPDATE, DELETE ON shop.*
TO 'shop_app_rw';
GRANT ALTER, CREATE, INDEX, DROP ON shop.*
TO 'shop_dba_change';
GRANT 'shop_readonly' TO 'report_user'@'10.20.%';
GRANT 'shop_app_rw' TO 'shop_app'@'10.0.%';
启用默认角色:
SET DEFAULT ROLE 'shop_app_rw'
TO 'shop_app'@'10.0.%';
查看权限:
SHOW GRANTS FOR 'shop_app'@'10.0.%';
SHOW GRANTS FOR 'shop_app'@'10.0.%'
USING 'shop_app_rw';
角色应表达职责,例如只读、写入、变更、备份,而不是把大权限换个名字继续扩散。
32.4 网络与传输安全
32.4.1 入口控制
生产建议:
- MySQL 不直接暴露公网;
- 应用通过内网或专线访问;
- 运维通过堡垒机或 VPN 访问;
- 安全组按源 IP 精确放行;
- 从库和备份机遵守同样规则;
- 开发、测试、生产网络隔离。
查看监听与解析配置:
SHOW VARIABLES LIKE 'bind_address';
SHOW VARIABLES LIKE 'skip_name_resolve';
bind_address 限制监听地址,账号 host 限制来源,两者配合使用。skip_name_resolve 可避免 DNS 解析带来的连接延迟,但账号应使用 IP 网段。
32.4.2 TLS 加密
查看连接加密状态:
SHOW STATUS LIKE 'Ssl_cipher';
SHOW VARIABLES LIKE 'have_ssl';
生产至少要求:
- 跨机房、跨云和公网链路启用 TLS;
- 应用与数据库之间启用 TLS;
- 主从复制启用 TLS;
- 客户端校验服务端证书;
- 高安全场景启用双向认证。
强制账号使用 TLS:
ALTER USER 'shop_app'@'10.0.%'
REQUIRE SSL;
双向认证:
ALTER USER 'shop_app'@'10.0.%'
REQUIRE X509;
32.5 SQL 注入防护
错误示例:
String sql = "SELECT id FROM users WHERE name = '" + name + "'";
Statement st = conn.createStatement();
ResultSet rs = st.executeQuery(sql);
攻击者传入 ` ‘ OR ‘1’=’1 ,就可能读取超出预期的数据,甚至通过 UNION`、堆叠语句和时间盲注扩大影响。
正确做法是参数化查询:
String sql = "SELECT id FROM users WHERE name = ?";
try (PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, name);
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
long id = rs.getLong("id");
}
}
}
动态排序字段不能直接拼接用户输入:
List<String> allowedColumns =
List.of("created_at", "amount", "status");
if (!allowedColumns.contains(sortField)) {
throw new IllegalArgumentException("invalid sort field");
}
String sql = "SELECT id, amount FROM orders ORDER BY "
+ sortField + " LIMIT 100";
防护清单:
- 所有值使用参数绑定;
- 动态表名、列名、排序字段使用白名单;
- ORM 中的原生 SQL 同样检查;
- 应用账号没有
FILE、DROP、SUPER权限; - 错误信息不回传 SQL 和堆栈;
- 管理后台做二次鉴权;
- WAF 和扫描器只能作为辅助,不能替代参数化。
32.6 敏感数据保护
32.6.1 数据分级
| 级别 | 示例 | 要求 |
|---|---|---|
| 公开 | 商品名、公告 | 完整性保护 |
| 内部 | 订单金额、用户 ID | 访问控制与审计 |
| 敏感 | 手机号、地址、身份证 | 加密或脱敏 |
| 高敏 | 密码、密钥、支付凭证 | 不可明文存储 |
32.6.2 密码存储
密码必须使用单向自适应哈希算法,不能使用 MD5 或 SHA1:
Argon2id
bcrypt
scrypt
PBKDF2
应用层示例:
String hash = passwordEncoder.encode(rawPassword);
boolean ok = passwordEncoder.matches(rawPassword, storedHash);
数据库只保存哈希值,不保存明文和可逆加密密码。找回密码时应生成一次性令牌,而不是把原密码发给用户。
32.6.3 加密与脱敏
| 方案 | 适用场景 |
|---|---|
| 应用层加密 | 少数高敏字段,数据库只见密文 |
| 透明数据加密 TDE | 防磁盘或备份文件丢失 |
| 传输加密 TLS | 防网络链路窃听 |
| 查询脱敏 | 报表、客服、测试环境 |
| 数据掩码 | 日志和页面展示 |
应用层加密后,等值查询要考虑密文随机性;范围查询和模糊查询会更困难。常见做法是单独保存 HMAC 检索值或维护脱敏索引,但不能为此降低安全边界。
32.6.4 测试数据
生产数据不应直接导入测试环境。应使用:
- 合成数据生成器;
- 静态脱敏工具;
- 按比例抽样并重命名;
- 删除或替换手机号、身份证、银行卡;
- 保留格式,不保留真实值。
32.7 审计与高危操作治理
32.7.1 审计方案
MySQL Enterprise Edition 提供 Audit Log 插件,可以记录连接、查询、DDL、DML 和管理操作:
INSTALL PLUGIN audit_log SONAME 'audit_log.so';
SHOW VARIABLES LIKE 'audit_log%';
社区版可以使用:
- ProxySQL 或数据库网关审计;
- 应用层操作审计;
- Binlog 分析变更;
general_log短期抓取;- 云厂商数据库审计能力。
general_log 可能带来明显性能和磁盘开销,只适合短时间诊断,不适合作为长期审计方案。
32.7.2 高危操作审批
以下操作必须审批:
DROP TABLE / DROP DATABASE
TRUNCATE TABLE
不带 WHERE 的 UPDATE / DELETE
大批量 UPDATE / DELETE
账号创建和授权
权限回收
关闭 Binlog
修改全局参数
主从切换
直接修改线上数据
审批单应包含:
- 业务原因;
- 影响表和行数;
- 执行 SQL;
- 备份点;
- 回滚方案;
- 执行窗口;
- 验证 SQL;
- 负责人。
32.7.3 权限审计
找出高权限账号:
SELECT user, host,
Grant_priv, Super_priv, Drop_priv, File_priv,
Create_user_priv, Repl_slave_priv
FROM mysql.user
WHERE Grant_priv = 'Y'
OR Super_priv = 'Y'
OR Drop_priv = 'Y'
OR File_priv = 'Y'
OR Create_user_priv = 'Y'
ORDER BY user, host;
找出拥有 GRANT OPTION 的授权:
SELECT grantee, privilege_type, is_grantable
FROM information_schema.schema_privileges
WHERE is_grantable = 'YES'
UNION ALL
SELECT grantee, privilege_type, is_grantable
FROM information_schema.table_privileges
WHERE is_grantable = 'YES';
回收:
REVOKE DROP ON shop.* FROM 'shop_app'@'10.0.%';
REVOKE ALL PRIVILEGES, GRANT OPTION
FROM 'old_project'@'10.0.%';
DROP USER 'old_project'@'10.0.%';
32.8 备份与导出安全
备份文件常常是安全治理的盲区。
要求:
- 备份存储私有化,禁止公开读写;
- 传输使用 TLS 或内网专线;
- 备份文件加密;
- 恢复实例也受网络和账号控制;
- 导出数据必须审批;
- 临时导出文件到期删除;
- 备份账号不共享;
- 记录备份访问日志。
示例:
mysqldump --single-transaction --set-gtid-purged=off \
--triggers --routines --events \
-h 127.0.0.1 -u backup_user -p shop > shop.sql
如果导出包含敏感字段,应先写入受控目录,再执行脱敏,不能在个人电脑或共享目录中留存明文副本。
32.9 安全基线清单
| 类别 | 检查项 |
|---|---|
| 账号 | 无匿名账号,无共享账号,无默认弱密码 |
| 权限 | 应用、报表、备份、变更账号分离 |
| 网络 | 不暴露公网,安全组最小放行 |
| 传输 | 客户端、复制、备份链路启用 TLS |
| 数据 | 密码不可逆哈希,敏感字段分级治理 |
| 审计 | 高危操作、授权、导出可追溯 |
| 备份 | 加密存储,访问受控,定期恢复演练 |
| 变更 | DDL 和数据修复有审批与回滚 |
| 监控 | 异常登录、权限变化、大批量导出告警 |
| 应急 | 拖库、误授权、数据泄露预案 |
32.10 事故场景演练
场景一:误授权 ALL
SHOW GRANTS FOR 'shop_app'@'10.0.%';
REVOKE ALL PRIVILEGES, GRANT OPTION
FROM 'shop_app'@'10.0.%';
GRANT SELECT, INSERT, UPDATE, DELETE ON shop.*
TO 'shop_app'@'10.0.%';
复盘时要确认误授权期间是否发生过 DROP、FILE 写入、账号创建或数据导出。
场景二:账号泄露
处理顺序:
- 锁定账号;
- 保留审计和连接日志;
- 通知安全团队;
- 修改凭据并轮换相关密钥;
- 检查异常 SQL 和导出;
- 收紧来源 IP;
- 确认数据影响;
- 输出改进项。
锁定账号:
ALTER USER 'shop_app'@'10.0.%' ACCOUNT LOCK;
场景三:发现 SQL 注入
止血:
- 下线受影响接口;
- 修复参数化查询;
- 收缩应用账号权限;
- 检查是否写入异常数据或文件;
- 分析访问日志确定影响范围;
- 重置可能泄露的凭据;
- 加固后台和上传入口。
本章小结
MySQL 安全治理覆盖账号、权限、网络、传输、数据、审计、备份和高危操作。应用账号必须最小权限并按职责拆分,MySQL 8.0 的角色可以让权限模板更清晰。SQL 注入的第一防线是参数化和白名单,而不是依赖 WAF。敏感数据要分级,密码必须使用单向自适应哈希,备份和导出同样需要纳入安全边界。权限审计和高危操作审批应常态化,才能让安全规则在长期迭代中不失效。
思考题
- 为什么账号由
user和host共同组成? - 应用账号为什么不应该拥有
DROP、FILE和SUPER权限? - 设计一套报表、业务、备份、发布四类账号的权限模板。
- 参数化查询为什么不能直接处理动态排序字段?
- 如果发现生产账号泄露,写出前 30 分钟的处理动作。