索引、事务、复制、分库、备份解决的是“能不能跑、跑得快不快、丢不丢数据”的问题;而安全解决的是“谁能进来、进来能干什么、干了什么能不能被追溯”的问题。MySQL 的安全事故,80% 不是被黑客攻破,而是账号权限过大、密码太弱、连接不加密、操作没审计导致的。本文聚焦权限、加密与审计这三根支柱。
一、安全设计的三个原则 在动手改权限之前,先统一认识:
最小权限原则 :一个账号只拥有完成工作所需的最小权限集合,能只读就不给写,能只访问一个库就不给所有库。
纵深防御原则 :安全不能只靠某一层,权限 + 密码 + 网络 + 应用层 + 审计要同时生效。
可追溯原则 :关键操作必须留痕,知道谁在什么时间做了什么,出了问题能回溯。
这三条会贯穿后面的所有配置。
二、MySQL 账号与权限模型 2.1 user@host 的含义 MySQL 的账号不是单纯的用户名,而是 用户名 + 允许登录的主机 的组合。
1 2 3 4 '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 子域名
'user'@'%' 是生产中最危险的账号形态。即使内网,也应该按业务网段、主机名或 IP 精确控制。运维跳板机、应用服务器、备份服务器应该分别使用不同账号。
2.2 账号创建与基础授权 1 2 3 4 5 6 7 8 CREATE USER 'app_read' @'10.0.0.%' IDENTIFIED BY 'ComplexP@ssw0rd!2026' ; 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 权限回收 1 2 3 4 5 REVOKE DELETE ON order.* FROM 'app_read' @'10.0.0.%' ;DROP USER 'app_read' @'10.0.0.%' ;
FLUSH PRIVILEGES 在 MySQL 5.7/8.0 中通常不是必须的:CREATE/GRANT/REVOKE 已经会自动刷新内存中的权限表。但在手动改 mysql.user 表时才需要它。
三、生产常用账号模板 一个典型的生产环境,至少应该把账号拆开。
3.1 应用只读账号 1 2 3 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 应用读写账号 1 2 3 4 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 备份专用账号 1 2 3 4 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 监控与审计账号 1 2 3 4 5 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 管理账号 1 2 3 4 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 登录;启用审计;禁用 % 主机;尽量使用 -ssl-mode=REQUIRED 连接。不要把 root 当 DBA 日常账号用。
四、MySQL 8.0 角色(Role) 角色让权限管理从“每人一份权限清单”变成“按岗位分组授权”。
4.1 创建并激活角色 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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 激活角色与查看 1 2 3 4 5 6 7 8 9 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 查看与启用 1 2 3 4 5 6 7 8 9 10 11 12 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 调整策略 1 2 3 4 5 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:
1 2 3 4 5 6 7 [mysqld] plugin-load-add = validate_password.sovalidate_password.policy = STRONGvalidate_password.length = 16 validate_password.mixed_case_count = 1 validate_password.number_count = 1 validate_password.special_char_count = 1
5.3 密码过期与历史 1 2 3 4 5 6 7 CREATE USER 'app_user' @'10.0.10.%' IDENTIFIED BY 'TmpPass!2026' PASSWORD EXPIRE INTERVAL 90 DAY ; ALTER USER 'app_user' @'10.0.10.%' PASSWORD HISTORY 6 ;
密码过期策略要配合应用发布流程:如果应用账号密码过期而应用没更新配置,会直接连接失败。建议用 Vault、KMS 或配置中心做密码轮转,不要硬编码在配置文件中。
5.4 认证插件选择 MySQL 8.0 默认认证插件从 mysql_native_password 改为 caching_sha2_password,安全性更高,但老旧驱动可能不支持。
1 2 3 4 5 6 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
高
不推荐新项目
新项目优先用 caching_sha2_password;只有历史系统确实升级不了驱动时,才为特定账号降级为 mysql_native_password。
六、SSL/TLS 加密连接 默认情况下,客户端到 MySQL 的报文是明文的,在内网抓包就能读到 SQL 和结果。开启 SSL 是生产安全基线。
6.1 检查 SSL 状态 1 2 SHOW VARIABLES LIKE '%ssl%' ;SHOW STATUS LIKE 'Ssl_cipher' ;
如果 have_ssl 为 YES,说明服务器已启用 SSL。
6.2 服务端启用 SSL MySQL 8.0 通常自带自签名证书,也可以替换为企业 CA 签发的证书。
1 2 3 4 5 [mysqld] require_secure_transport = ON ssl_ca = /etc/mysql/ssl/ca.pemssl_cert = /etc/mysql/ssl/server-cert.pemssl_ssl_key = /etc/mysql/ssl/server-key.pem
require_secure_transport = ON 表示所有连接必须使用 SSL 或 Unix socket,拒绝明文 TCP。
自签名证书可以加密传输,但无法做服务端身份认证。高安全场景建议用内部 CA 签发,并在客户端配置 ssl_ca 校验服务器证书。
6.3 客户端强制 SSL 1 2 3 4 5 6 7 8 9 mysql -u app_user -p -h db.example.com --ssl-mode=REQUIRED --ssl-mode=DISABLED --ssl-mode=PREFERRED --ssl-mode=REQUIRED --ssl-mode=VERIFY_CA --ssl-mode=VERIFY_IDENTITY
JDBC 连接示例:
1 2 3 String url = "jdbc:mysql://db.example.com:3306/order?" + "useSSL=true&requireSSL=true&verifyServerCertificate=true&" + "trustCertificateKeyStoreUrl=file:/path/to/truststore.jks" ;
6.4 账号级别强制 SSL 1 2 3 4 5 ALTER USER 'app_user' @'10.0.10.%' REQUIRE SSL;ALTER USER 'app_user' @'10.0.10.%' REQUIRE X509;
REQUIRE 类型
含义
NONE
不强制
SSL
必须 SSL
X509
必须 SSL 且客户端提供有效 X509 证书
ISSUER '...'
必须指定 CA 签发
SUBJECT '...'
必须指定证书主题
CIPHER '...'
强制加密套件
七、连接层与网络加固 7.1 绑定监听地址 1 2 3 4 [mysqld] bind-address = 10.0 .0.5
云服务器不要把 MySQL 端口暴露在公网。即使是内网,也应该放在独立的数据库子网,通过网络 ACL 限制来源。
7.2 关闭危险功能 1 2 3 4 5 6 7 8 [mysqld] local_infile = 0 symbolic-links = 0
7.3 安全 SQL 模式 1 SET GLOBAL sql_mode = 'STRICT_TRANS_TABLES,NO_ZERO_DATE,NO_ZERO_IN_DATE,ONLY_FULL_GROUP_BY,ERROR_FOR_DIVISION_BY_ZERO' ;
另外,可以在个人会话开启安全更新模式,防止误删:
1 2 SET sql_safe_updates = ON ;
7.4 失败登录与连接限制 1 2 3 4 5 6 7 8 9 [mysqld] 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 插件,开箱即用:
1 2 3 4 5 INSTALL PLUGIN audit_log SONAME 'audit_log.so' ; SET GLOBAL audit_log_policy = 'ALL' ;
8.2 社区版审计方案 社区版没有官方审计插件,常用替代方案:
方案
原理
优缺点
通用日志 general_log
记录所有 SQL
性能开销大,明文敏感信息
慢查询日志
只记录慢 SQL
无法覆盖全部操作
init_connect
连接时执行审计语句
只能记录连接,不记录 SQL
Percona Audit Log Plugin
第三方审计插件
功能接近企业版
数据库审计网关/旁路镜像
在网络层抓包解析
性能好,部署成本高
通用日志 1 2 3 [mysqld] general_log = 1 general_log_file = /var/log/mysql/general.log
general_log 会记录完整 SQL 文本,包括可能的密码参数,开启前务必评估合规要求,并配置好日志轮转和访问权限(600)。
init_connect 轻量方案 1 2 [mysqld] init_connect = "INSERT INTO audit.audit_connection(user,host,login_time,program_name) VALUES (CURRENT_USER(), CONNECTION_ID(), NOW(), @@program_name)"
1 2 3 4 5 6 7 8 9 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 错误示例 1 2 String sql = "SELECT * FROM user WHERE phone = '" + phone + "'" ;
如果 phone 传入 ' OR '1'='1,整个条件被改写。
9.2 正确示例 1 2 3 4 String sql = "SELECT * FROM user WHERE phone = ?" ;PreparedStatement ps = conn.prepareStatement(sql);ps.setString(1 , phone); ResultSet rs = ps.executeQuery();
预编译语句把数据和 SQL 结构分离,任何输入都只被当作参数值处理。
9.3 数据库层补充 1 2 GRANT SELECT , INSERT , UPDATE ON order.* TO 'app_user' @'10.0.10.%' ;
权限是最后一道闸门,不是第一道防线。安全优先级:参数化查询 > 输入校验 > WAF > 最小权限 。
十、账号生命周期管理 10.1 定期盘点账号 1 2 3 4 SELECT user , host, plugin, password_expired, account_lockedFROM mysql.userWHERE user NOT IN ('mysql.infoschema' , 'mysql.session' , 'mysql.sys' , 'root' );
10.2 锁定/解锁账号 1 2 3 4 5 ALTER USER 'old_dev' @'10.0.10.%' ACCOUNT LOCK;ALTER USER 'old_dev' @'10.0.10.%' ACCOUNT UNLOCK;
10.3 清理匿名账号和空密码 1 2 3 4 5 6 DROP USER '' @'localhost' ;DROP USER '' @'%' ;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 替代配置文件里的明文密码,用审计替代“出了问题再查日志”,才能把数据库从“人人皆可访问的宝库”变成“有门禁、有监控、有闸口的金库”。