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
⚠️ 注意: 限流不是越严越好。太严了会影响正常用户体验。我建议用「分级限流」——普通用户限得严一点,付费用户宽一点。另外,限流后要给用户友好的提示,别直接返回 500。

嗯,这四个维度加起来,基本能挡住 90% 的查询层攻击。剩下的 10%,靠监控和应急响应来兜底。记住一句话:安全不是一锤子买卖,而是一个持续对抗的过程