索引、事务、复制、分库、备份解决的是“能不能跑、跑得快不快、丢不丢数据”的问题;而安全解决的是“谁能进来、进来能干什么、干了什么能不能被追溯”的问题。MySQL 的安全事故,80% 不是被黑客攻破,而是账号权限过大、密码太弱、连接不加密、操作没审计导致的。本文聚焦权限、加密与审计这三根支柱。
一、安全设计的三个原则
在动手改权限之前,先统一认识:
- 最小权限原则:一个账号只拥有完成工作所需的最小权限集合,能只读就不给写,能只访问一个库就不给所有库。
- 纵深防御原则:安全不能只靠某一层,权限 + 密码 + 网络 + 应用层 + 审计要同时生效。
- 可追溯原则:关键操作必须留痕,知道谁在什么时间做了什么,出了问题能回溯。
这三条会贯穿后面的所有配置。
二、MySQL 账号与权限模型
2.1 user@host 的含义
MySQL 的账号不是单纯的用户名,而是 用户名 + 允许登录的主机 的组合。
-- 三个完全不同的账号
'app_user'@'10.0.0.%'
'app_user'@'app-server-01'
'app_user'@'%'
| 主机表达式 | 含义 |
|---|---|
'user'@'localhost' |
只允许本机 socket/tcp 登录 |
'user'@'127.0.0.1' |
只允许本机 TCP 登录 |
'user'@'10.0.0.10' |
只允许指定 IP |
'user'@'10.0.0.%' |
允许 10.0.0.0/24 网段 |
'user'@'%' |
允许任意主机 |
'user'@'%.example.com' |
允许 example.com 子域名 |
是生产中最危险的账号形态。即使内网,也应该按业务网段、主机名或 IP 精确控制。运维跳板机、应用服务器、备份服务器应该分别使用不同账号。
2.2 账号创建与基础授权
-- 创建只允许从 10.0.0.% 登录的只读账号
CREATE USER 'app_read'@'10.0.0.%'
IDENTIFIED BY 'ComplexP@ssw0rd!2026';
-- 授予对 order 库的 SELECT 权限
GRANT SELECT ON order.* TO 'app_read'@'10.0.0.%';
FLUSH PRIVILEGES;
常用授权粒度:
| 粒度 | 示例 | 适用场景 |
|---|---|---|
| 全局 | GRANT SELECT ON *.* |
监控、审计账号 |
| 数据库 | GRANT ALL ON order.* |
业务应用账号 |
| 表级 | GRANT SELECT, INSERT ON order.user |
受限 ETL 账号 |
| 列级 | GRANT SELECT(id, name) ON order.user |
敏感字段脱敏 |
| 存储过程 | GRANT EXECUTE ON PROCEDURE db.proc |
只允许调用指定接口 |
2.3 权限回收
-- 回收某个权限
REVOKE DELETE ON order.* FROM 'app_read'@'10.0.0.%';
-- 删除账号(连同权限)
DROP USER 'app_read'@'10.0.0.%';
在 MySQL 5.7/8.0 中通常不是必须的:CREATE/GRANT/REVOKE 已经会自动刷新内存中的权限表。但在手动改 表时才需要它。
三、生产常用账号模板
一个典型的生产环境,至少应该把账号拆开。
3.1 应用只读账号
CREATE USER 'order_app_ro'@'10.0.10.%'
IDENTIFIED BY 'ro-密码';
GRANT SELECT ON order.* TO 'order_app_ro'@'10.0.10.%';
只读账号不能写,也不能访问其他业务库,即使应用被 SQL 注入,也能把损失控制在只读范围内。
3.2 应用读写账号
CREATE USER 'order_app_rw'@'10.0.10.%'
IDENTIFIED BY 'rw-密码';
GRANT SELECT, INSERT, UPDATE, DELETE ON order.*
TO 'order_app_rw'@'10.0.10.%';
不授予 DROP、ALTER、CREATE 等 DDL 权限,应用不应该在线上做结构变更。
3.3 备份专用账号
CREATE USER 'backup_user'@'10.0.20.%'
IDENTIFIED BY 'backup-密码';
GRANT SELECT, RELOAD, LOCK TABLES, REPLICATION CLIENT, SHOW VIEW
ON *.* TO 'backup_user'@'10.0.20.%';
权限说明:
| 权限 | 作用 |
|---|---|
SELECT |
读取数据 |
RELOAD |
执行 FLUSH TABLES |
LOCK TABLES |
一致性锁定(mysqldump) |
REPLICATION CLIENT |
查看 binlog 位点 |
SHOW VIEW |
导出视图定义 |
3.4 监控与审计账号
CREATE USER 'monitor'@'10.0.30.%'
IDENTIFIED BY 'monitor-密码';
GRANT SELECT ON performance_schema.* TO 'monitor'@'10.0.30.%';
GRANT SELECT ON sys.* TO 'monitor'@'10.0.30.%';
GRANT REPLICATION CLIENT ON *.* TO 'monitor'@'10.0.30.%';
监控账号不需要写权限,也不需要访问业务数据表。
3.5 DBA 管理账号
CREATE USER 'dba_admin'@'10.0.0.10'
IDENTIFIED BY 'dba-强密码';
GRANT ALL PRIVILEGES ON *.* TO 'dba_admin'@'10.0.0.10'
WITH GRANT OPTION;
DBA 账号应该:从固定堡垒机/跳板机 IP 登录;启用审计;禁用 主机;尽量使用 连接。不要把 root 当 DBA 日常账号用。
四、MySQL 8.0 角色(Role)
角色让权限管理从“每人一份权限清单”变成“按岗位分组授权”。
4.1 创建并激活角色
-- 创建角色
CREATE ROLE 'app_read_role', 'app_write_role', 'backup_role';
-- 给角色授权
GRANT SELECT ON order.* TO 'app_read_role';
GRANT SELECT, INSERT, UPDATE, DELETE ON order.* TO 'app_write_role';
GRANT SELECT, RELOAD, LOCK TABLES ON *.* TO 'backup_role';
-- 把角色授予用户
GRANT 'app_read_role' TO 'order_app_ro'@'10.0.10.%';
GRANT 'app_write_role' TO 'order_app_rw'@'10.0.10.%';
-- 设置用户默认激活角色(登录时自动生效)
SET DEFAULT ROLE 'app_read_role' TO 'order_app_ro'@'10.0.10.%';
SET DEFAULT ROLE 'app_write_role' TO 'order_app_rw'@'10.0.10.%';
4.2 激活角色与查看
-- 当前会话激活所有已授权角色
SET ROLE ALL;
-- 查看当前用户的角色和权限
SHOW GRANTS;
SHOW GRANTS FOR CURRENT_USER USING 'app_read_role';
-- 查看角色成员
SELECT * FROM mysql.role_edges;
角色特别适合中台或 SaaS 场景:每个业务线创建一个库,给该库配一组读写/只读角色,新成员入职只授予角色,离职只回收角色,避免在 N 个库里反复改权限。
五、密码策略
弱密码是数据库被入侵最常见的原因之一。MySQL 提供 validate_password 插件强制密码复杂度。
5.1 查看与启用
-- MySQL 8.0 默认已安装
SHOW VARIABLES LIKE 'validate_password%';
-- 关键参数
| Variable_name | Value |
|----------------------------------------|--------|
| validate_password.check_user_name | ON |
| validate_password.length | 8 |
| validate_password.mixed_case_count | 1 |
| validate_password.number_count | 1 |
| validate_password.special_char_count | 1 |
| validate_password.policy | MEDIUM |
5.2 调整策略
SET GLOBAL validate_password.policy = STRONG;
SET GLOBAL validate_password.length = 16;
SET GLOBAL validate_password.mixed_case_count = 1;
SET GLOBAL validate_password.number_count = 1;
SET GLOBAL validate_password.special_char_count = 1;
要永久生效,写入 my.cnf:
[mysqld]
plugin-load-add = validate_password.so
validate_password.policy = STRONG
validate_password.length = 16
validate_password.mixed_case_count = 1
validate_password.number_count = 1
validate_password.special_char_count = 1
5.3 密码过期与历史
-- 创建账号时设置 90 天必须改密码
CREATE USER 'app_user'@'10.0.10.%'
IDENTIFIED BY 'TmpPass!2026'
PASSWORD EXPIRE INTERVAL 90 DAY;
-- 保留最近 6 次密码,禁止复用
ALTER USER 'app_user'@'10.0.10.%' PASSWORD HISTORY 6;
密码过期策略要配合应用发布流程:如果应用账号密码过期而应用没更新配置,会直接连接失败。建议用 Vault、KMS 或配置中心做密码轮转,不要硬编码在配置文件中。
5.4 认证插件选择
MySQL 8.0 默认认证插件从 mysql_native_password 改为 caching_sha2_password,安全性更高,但老旧驱动可能不支持。
-- 查看默认认证插件
SHOW VARIABLES LIKE 'default_authentication_plugin';
-- 为兼容老驱动,指定某用户使用原生密码
CREATE USER 'legacy_app'@'10.0.10.%'
IDENTIFIED WITH mysql_native_password BY '密码';
| 插件 | 安全性 | 兼容性 |
|---|---|---|
caching_sha2_password |
高 | MySQL 8.0+ 驱动 |
mysql_native_password |
中 | 兼容旧驱动 |
sha256_password |
高 | 不推荐新项目 |
新项目优先用 ;只有历史系统确实升级不了驱动时,才为特定账号降级为 。
六、SSL/TLS 加密连接
默认情况下,客户端到 MySQL 的报文是明文的,在内网抓包就能读到 SQL 和结果。开启 SSL 是生产安全基线。
6.1 检查 SSL 状态
SHOW VARIABLES LIKE '%ssl%';
SHOW STATUS LIKE 'Ssl_cipher';
如果 have_ssl 为 YES,说明服务器已启用 SSL。
6.2 服务端启用 SSL
MySQL 8.0 通常自带自签名证书,也可以替换为企业 CA 签发的证书。
[mysqld]
require_secure_transport = ON
ssl_ca = /etc/mysql/ssl/ca.pem
ssl_cert = /etc/mysql/ssl/server-cert.pem
ssl_ssl_key = /etc/mysql/ssl/server-key.pem
require_secure_transport = ON 表示所有连接必须使用 SSL 或 Unix socket,拒绝明文 TCP。
自签名证书可以加密传输,但无法做服务端身份认证。高安全场景建议用内部 CA 签发,并在客户端配置 校验服务器证书。
6.3 客户端强制 SSL
# 命令行
mysql -u app_user -p -h db.example.com --ssl-mode=REQUIRED
# SSL 模式说明
--ssl-mode=DISABLED # 明文
--ssl-mode=PREFERRED # 优先 SSL,失败则明文(默认,不安全)
--ssl-mode=REQUIRED # 必须 SSL,但不校验证书
--ssl-mode=VERIFY_CA # 必须 SSL 且校验 CA
--ssl-mode=VERIFY_IDENTITY # 校验 CA + 主机名
JDBC 连接示例:
String url = "jdbc:mysql://db.example.com:3306/order?" +
"useSSL=true&requireSSL=true&verifyServerCertificate=true&" +
"trustCertificateKeyStoreUrl=file:/path/to/truststore.jks";
6.4 账号级别强制 SSL
-- 该账号必须从 SSL 连接
ALTER USER 'app_user'@'10.0.10.%' REQUIRE SSL;
-- 要求指定证书 CN
ALTER USER 'app_user'@'10.0.10.%' REQUIRE X509;
| REQUIRE 类型 | 含义 |
|---|---|
NONE |
不强制 |
SSL |
必须 SSL |
X509 |
必须 SSL 且客户端提供有效 X509 证书 |
ISSUER '...' |
必须指定 CA 签发 |
SUBJECT '...' |
必须指定证书主题 |
CIPHER '...' |
强制加密套件 |
七、连接层与网络加固
7.1 绑定监听地址
[mysqld]
bind-address = 10.0.0.5
# 或者只监听本机
# bind-address = 127.0.0.1
云服务器不要把 MySQL 端口暴露在公网。即使是内网,也应该放在独立的数据库子网,通过网络 ACL 限制来源。
7.2 关闭危险功能
[mysqld]
# 禁止通过 LOAD DATA INFILE 读取服务器本地任意文件
local_infile = 0
# 关闭符号链接(防止目录遍历)
symbolic-links = 0
# 禁用危险函数(需要安全插件或 SQL 模式配合)
7.3 安全 SQL 模式
SET GLOBAL sql_mode = 'STRICT_TRANS_TABLES,NO_ZERO_DATE,NO_ZERO_IN_DATE,ONLY_FULL_GROUP_BY,ERROR_FOR_DIVISION_BY_ZERO';
另外,可以在个人会话开启安全更新模式,防止误删:
SET sql_safe_updates = ON;
-- 之后没有 WHERE 条件的 UPDATE/DELETE 会被拒绝
7.4 失败登录与连接限制
[mysqld]
# 失败 5 次锁定账号(需安装 connection_control 插件)
connection_control_min_connection_delay = 1000
connection_control_max_connection_delay = 60000
connection_control_failed_connections_threshold = 5
# 最大连接数
max_connections = 1000
max_user_connections = 100
八、审计:知道“谁做了什么”
8.1 企业版审计插件(Enterprise Audit)
MySQL 企业版提供 audit_log 插件,开箱即用:
-- 安装插件(企业版)
INSTALL PLUGIN audit_log SONAME 'audit_log.so';
-- 配置审计策略
SET GLOBAL audit_log_policy = 'ALL'; -- ALL / LOGINS / QUERIES / NONE
8.2 社区版审计方案
社区版没有官方审计插件,常用替代方案:
| 方案 | 原理 | 优缺点 |
|---|---|---|
通用日志 general_log |
记录所有 SQL | 性能开销大,明文敏感信息 |
| 慢查询日志 | 只记录慢 SQL | 无法覆盖全部操作 |
init_connect |
连接时执行审计语句 | 只能记录连接,不记录 SQL |
| Percona Audit Log Plugin | 第三方审计插件 | 功能接近企业版 |
| 数据库审计网关/旁路镜像 | 在网络层抓包解析 | 性能好,部署成本高 |
通用日志
[mysqld]
general_log = 1
general_log_file = /var/log/mysql/general.log
会记录完整 SQL 文本,包括可能的密码参数,开启前务必评估合规要求,并配置好日志轮转和访问权限(600)。
init_connect 轻量方案
[mysqld]
init_connect = "INSERT INTO audit.audit_connection(user,host,login_time,program_name) VALUES (CURRENT_USER(), CONNECTION_ID(), NOW(), @@program_name)"
-- 创建审计连接表
CREATE DATABASE IF NOT EXISTS audit;
CREATE TABLE audit.audit_connection (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user VARCHAR(128),
host VARCHAR(128),
login_time DATETIME,
program_name VARCHAR(128)
);
这种方式只能记录“谁连上来了”,无法记录具体执行的 SQL,适合轻量合规场景。
8.3 审计日志应该关注什么
审计不是为了存所有 SQL,而是为了回答三个问题:
- 谁在访问数据库?(user + host + program_name + 时间)
- 有没有异常权限提升?(GRANT、CREATE USER 操作)
- 有没有危险操作?(DROP、TRUNCATE、DELETE 不带 WHERE、批量导出)
建议把审计日志同步到 SIEM 或 ELK,设置告警规则。
九、应用层防 SQL 注入
数据库权限再严,也拦不住拼接 SQL。防注入的核心是参数化查询。
9.1 错误示例
// 拼接用户输入,极度危险
String sql = "SELECT * FROM user WHERE phone = '" + phone + "'";
如果 phone 传入 ' OR '1'='1,整个条件被改写。
9.2 正确示例
String sql = "SELECT * FROM user WHERE phone = ?";
PreparedStatement ps = conn.prepareStatement(sql);
ps.setString(1, phone);
ResultSet rs = ps.executeQuery();
预编译语句把数据和 SQL 结构分离,任何输入都只被当作参数值处理。
9.3 数据库层补充
-- 应用账号只授予最小权限,即使被注入,也无法 DROP/CREATE
GRANT SELECT, INSERT, UPDATE ON order.* TO 'app_user'@'10.0.10.%';
权限是最后一道闸门,不是第一道防线。安全优先级:参数化查询 > 输入校验 > WAF > 最小权限。
十、账号生命周期管理
10.1 定期盘点账号
-- 列出所有非系统账号
SELECT user, host, plugin, password_expired, account_locked
FROM mysql.user
WHERE user NOT IN ('mysql.infoschema', 'mysql.session', 'mysql.sys', 'root');
10.2 锁定/解锁账号
-- 离职员工账号锁定
ALTER USER 'old_dev'@'10.0.10.%' ACCOUNT LOCK;
-- 重新启用
ALTER USER 'old_dev'@'10.0.10.%' ACCOUNT UNLOCK;
10.3 清理匿名账号和空密码
-- 删除匿名账号
DROP USER ''@'localhost';
DROP USER ''@'%';
-- 给 root 设置强密码并限制 host
ALTER USER 'root'@'localhost' IDENTIFIED BY '强密码';
10.4 密码轮换流程
- 在 KMS/Vault 生成新密码。
- 更新 MySQL 账号密码。
- 更新应用配置中心或环境变量。
- 滚动重启应用实例。
- 旧密码保留观察一个版本周期后废弃。
十一、生产安全 checklist
- [ ] root 账号强密码,且
host限定为localhost或堡垒机 IP。 - [ ] 删除匿名账号、测试账号、空密码账号。
- [ ] 应用账号按“只读 / 读写 / 备份 / 监控 / DBA”拆分。
- [ ] 不使用
'user'@'%'形态,按 IP 段或主机名精确授权。 - [ ] 启用
validate_password并设置 MEDIUM/STRONG 策略。 - [ ] 开启 SSL,账号级别
REQUIRE SSL,禁用明文连接。 - [ ] MySQL 不监听公网,数据库子网通过安全组限制来源。
- [ ] 应用使用参数化查询(PreparedStatement)。
- [ ] 按需开启审计:通用日志、Percona Audit 或网络层审计。
- [ ] 定期 review
mysql.user、mysql.db、mysql.role_edges。 - [ ] 关键账号启用密码过期和登录失败锁定。
- [ ] 备份账号最小权限,备份文件加密并异地保存。
总结
MySQL 安全不是装一个插件或改一个密码就能解决的。它的核心是:
- 权限最小化:谁能连、从哪连、能读还是能写,都要精确控制。
- 传输加密:SSL/TLS 防止内网抓包,是生产基线。
- 操作可审计:关键动作留痕,出了问题可追溯。
- 应用层兜底:参数化查询 + 最小权限,把注入损失降到最低。
把账号体系当成代码一样管理:用角色替代散落的权限,用 KMS 替代配置文件里的明文密码,用审计替代“出了问题再查日志”,才能把数据库从“人人皆可访问的宝库”变成“有门禁、有监控、有闸口的金库”。