--- tags: [MySQL, 安全加固, SSL/TLS, SQL Injection, ACL] create time: 2026-05-16 00:00 --- # 安全加固 ## 概述 数据库安全涉及访问控制、传输加密、数据保护和防御攻击等多个层面。MySQL 内置了丰富的安全机制,但默认配置往往不够严格。本节梳理生产环境必须做的安全加固措施。 ## 最小权限原则 > [!QUESTION] 思考 > 为什么应用账号不应该拥有 `DROP` 或 `ALTER` 权限?如果业务确实需要建表,应该怎么设计? ```sql -- ❌ 最差实践:应用直接用 root 连接(常见于开发环境) GRANT ALL PRIVILEGES ON *.* TO 'app_user'@'%'; -- ✅ 正确做法:最小权限 + 限制来源 IP CREATE USER 'app_user'@'10.0.1.%' IDENTIFIED BY 'strong_password_here'; GRANT SELECT, INSERT, UPDATE, DELETE ON app_db.* TO 'app_user'@'10.0.1.%'; -- ⚠️ 不给 CREATE、DROP、ALTER、FILE、PROCESS、SUPER 等管理权限 FLUSH PRIVILEGES; ``` ### MySQL 角色系统(8.0+) > [!TIP] 角色 vs 直接授权 > 当用户数量增多时,逐个给用户赋权难以维护。角色可以理解为"权限模板",给角色授权后再把角色分配给用户。 ```sql -- 创建角色(权限模板) CREATE ROLE 'app_reader', 'app_writer', 'app_admin'; -- 按角色分配权限 GRANT SELECT ON app_db.* TO 'app_reader'; GRANT SELECT, INSERT, UPDATE, DELETE ON app_db.* TO 'app_writer'; GRANT ALL ON app_db.* TO 'app_admin'; -- 给用户分配角色 GRANT 'app_reader', 'app_writer' TO 'app_user'@'%'; SET DEFAULT ROLE ALL TO 'app_user'@'%'; -- 登录后自动激活这些角色 ``` > [!NOTE] 高危权限清单(绝对不可滥用) > - **FILE**: 可以读取服务器上的任意文件(`LOAD DATA INFILE`)→ 可读取 `/etc/shadow` > - **PROCESS**: 可以看到所有用户的 running query → 泄露业务逻辑和敏感数据 > - **SUPER**: 可以修改全局配置(包括关闭 SSL 要求)→ 可能绕过多层安全防御 > - **SHUTDOWN**: 可以直接关闭数据库 → 造成停机 > - ** grant option**: 可以将自己拥有的权限授予他人 → 权限扩散 > > 应用账号绝对不应该拥有以上任何权限。 ## 账户初始化与清理 > [!QUESTION] 你以为干净的数据库,真的干净吗? > MySQL 初始化后会默认创建一些不安全的账户和数据库,很多团队忽略了这一步。 ```sql -- 初始化后必须执行的操作: DROP DATABASE IF EXISTS test; -- 默认的 test 库任何人都可以访问 DROP USER ''@'localhost'; -- 删除匿名账户(无密码即可登录) DROP USER 'root'@'%' ; -- 删除 root 的远程访问权限 -- 检查残留账户 SELECT user, host, plugin FROM mysql.user WHERE user = ''; -- 确保 root 只能从本地登录 RENAME USER 'root'@'%' TO 'root'@'localhost'; ``` ## 密码安全 > [!QUESTION] 你的密码策略能挡住暴力破解吗? > `123456`、`password`、`admin123` 这类弱口令是黑客的首选攻击目标。 ```sql -- 启用密码强度验证插件 INSTALL COMPONENT 'file://component_validate_password'; -- 配置密码策略 SET GLOBAL validate_password.policy = MEDIUM; -- LOW/MEDIUM/STRONG SET GLOBAL validate_password.length = 12; -- 最小长度建议 ≥ 16 SET GLOBAL validate_password.mixed_case_count = 1; -- 大小写混合 SET GLOBAL validate_password.number_count = 1; -- 数字要求 SET GLOBAL validate_password.special_char_count = 1; -- 特殊字符 -- 强制现有用户使用强密码并定期轮换 ALTER USER 'app_user'@'%' PASSWORD EXPIRE INTERVAL 90 DAY; -- 90 天强制更换密码 -- 服务账号可以关闭过期(避免定时任务因密码过期而失败) ALTER USER 'app_user'@'%' PASSWORD EXPIRE NEVER; ``` ## SQL 注入防护 > [!WARNING] 核心原则 > SQL 注入的本质是**将用户输入当作 SQL 代码来执行**。参数化查询通过驱动层发送参数,服务端将其视为纯数据而非可执行语句。 ```mermaid flowchart LR A["恶意输入: admin' OR '1'='1"] --> B{是否参数化?} B -->|❌ 字符串拼接| C["最终 SQL: SELECT * FROM users WHERE name = 'admin' OR '1'='1'"] C --> D["⚠️ 返回所有记录"] B -->|✅ 占位符 ?| E["driver 发送参数作为数据"] E --> F["最终 SQL: SELECT * FROM users WHERE name = ? \n(参数值: admin' OR '1'='1")"] F --> G["✅ 找不到匹配,返回空"] style D fill:#FF6B6B,color:#fff style G fill:#00D866,color:#fff ``` ### 参数化查询(唯一有效的手段) ```go // ❌ 危险拼接(SQL 注入漏洞) query := fmt.Sprintf("SELECT * FROM users WHERE username = '%s'", userInput) db.Query(query) // ✅ 正确:使用占位符(Driver 层处理转义) db.Query("SELECT * FROM users WHERE username = ?", userInput) // ✅ 正确:GORM Prepared Statement(预编译,性能更好) db.Where("username = ?", userInput).First(&user) ``` ```java // Java PreparedStatement(预编译,同一语句可多次执行不同参数) PreparedStatement ps = conn.prepareStatement("SELECT * FROM users WHERE id = ?"); ps.setInt(1, userId); ResultSet rs = ps.executeQuery(); ``` ### 不能参数化的场景及替代方案 > [!IMPORTANT] 参数化只能处理**数据值**,不能处理标识符(表名、列名、排序方向)。 ```sql -- 场景:动态表名(LIKE 'prefix_%') -- ❌ 不能用参数化,因为参数不能用于标识符 SELECT * FROM ? WHERE id = 1; -- 语法错误 -- ✅ 白名单校验 + 反引号包裹 allowedTables := map[string]bool{ "users": true, "orders": true, "products": true, } if !allowedTables[tableName] { return errors.New("invalid table name") } query := fmt.Sprintf("SELECT * FROM `%s` WHERE id = ?", tableName) db.Query(query, id) -- ✅ 动态 ORDER BY 使用白名单 allowedColumns := map[string]bool{"created_at": true, "name": true, "id": true} col := r.URL.Query().Get("order_by") if !allowedColumns[col] { col = "created_at" // 默认排序 } // 再配合 ASC/DESC 白名单 direction := "ASC" if dir := r.URL.Query().Get("direction"); dir == "DESC" { direction = dir } query := fmt.Sprintf("SELECT * FROM users ORDER BY `%s` %s", col, direction) ``` ## 传输加密 ### SSL/TLS 配置 > [!TIP] 为什么需要 TLS? > 内网通信不等于安全。同一台交换机下可以用 tcpdump 抓包,中间人攻击(MITM)在容器化环境中也完全可行。 ```bash # 生成 CA + 服务端证书 mysql_ssl_rsa_setup --datadir=/var/lib/mysql/mysql-ssl # my.cnf 配置服务端 SSL [mysqld] require_secure_transport = ON # 强制所有连接使用 SSL(MySQL 8.0.12+) ssl-ca = /var/lib/mysql/mysql-ssl/ca.pem ssl-cert = /var/lib/mysql/mysql-ssl/server-cert.pem ssl-key = /var/lib/mysql/mysql-ssl/server-key.pem # 可选:禁止不安全的 LOCAL_INFILE(防止通过 LOAD DATA LOCAL 读客户端文件) local-infile = 0 ``` ```go // Go 驱动 SSL 配置(生产环境必须验证证书) import ( "crypto/tls" _ "github.com/go-sql-driver/mysql" ) tlsConfig := &tls.Config{ MinVersion: tls.VersionTLS12, // 不支持 TLS 1.0/1.1 InsecureSkipVerify: false, // ⚠️ 测试环境可用 true,生产必须 false ClientAuth: tls.RequireAndVerifyClientCert, // mTLS:双向认证(可选) } mysql.RegisterTLSConfig("custom", tlsConfig) dsn := "user:pass@tcp(host:3306)/db?tls=custom&parseTime=True" db, _ := sql.Open("mysql", dsn) ``` > [!CHECK] 验证 TLS 是否生效 > ```sql > SHOW SESSION STATUS LIKE 'Ssl_cipher'; > -- 如果返回空,说明当前连接未使用加密 > > SHOW VARIABLES LIKE '%ssl%'; > -- Require_SSL 应为 YES > ``` ### 身份认证插件 > [!NOTE] caching_sha2_password vs mysql_native_password > MySQL 8.0 默认使用 `caching_sha2_password`,它支持 SHA-256 哈希和密码缓存机制(性能更好)。但部分老旧驱动(如 Python MySQLdb < 2.1.0)只支持 `mysql_native_password`。 ```sql -- MySQL 8.0 默认使用 caching_sha2_password(更安全) -- legacy 客户端可能需要切换回 mysql_native_password ALTER USER 'app_user'@'%' IDENTIFIED WITH mysql_native_password BY 'password'; -- 检查当前使用的认证插件 SELECT user, host, plugin, authentication_string FROM mysql.user; -- 统一切换为更安全的方式(推荐) ALTER USER 'app_user'@'%' IDENTIFIED WITH caching_sha2_password BY 'new_strong_password'; ``` ## 信息与版本泄露 > [!QUESTION] 你在帮敌人认识你? > 数据库版本号让攻击者知道该用哪些 CVE 漏洞;主机名可能暴露部署架构。 ```sql -- ❌ 默认暴露了大量信息 SELECT VERSION(), @@hostname, @@basedir; -- 方法1: 使用反向代理隐藏真实端口 -- nginx 配置:将 /db 路径转发到 MySQL,应用通过 HTTP 走 ProxyProtocol -- 方法2: 自定义 error_log 输出格式 [mysqld] log_error_verbosity = 2 # 减少详细错误信息输出 ``` ## 审计日志 > [!IMPORTANT] 通用日志 vs 专用审计 > `general_log` 记录一切(包括普通查询),性能开销巨大。**仅用于临时排查**,不适合长期开启。生产审计推荐使用专用审计插件。 ```sql -- MySQL Enterprise Audit Plugin(商业版) -- 社区版可以用 general_log 作为短期替代方案 -- ⚠️ 开启通用日志(性能开销较大,仅用于临时审计) SET GLOBAL general_log = 'ON'; SET GLOBAL general_log_file = '/var/log/mysql/general.log'; SET GLOBAL general_log = 'OFF'; -- 查完后立即关闭 -- slow_query_log 长期开启(记录慢查询,便于发现异常高频查询) SET GLOBAL slow_query_log = 'ON'; SET GLOBAL long_query_time = 2; -- 超过 2 秒的记录 SET GLOBAL log_queries_not_using_indexes = 'ON'; ``` ### 社区审计方案(DDL/DML 追踪) ```sql -- 记录所有 DDL 操作到自定义审计表 CREATE TABLE ddl_audit ( id BIGINT AUTO_INCREMENT PRIMARY KEY, event_time DATETIME DEFAULT CURRENT_TIMESTAMP, user_host VARCHAR(255), ddl_statement TEXT ); -- DML 行级变更追踪(触发器方式) DELIMITER // CREATE TRIGGER audit_users_insert_after AFTER INSERT ON users FOR EACH ROW BEGIN INSERT INTO dml_audit (action, table_name, row_id, changed_at) VALUES ('INSERT', 'users', NEW.id, NOW()); END// CREATE TRIGGER audit_users_update_after AFTER UPDATE ON users FOR EACH ROW BEGIN IF OLD.status != NEW.status THEN INSERT INTO dml_audit (action, table_name, row_id, old_val, new_val, changed_at) VALUES ('UPDATE', 'users', NEW.id, OLD.status, NEW.status, NOW()); END IF; END// DELIMITER ; -- 使用 EVENT TRIGGER(MySQL 8.0.16+)记录 DDL CREATE EVENT TRIGGER trg_ddl_audit ON OBJECT SCALAR EVENT DDL EXECUTE AS OWNER WHEN (CURRENT_USER() NOT IN ('root')) DO LOG 'DDL detected by ' || CURRENT_USER(); ``` > [!TIP] 开源审计替代方案 > - **MariaDB Audit Plugin**: 免费,支持事件类型过滤 > - **Percona Audit Log Plugin**: Percona Server 内置 > - **MySQL Router + ProxySQL**: 通过网络代理层拦截和记录所有查询 ## 安全加固 Checklist ```mermaid flowchart TD A["🔒 MySQL 安全加固"] --> B["1. 访问控制"] B --> B1["最小权限原则"] B --> B2["限制来源 IP/CIDR"] B --> B3["禁用匿名账户"] A --> C["2. 密码安全"] C --> C1["validate_password 插件"] C --> C2["定期轮换密码"] C --> C3["禁用空密码账户"] A --> D["3. 传输加密"] D --> D1["SSL/TLS 强制"] D --> D2["TLS 1.2+"] D --> D3["禁用 LOAD DATA LOCAL"] A --> E["4. 审计日志"] E --> E1["slow_query_log 常开"] E --> E2["general_log 按需开关"] E --> E3["binlog 备份保护"] A --> F["5. 信息管控"] F --> F1["隐藏版本/主机名"] F --> F2["custom error messages"] F --> F3["关闭 performance_schema 敏感暴露"] A --> G["6. OS & 网络"] G --> G1["mysql 目录 chmod 700"] G --> G2["删除 test 库 + 示例数据"] G --> G3["防火墙限制 3306 端口"] style B1 fill:#00D866,color:#fff style D1 fill:#00B6BC,color:#fff style F1 fill:#FFB800,color:#fff ``` ## 关联笔记 - [[hhs/GORM/01-安装与初始化]] — GORM 连接参数中的安全配置 - [[hhs/DEV/Go-Database]] — Go 驱动的安全连接配置