4、查询安全:查询语句过滤、查询参数校验、查询结果脱敏、查询频率限制
查询安全这块,说实话,是搜索引擎最容易翻车的地方。
我见过太多团队,把精力都花在索引构建和排序算法上,结果上线第一天就被搞趴下了。为什么?因为查询入口是敞开的,谁都能往里扔东西。你想想看,一个恶意用户往你的搜索框里塞一段 SQL 注入代码,或者一个脚本小子疯狂刷你的搜索接口,你的系统扛得住吗?
我个人习惯把查询安全拆成四个维度来防守:过滤、校验、脱敏、限流。缺一个都不行。
4.1 查询语句过滤:把脏东西挡在门外
查询语句过滤,说白了就是「洗词」。用户输入什么,你不能直接拿去查。
我在项目中遇到过最典型的案例:用户搜索「DROP TABLE users」,结果我们的系统直接把这段文本拼进了查询语句。嗯,后果你懂的。
过滤策略我一般分三层:
- 字符层过滤:干掉特殊字符,比如
; -- ' " /* */等。这些是 SQL 注入和脚本注入的常用武器。 - 关键词层过滤:维护一个黑名单词库,比如
DROP、DELETE、ALTER、EXEC等。一旦命中,直接拒绝或替换。 - 语义层过滤:这个比较高级。比如用户搜索「如何删除数据库」,虽然没触发关键词,但意图可疑。可以降级处理,不给高权限结果。
// 一个简单的查询过滤示例(Java)
public String sanitizeQuery(String rawQuery) {
// 1. 字符层:移除危险字符
String cleaned = rawQuery.replaceAll("[;'\"\\\\]", "");
// 2. 关键词层:检查黑名单
String[] blacklist = {"DROP", "DELETE", "ALTER", "EXEC", "UNION"};
for (String keyword : blacklist) {
if (cleaned.toUpperCase().contains(keyword)) {
throw new SecurityException("查询包含非法关键词: " + keyword);
}
}
return cleaned;
}
4.2 查询参数校验:别让参数变成武器
参数校验和过滤不一样。过滤是洗掉脏东西,校验是检查参数「对不对、合不合理」。
举个例子。用户搜索「北京天气」,传了个参数 pageSize=9999999。你想想看,如果系统不校验,直接去查,内存直接爆掉。
我一般校验这几个维度:
| 参数类型 | 校验规则 | 常见攻击方式 |
|---|---|---|
| 分页参数(page, pageSize) | page >= 1, pageSize 1~100 | 超大 pageSize 导致内存溢出 |
| 排序参数(sort, order) | 只允许白名单字段,order 只能是 asc/desc | 注入恶意排序字段导致慢查询 |
| 过滤参数(filter, tag) | 长度限制、字符集限制 | 超长字符串导致索引失效 |
| 搜索关键词(q, query) | 长度 1~200 字符,禁止控制字符 | 注入特殊语法或正则回溯攻击 |
我曾经遇到过一个线上事故:用户传了一个 sort=title; DROP TABLE logs,我们的排序逻辑直接拼进了 SQL 的 ORDER BY 子句。嗯,那次事故让我养成了一个习惯——排序字段必须走白名单,绝不拼接用户输入。
// 参数校验示例(Python)
def validate_search_params(params):
# 分页校验
page = int(params.get('page', 1))
page_size = int(params.get('page_size', 20))
if page < 1 or page_size < 1 or page_size > 100:
raise ValueError("分页参数不合法")
# 排序校验 - 白名单模式
allowed_sort_fields = ['relevance', 'date', 'title']
sort_field = params.get('sort', 'relevance')
if sort_field not in allowed_sort_fields:
raise ValueError("不支持的排序字段")
# 关键词长度校验
query = params.get('q', '')
if len(query) > 200:
raise ValueError("查询词过长")
return page, page_size, sort_field, query
4.3 查询结果脱敏:不该看的别让人看到
结果脱敏,说白了就是「该藏的藏起来」。
搜索引擎里,索引里存的数据往往比用户能看的多得多。比如用户搜索「张三」,你可能索引里存了张三的手机号、身份证、家庭住址。但这些能直接返回吗?当然不能。
我常用的脱敏策略有几种:
- 字段级脱敏:某些字段直接不返回。比如用户信息中的密码、密钥、内部 ID。
- 部分脱敏:比如手机号显示成
138****1234,身份证显示成110101********1234。 - 权限级脱敏:不同角色看到不同内容。普通用户看不到内部备注,管理员可以。
- 动态脱敏:根据上下文决定是否脱敏。比如搜索自己的信息时,可以看到完整内容;搜索别人时,只能看到脱敏后的。
// 结果脱敏示例(Java)
public SearchResult desensitize(SearchResult result, User user) {
// 1. 手机号脱敏
if (result.getPhone() != null) {
String phone = result.getPhone();
result.setPhone(phone.substring(0, 3) + "****" + phone.substring(7));
}
// 2. 身份证脱敏
if (result.getIdCard() != null) {
String idCard = result.getIdCard();
result.setIdCard(idCard.substring(0, 6) + "********" + idCard.substring(14));
}
// 3. 权限判断:非管理员隐藏内部标签
if (!user.isAdmin()) {
result.setInternalTags(null);
}
return result;
}
4.4 查询频率限制:别让一个人拖垮整个系统
频率限制,说白了就是「限流」。没有限流的搜索引擎,就像没上锁的自行车——谁都能骑走,而且骑坏了算你的。
我见过最夸张的一次:一个爬虫程序,每秒发 5000 个搜索请求,直接把我们的 Elasticsearch 集群打挂了。从那以后,我把限流列为了最高优先级。
常用的限流策略:
- 用户级限流:每个用户每秒最多 N 次查询。比如普通用户 10 次/秒,VIP 用户 50 次/秒。
- IP 级限流:每个 IP 每秒最多 N 次。防止恶意用户换账号刷。
- 接口级限流:整个搜索接口的总 QPS 上限。比如集群扛不住 10000 QPS,那就设个 8000 的阈值。
- 滑动窗口 vs 令牌桶:我推荐用令牌桶算法。突发流量时能平滑处理,不会一刀切。
// 令牌桶限流示例(Python + Redis)
import time
import redis
r = redis.Redis()
def check_rate_limit(user_id, max_requests=10, window_seconds=1):
key = f"rate_limit:{user_id}"
current = r.get(key)
if current is None:
# 第一次请求,设置过期时间
r.setex(key, window_seconds, 1)
return True
elif int(current) < max_requests:
# 未超限,计数+1
r.incr(key)
return True
else:
# 超限了
return False
# 使用示例
if not check_rate_limit("user_123"):
return {"error": "查询过于频繁,请稍后再试"}, 429
嗯,这四个维度加起来,基本能挡住 90% 的查询层攻击。剩下的 10%,靠监控和应急响应来兜底。记住一句话:安全不是一锤子买卖,而是一个持续对抗的过程。