一、为什么需要请求过滤

在Web开发中,我们经常会遇到一些恶意请求或者不符合业务逻辑的请求。比如,某些爬虫会疯狂抓取数据,或者黑客尝试通过构造特殊参数进行SQL注入攻击。如果不对这些请求进行拦截,轻则影响服务器性能,重则可能导致数据泄露甚至系统崩溃。

OpenResty作为一个基于Nginx和Lua的高性能Web平台,非常适合用来做请求过滤。它可以在请求到达后端服务之前,就对URI、请求头、参数等进行快速判断,决定是否放行或者拦截。

二、OpenResty的基本请求过滤实现

OpenResty的请求过滤主要依赖Lua脚本,我们可以通过access_by_lua_block在Nginx的access阶段进行逻辑处理。下面是一个最简单的示例,拦截所有访问/admin路径的请求:

location /admin {
    access_by_lua_block {
        ngx.exit(403)  -- 直接返回403禁止访问
    }
}

这个例子虽然简单,但已经展示了OpenResty的基本能力——在Nginx层面快速拦截请求,而不需要走到后端服务。

三、基于URI的请求过滤

URI过滤是最常见的需求之一。比如,我们想要阻止某些敏感路径的访问,或者只允许特定前缀的请求通过。

location / {
    access_by_lua_block {
        local uri = ngx.var.uri  -- 获取当前请求的URI
        
        -- 如果URI包含"admin"或"config",则拦截
        if string.find(uri, "admin") or string.find(uri, "config") then
            ngx.exit(403)
        end
        
        -- 允许其他请求继续处理
    }
}

这个脚本会检查URI是否包含adminconfig关键字,如果包含则直接返回403。

四、基于请求头的过滤

某些恶意请求可能会携带特殊的Header,比如User-Agent是某个已知的爬虫工具,或者请求头里缺少必要的认证信息。我们可以通过检查请求头来进行过滤。

location / {
    access_by_lua_block {
        local headers = ngx.req.get_headers()  -- 获取所有请求头
        
        -- 检查User-Agent是否是某个爬虫
        if headers["User-Agent"] == "BadBot/1.0" then
            ngx.exit(403)
        end
        
        -- 检查是否缺少Authorization头
        if not headers["Authorization"] then
            ngx.exit(401)
        end
    }
}

这个脚本会检查User-Agent是否为BadBot/1.0,如果是则拦截;同时,如果请求没有Authorization头,则返回401未授权。

五、基于请求参数的过滤

请求参数(Query String或POST Body)往往是攻击者的重点目标。比如SQL注入、XSS攻击等都可能通过参数传递。我们可以对参数进行检查,拦截可疑请求。

location /api {
    access_by_lua_block {
        local args = ngx.req.get_uri_args()  -- 获取GET参数
        
        -- 检查是否有SQL注入特征
        for key, val in pairs(args) do
            if string.find(val:lower(), "select%s+.+from") then
                ngx.exit(400)
            end
        end
        
        -- 如果是POST请求,检查Body
        if ngx.req.get_method() == "POST" then
            ngx.req.read_body()
            local post_args = ngx.req.get_post_args()
            
            -- 检查是否有XSS特征
            for key, val in pairs(post_args) do
                if string.find(val:lower(), "<script>") then
                    ngx.exit(400)
                end
            end
        end
    }
}

这个脚本会检查GET和POST参数,如果发现select * from(SQL注入特征)或<script>(XSS特征),则返回400错误。

六、结合Redis实现动态规则

硬编码的过滤规则虽然简单,但不够灵活。我们可以结合Redis存储过滤规则,实现动态更新。

location / {
    access_by_lua_block {
        local redis = require "resty.redis"
        local red = redis:new()
        
        -- 连接Redis
        local ok, err = red:connect("127.0.0.1", 6379)
        if not ok then
            ngx.log(ngx.ERR, "Redis连接失败: ", err)
            return
        end
        
        -- 从Redis获取黑名单URI
        local blocked_uris = red:smembers("blocked_uris")
        for _, uri in ipairs(blocked_uris) do
            if ngx.var.uri == uri then
                ngx.exit(403)
            end
        end
        
        -- 释放Redis连接
        red:close()
    }
}

这个脚本会从Redis的blocked_uris集合中读取黑名单URI,如果当前请求的URI在黑名单中,则拦截。

七、应用场景与技术优缺点

应用场景

  1. 防止爬虫:通过User-Agent或请求频率拦截爬虫。
  2. 安全防护:拦截SQL注入、XSS等攻击请求。
  3. 权限控制:对未授权请求进行快速拦截。

技术优点

  • 高性能:在Nginx层面拦截,不消耗后端资源。
  • 灵活:Lua脚本可以编写复杂逻辑。
  • 动态更新:结合Redis可实现规则热更新。

技术缺点

  • 维护成本:需要编写和调试Lua脚本。
  • 误杀风险:过于严格的规则可能导致合法请求被拦截。

八、注意事项

  1. 日志记录:拦截的请求应该记录日志,便于后续分析。
  2. 灰度发布:新规则建议先灰度发布,观察效果。
  3. 性能监控:频繁的Lua操作可能影响性能,需监控Nginx的响应时间。

九、总结

OpenResty的请求过滤功能非常强大,可以在不影响后端服务的情况下,快速拦截恶意或非法请求。无论是基于URI、请求头还是参数的过滤,都能通过Lua脚本灵活实现。结合Redis等外部存储,还能做到动态规则更新,非常适合高并发的Web应用。