项目地址:https://github.com/elaysia-feng/NebulaChat.git
一、SQL 注入
1. 定义
-
攻击者把恶意字符串作为输入,拼接进 SQL 语句中。
-
数据库按"正常 SQL"执行这些内容,造成绕过登录、泄露数据、删表等后果。
-
根本原因:用户输入直接参与字符串拼接构造 SQL。
2. 漏洞示例
错误写法(C++ + MySQL):
cpp
std::string sql =
"SELECT id FROM users WHERE name='" + user +
"' AND password='" + pass + "' LIMIT 1";
如果输入:
cpp
user = Elias
pass = 1234' OR 1=1 --
拼接后的 SQL:
bash
SELECT id FROM users
WHERE name='Elias' AND password='1234' OR 1=1 --'
LIMIT 1;
OR 1=1 恒为真,登录验证被绕过。
3. 常见注入手法
-
恒真条件
bash' OR 1=1 -- ' OR 'a'='a -
UNION 联合查询
bash' UNION SELECT id, password FROM users -- -
堆叠查询(多语句执行)
bash'; DROP TABLE users; --
4. 防御方案(按优先级)
4.1 预处理语句(首选)
思路:SQL 结构固定,参数占位符 ?,参数单独绑定,不参与字符串拼接。
SQL 模板:
bash
SELECT id FROM users WHERE name = ? AND password = ?
伪代码流程:
bash
const char* sql =
"SELECT id FROM users WHERE name = ? AND password = ?";
MYSQL_STMT* stmt = mysql_stmt_init(conn);
mysql_stmt_prepare(stmt, sql, strlen(sql));
// 绑定参数(只示意)
MYSQL_BIND bind[2] = {};
// bind[0] 绑定 user,bind[1] 绑定 pass ...
mysql_stmt_bind_param(stmt, bind);
mysql_stmt_execute(stmt);
mysql_stmt_close(stmt);
特点:参数永远当"值",不会当 SQL 代码执行,从根源上防注入。
4.2 转义用户输入(次优)
用于旧代码过渡,仍然是拼接,但先把危险字符转义掉。
封装:
cpp
std::string escapeForSql(MYSQL* conn, const std::string& s) {
if (!conn || s.empty()) return s;
std::string out;
out.resize(s.size() * 2 + 1);
unsigned long len = mysql_real_escape_string(
conn,
&out[0],
s.c_str(),
static_cast<unsigned long>(s.size())
);
out.resize(len);
return out;
}
使用:
cpp
std::string userEsc = escapeForSql(conn, user);
std::string passEsc = escapeForSql(conn, pass);
std::string sql =
"SELECT id FROM users WHERE name='" + userEsc +
"' AND password='" + passEsc + "' LIMIT 1";
4.3 输入校验(辅助)
-
限制长度(如用户名 ≤ 32)。
-
限制字符集(只允许字母 / 数字 / 少量符号)。
-
只能减少攻击面,不能单独用来防注入。
5.总结
遇到
"... WHERE name='" + user + "' ..."这种拼接,就说明写法有问题。 新代码统一用预处理语句,旧代码至少先做转义。
二、Redis 缓存问题总览
Redis 相关常见三个词:
-
缓存穿透:请求的 key,在缓存和数据库里都不存在,请求每次直达数据库。
-
缓存击穿 :某个热点 key 过期瞬间,大量并发同时访问这个 key,一起打到数据库。
-
缓存雪崩:大量 key 在同一时间段集中失效,或者 Redis 整体不可用,导致大量请求直接压到数据库。
下面重点记 缓存击穿 和 缓存雪崩。
三、缓存击穿(Hot Key Breakdown)
1. 场景
-
某个 key 非常热门(例如:热门商品详情、热门直播间信息)。
-
这个 key 设置了过期时间。
-
到期瞬间,大量请求同时访问:
-
缓存中没有
-
大量请求直接查数据库
-
数据库短时间内被"打穿"。
-
2. 解决方案
2.1 互斥锁重建缓存
目标:同一时间只允许一个线程/进程去数据库重建这个 key,其它请求等待或稍后重试。
伪代码:
cpp
val = redis.get(key)
if (val exists) return val
if (!tryLock("lock:" + key)) {
sleep(10~50ms)
return redis.get(key) // 再读一次,读到就直接返回
}
val = db.query(...)
redis.setex(key, ttl, val)
unlock("lock:" + key)
return val
要点:
-
单机可以用本地互斥锁。
-
分布式要用 Redis 分布式锁(SET NX EX)保证只有一台机器在重建。
适用:登录信息、用户数据、房间元信息等要求一致性比较高的场景。
2.2 逻辑过期 + 异步刷新
思路:
-
缓存中保存:
{data, logic_expire_time}。 -
请求到来时:
-
如果
现在时间 < logic_expire_time:直接返回数据。 -
如果已超过逻辑过期时间:
-
仍然先返回旧数据给用户。
-
后台异步启动一个线程从数据库刷新,并更新
logic_expire_time。
-
-
伪代码:
cpp
cache = redis.get(key)
if (cache == null) {
// 首次加载,按普通查询 + setex 处理
}
if (now < cache.logic_expire_time) {
return cache.data
}
// 逻辑过期
if (tryLock("refresh:" + key)) {
async {
val = db.query(...)
redis.set(key, {data: val, logic_expire_time: now + T})
unlock("refresh:" + key)
}
}
return cache.data // 先返回旧值
特点:
-
用户几乎感受不到阻塞。
-
适合读多写少,对"绝对实时"要求不高的业务(如群资料、排行榜)。
四、缓存雪崩(Cache Avalanche)
1. 场景
-
很多 key 在同一时刻或短时间内密集过期,缓存命中率突然大幅下降。
-
大量请求绕过 Redis,直接压到数据库。
-
或者 Redis 故障、集群整体不可用,效果类似。
2. 解决方案
2.1 打散过期时间
不要所有 key 都:
cpp
EX 3600
改为:
cpp
EX 3600 + rand(0, 600)
让失效时间分布在一个区间,避免同一时间集中过期。
2.2 热点数据预热
-
系统启动或大促前,提前把核心数据加载进 Redis。
-
对核心 key 拉长 TTL,避免在高峰期过期。
2.3 多级缓存
-
本地缓存(进程内 LRU / map) + Redis。
-
访问顺序:
-
先查本地缓存;
-
没有再查 Redis;
-
还没有再查数据库。
-
即使 Redis 挂掉,本地缓存还能挡掉一部分重复请求。
2.4 降级与限流
-
Redis 或数据库压力过高时:
-
对非核心接口直接返回默认数据 / 静态页。
-
对热点接口做限流(令牌桶、漏桶、滑动窗口)。
-
-
目标:保护数据库,保证核心功能活着。
五、总结
-
SQL 注入:不要拼接 SQL,用预处理语句,旧代码至少先做转义。
-
缓存击穿:热点 key 过期瞬间被打爆,用互斥锁或逻辑过期 + 异步刷新。
-
缓存雪崩:一堆 key 同时过期或 Redis 整体挂掉,用随机过期时间、多级缓存、预热、降级限流。