基于OpenResty的防CC攻击方案:Lua脚本动态限流实战
摘要:# 当你的网站被“薅羊毛”时,该上点硬核手段了 昨天半夜,一个做电商的朋友火急火燎地给我打电话:“哥,后台又卡死了,页面刷不出来,客服电话都快被打爆了。是不是又被‘攻击’了?” 我让他截图发我看看服务器日志。好家伙,清一色的来自某个IP段的请求,每秒上…
当你的网站被“薅羊毛”时,该上点硬核手段了
昨天半夜,一个做电商的朋友火急火燎地给我打电话:“哥,后台又卡死了,页面刷不出来,客服电话都快被打爆了。是不是又被‘攻击’了?”
我让他截图发我看看服务器日志。好家伙,清一色的来自某个IP段的请求,每秒上百次,全在疯狂访问那个“限时秒杀”的商品详情页。这场景,做互联网的你应该不陌生吧?说白了,这就是典型的CC攻击——不搞垮你服务器,但能让你真正的用户一个都进不来,业务直接“瘫痪”。
很多人第一反应是:上高防!买WAF!但说实话,很多中小公司真扛不住那个成本。而且,很多所谓“智能防护”的PPT做得天花乱坠,真遇到这种针对性的、模拟真人行为的CC攻击,规则稍微配错一点,就可能把正常用户也一并拦在外面,那真是赔了夫人又折兵。
今天,我就想聊点实在的、能自己掌控的“硬核”手段——基于OpenResty的Lua脚本动态限流。这方案不花哨,但就像在你家门前装了个智能门禁,谁在正常敲门,谁在恶意踹门,看得一清二楚。
一、为什么是OpenResty+Lua?—— 把防线推到最前沿
先泼盆冷水。如果你的防护思路还停留在“源站前面套个WAF就万事大吉”,那真得改改了。尤其是CC攻击,它的请求看起来和正常用户几乎一样(一样的HTTP头,一样访问登录页、搜索页),传统基于签名的WAF经常识别不出来。
这时候,你需要的是应用层(第7层)的实时行为分析能力,而且必须在流量到达你脆弱的业务服务器(比如Tomcat、PHP-FPM)之前就进行拦截。
—— OpenResty 正好干这个。
它不是什么新东西,说白了就是Nginx加了个LuaJIT虚拟机。但就这一点,让它从单纯的Web服务器变成了一个强大的Web平台。你可以在Nginx处理请求的11个不同阶段(比如访问控制、内容过滤、日志记录)里,嵌入Lua脚本。这意味着,你可以用代码直接、实时地分析每一个请求,并当场做出决策:是放行、限速,还是直接拒绝。
用个接地气的比喻:传统的防火墙像小区大门保安(只看你是不是小区的人),而OpenResty+Lua的方案,相当于给每栋楼都配了个楼长,他能认出谁天天半夜乱按门铃骚扰邻居,然后当场把他请出去。
二、实战:一个动态限流脚本是怎么“思考”的
理论说多了没劲,我们直接看核心逻辑。下面我拆解一个简化但可用的防CC Lua脚本的核心思路,你可以把它放在 access_by_lua_file 阶段执行。
核心目标:不是一棍子打死,而是精准识别异常会话,并对其进行动态限速或封禁。
-
识别“会话”,而不是IP: CC攻击现在很多都用代理IP池,光封IP没用。我们得更聪明点。通常,我们可以用“IP+UserAgent”作为一个简易会话标识(
session_key)。更严谨的,可以要求必须携带登录后的Token,对未登录的访问关键页面(如秒杀、抽奖)直接进行更严格的限制。-- 获取客户端IP和User-Agent local client_ip = ngx.var.remote_addr local user_agent = ngx.var.http_user_agent or "" local session_key = client_ip .. ":" .. ngx.md5(user_agent) -
选择“记忆”存储——Redis是首选: 计数和限流状态需要在一个共享的、快速的外部存储中。内存字典(
ngx.shared.DICT)只能用于单机,而Redis是分布式的,适合多台OpenResty服务器共享数据。我们就用Redis记录每个session_key在最近一段时间内的请求次数。 -
设计动态规则(这才是灵魂): 别搞静态阈值(比如每秒10次)。攻击者会试探,正常用户也可能突发请求。我们的脚本要能“动态适应”。
- 基线学习:可以设计一个简单的算法,比如统计过去5分钟内所有会话的平均请求频率,作为“正常基线”。
- 异常判定:当前会话的频率,如果超过“基线”的N倍(比如5倍),并且同时超过一个绝对安全阈值(比如每秒60次),就将其标记为异常。
- 阶梯处罚:第一次异常,可能是误伤?那就先记录,并返回一个HTTP 429(Too Many Requests)提示,或者插入一个几秒的延迟。如果同一会话在短时间内连续触发规则,处罚升级——从限速到短时封禁(如5分钟),屡教不改的直接拉黑更长的时间。
-- 伪代码逻辑,展示思路 local current_rate = get_current_request_rate(session_key) -- 从Redis获取当前频率 local baseline_rate = get_baseline_rate() -- 获取动态基线 if current_rate > baseline_rate * 5 and current_rate > 60 then local punish_level = redis:incr("punish:" .. session_key) if punish_level == 1 then -- 首次违规,限速 ngx.sleep(0.5) -- 延迟500毫秒响应 elseif punish_level >= 3 then -- 多次违规,封禁一段时间 ngx.exit(403) -- 或者返回一个错误页面 end end -
别忘了给“误伤”留个后门: 任何自动规则都可能出错。一定要设计一个安全密钥(Secure Key)机制。比如,在URL中加入一个特定参数(
?bypass_cc_check=加密的动态令牌),持有该令牌的请求可以跳过所有CC检查。这个令牌可以来自你的管理后台,在紧急情况下发给需要测试的内部人员或真实用户。
三、落地时,你会踩的坑和我的私货建议
方案听起来不错,但真部署起来,有几个坑我几乎见一个客户踩一个。
-
坑1:Redis成了单点故障和性能瓶颈。
- 我的建议:一定要用Redis集群。并且,Lua脚本里访问Redis必须设置超时和重试,比如用
ngx.timer.at做异步处理,避免因为Redis网络抖动导致Nginx worker进程被卡死。实在不行,降级方案可以先用ngx.shared.DICT顶一下,虽然丢失了全局一致性,但至少保证服务不雪崩。
- 我的建议:一定要用Redis集群。并且,Lua脚本里访问Redis必须设置超时和重试,比如用
-
坑2:规则太严,把促销活动的正常用户也拦了。
- 我的建议:这是最常见的问题。所以我才强调动态基线和阶梯处罚。在活动开始前,用压测工具模拟正常用户流量,跑一下你的脚本,观察“误杀率”。另外,关键业务接口(如下单、支付)要走单独的白名单或更宽松的通道,CC攻击通常集中在浏览型页面。
-
坑3:以为上了这个就高枕无忧了。
- 我的大实话:没有任何单一方案是银弹。基于OpenResty的动态限流,是你的最后一道、也是最精细的应用层防线。它前面,依然需要:
- 高防IP/高防CDN:扛住流量型的DDoS,把洪水挡在机房外。
- Web应用防火墙(WAF):防御SQL注入、XSS等已知漏洞攻击。
- 源站隐藏:让你的真实服务器IP永不暴露。 这套组合拳,才是业务连续性的保障。OpenResty方案是其中那把最锋利的“手术刀”,用于精准切除“CC”这个肿瘤。
- 我的大实话:没有任何单一方案是银弹。基于OpenResty的动态限流,是你的最后一道、也是最精细的应用层防线。它前面,依然需要:
四、写在最后:安全感,来自于对细节的掌控
折腾这一套,图啥?就图个心里踏实。
当你能在监控大屏上,清晰地看到哪个异常会话被自动限流,攻击流量像撞上透明墙壁一样被化解,而正常用户丝滑访问时,那种感觉,就像给自家城堡装上了自动识别的智能箭垛——你知道它在哪里,怎么工作,以及如何控制它。
技术方案从来不是越复杂越好。用OpenResty+Lua,你可能只需要几百行代码,就能构建起一个理解你业务逻辑的智能防护层。它不依赖黑盒AI,规则你说了算,调整起来也快。
当然,这需要你或你的团队对Nginx和Lua有基本的了解。但这份投入,绝对值得。因为在这个时代,业务的脆弱性,往往就体现在你对核心流量失去掌控的那一刻。
行了,方案和坑都摆在这儿了。如果你的网站还在被CC攻击困扰,不妨从看懂你的Nginx日志开始,试试把这把“手术刀”磨锋利吧。

