This repository has been archived on 2026-05-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files

363 lines
12 KiB
Markdown
Raw Permalink Normal View History

2026-05-17 00:06:11 +08:00
---
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 驱动的安全连接配置