网关层拦截比空值缓存更有效,因其在请求转发前毫秒级拒绝非法请求,避免穿透至Redis/DB;空值缓存则是事后补救,每个新key仍需完整链路查询。

直接在 Envoy 网关层做参数校验,能拦下 70% 以上的穿透请求,根本不用让流量进业务服务——更别提打到 Redis 或数据库了。
为什么网关层拦截比缓存层空值缓存更有效
空值缓存(比如 SET user:999999 "" EX 60)本质是“事后补救”:请求已经穿过网关、到达业务服务、查了 Redis、又查了 DB、最后才写一个空值。而攻击者用不同 key 扫描时,每个新 key 都要走一遍这套链路,QPS 上去后 Redis 和 DB 同时承压。
网关层拦截是“事前掐断”:在 TLS 解密后、路由转发前就完成规则匹配,毫秒级拒绝,不占业务线程、不发 Redis 命令、不建 DB 连接。
- 校验发生在
envoy.filters.http.ext_authz或自定义 Lua filter 中,早于任何 upstream 路由 - 无效请求返回
400 Bad Request或422 Unprocessable Entity,不计入限流统计(避免误伤) - 规则可热更新(通过 xDS 或文件监听),无需重启网关
用 Envoy 的 Lua filter 校验 ID 类参数
以用户查询接口 /api/v1/users/{id} 为例,ID 必须为正整数且 ≤ 10^8。在 Envoy 配置中嵌入 Lua 脚本:
http_filters:
- name: envoy.filters.http.lua
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua
default_source_code: |
function envoy_on_request(request_handle)
local path = request_handle:headers():get(":path")
if string.match(path, "^/api/v1/users/%d+$") then
local id_str = string.match(path, "/api/v1/users/(%d+)")
local id = tonumber(id_str)
if not id or id <= 0 or id > 100000000 then
request_handle:sendLocalResponse(400, "invalid user id", nil, "application/json", 0)
return
end
end
end注意点:
- 正则
"^/api/v1/users/%d+$"必须锚定开头和结尾,防止/api/v1/users/123abc逃逸 -
tonumber()失败返回nil,不能只判== 0 - 不要在 Lua 中调用外部 HTTP 服务或 Redis——它运行在 C++ 主线程,阻塞即全网关卡死
结合 ext_authz 做更复杂的业务规则校验
当需要校验前缀、格式组合(如订单号必须是 ORD- 开头 + 12 位数字)、或实时白名单时,Lua 不够用,应走 ext_authz 调外部鉴权服务:
- 请求头中提取
x-order-id,转发给鉴权服务校验是否符合^ORD-\d{12}$且不在黑名单 - 鉴权服务返回
403时,Envoy 直接拦截,不发往 upstream - 务必设置
timeout: 50ms和failure_mode_allow: false,避免鉴权服务挂掉导致全量放行 - 不要把校验逻辑写在业务服务里——那已经晚了,Redis 穿透压力早就产生了
别忽略路径参数与查询参数的双重校验
很多穿透攻击混用路径和 query,比如 /api/v1/products/123?category_id=-1。光校验路径 ID 不够:
- 对 query 参数,用
envoy.filters.http.header_to_metadata提取category_id到 metadata,再由 Lua 或 ext_authz 检查范围 - 禁止传入明显非法值:
id=-1、page=9999999、sort=;cat+/etc/passwd - 所有校验必须在
envoy.filters.http.router之前执行,否则路由一匹配,请求就发出去了
真正难的不是写几行 Lua,而是把业务语义(比如“分类 ID 只能是 1~20”)固化成网关可执行的规则,并确保它永远跑在缓存逻辑之前——这点一旦漏掉,前面所有防护都白搭。


















