动态页面缓存对抗CC攻击,本质上是一场资源消耗不对等的博弈。攻击者用极低成本的海量请求,试图耗尽你后端动态脚本和数据库的每一滴算力。而你要做的,不是硬扛,是让这些请求在到达真正的处理逻辑前,就被极其廉价地“挡”回去。这个“挡”的机制,就是缓存。但缓存不是万能的,它的有效性存在一条清晰的物理边界,这条边界由你的业务逻辑、数据实时性要求和缓存命中率共同划定。

很多人以为上了缓存就能防住CC,结果攻击一来,数据库还是瞬间被打挂。问题出在哪儿?出在缓存没有真正命中攻击流量。攻击者不傻,他们不会反复请求同一个URL,而是会带上随机参数、随机Cookie、随机User-Agent,让每一个请求看起来都像是一个全新的、未曾访问过的页面。你的缓存系统一看,哈希键值不匹配,全部穿透,直接落到后端PHP、Java或Python进程上,等于没有缓存。所以,动态页面缓存对抗CC的第一个有效边界,就是缓存键的设计能否覆盖攻击面

要突破这个边界,你需要对请求进行“归一化”处理,再计算缓存键。具体来说,就是在反向代理层或应用层入口,写一段逻辑,把那些明显是随机生成的、不影响页面内容的查询参数剔除掉。比如,攻击者可能加上?cc=random123,如果你的业务逻辑根本不使用这个参数,就在Nginx或Lua脚本里把它干掉,再对剩余部分做哈希。更进一步,对于User-Agent这类头信息,如果你的页面内容并不根据它做差异化输出,就绝对不要把它放进缓存键的计算因子中。你甚至可以强制将来自同一IP段的请求,在短时间内归一到同一个缓存版本,当然这需要谨慎,容易误伤正常用户。这一步的核心思想是:让攻击者构造的“新”请求,在缓存系统眼里依然是“旧”请求

第二个边界,是缓存时长的颗粒度与业务实时性的冲突。动态页面之所以叫动态,是因为它包含用户相关的、实时变化的数据。比如一个电商商品页,价格和库存可能随时在变,但商品描述和图片相对固定。如果你把整个页面缓存5分钟,CC攻击是挡住了,但用户可能看到过期价格,导致投诉。如果你不缓存,或者只缓存1秒,那攻击流量稍微大一点,缓存还没来得及服务下一个请求就失效了,后端依然承受巨大压力。这中间的有效边界在于页面碎片化缓存与异步加载

不要试图缓存整个HTML文档,而是把页面拆解。将那些不常变动、用户不敏感的公共部分(导航栏、页脚、商品图文详情)做较长时间的全量缓存,比如10分钟甚至1小时。而用户登录状态、购物车数量、实时价格这些高度动态的模块,做成异步接口,在页面加载后通过JavaScript二次请求获取。这样,主HTML文档本质上变成了一个静态外壳,可以被CDN和反向代理长时间缓存。CC攻击的流量即使再大,命中的也只是这个静态外壳,回源压力极小。那些动态接口虽然仍需回源,但你可以对它们单独施加更激进的限流和短时间缓存策略,比如针对单个用户ID或IP,对价格查询接口做1到5秒的缓存。1到5秒对于人来说感知不强,但对于每秒成千上万次的CC攻击,却能让后端查询量下降好几个数量级。

第三个边界,是缓存存储和分发的地理位置与攻击流量的物理源头。如果你的缓存节点就在源站服务器本地,比如单机Redis或本地文件缓存,那么即使请求命中了缓存,依然会消耗你服务器的网络带宽和部分I/O资源。面对大流量CC攻击,你的服务器带宽可能先被打满,服务一样瘫痪。真正的有效对抗,必须把缓存边界推到离用户更近的地方,也就是CDN节点。让攻击流量在遍布全球的边缘节点就被吸收和响应,根本抵达不了你的源站。这里的关键技术点是,你需要配置CDN供应商的边缘计算规则,在边缘节点直接执行那些请求归一化逻辑和缓存键计算,而不是等请求回源后再做。这要求你的CDN服务支持类似Edge Workers的功能。

第四个边界,也是最容易被忽视的,是缓存一致性在攻击期间的降级策略。正常情况下,你更新了数据库里的商品信息,会通过消息队列或直接调用来主动清除CDN或应用层的相关缓存,以保证用户立刻看到新数据。但在CC攻击期间,这种主动清除机制本身可能成为攻击者的利用点。攻击者可以伪造大量请求,触发你的缓存清除逻辑,导致缓存频繁失效,穿透率飙升。所以,一个健壮的动态缓存对抗体系,必须包含攻击模式下的缓存策略自动切换。当系统检测到请求量异常飙升,或缓存命中率急剧下降时,应自动进入“防守模式”。在防守模式下,主动缓存清除动作被暂时抑制或延迟执行,缓存过期策略从“主动清除”强制切换为“到期自然失效”。这意味着数据可能会有短暂延迟,但保证了绝大多数请求能被缓存挡住,保全了数据库的性命。这是一种典型的可用性优先于一致性的战术选择。

第五个边界,涉及到计算型CC攻击与缓存的对策。并非所有CC攻击都是简单的HTTP洪水。有一种更阴险的攻击是专门挑选你站点中计算最昂贵的动态页面,比如复杂的搜索查询、大型报表生成、或者需要遍历数据库多表关联的过滤页。攻击者可能只请求几十个不同的搜索词,但每个词都足以让你的数据库CPU飙高。对于这类页面,常规的全页缓存键设计可能不够用,因为搜索词组合千变万化。你需要引入“门控”缓存机制。简单说,就是在请求到达复杂业务逻辑前,先设置一道极速检查。例如,用一个极轻量级的缓存存储所有“已知的恶意搜索模式”或“高频搜索词的合法结果”。对于任何请求,先查这个门控缓存,如果命中恶意模式,直接返回验证码或空白页;如果命中合法高频词,直接返回预计算好的结果;只有当完全不命中时,才放行到后端。同时,对后端执行时间过长的请求进行计数,同一个IP或会话如果在短时间内多次触发慢查询,直接将其判定为攻击源,将其请求特征码加入门控缓存的黑名单,后续请求一律在门控层拦截。这相当于用缓存建立了一个智能的、动态学习的护城河。

实现这些逻辑,在架构上通常离不开高性能的反向代理。以Nginx和OpenResty生态为例,你可以通过编写Lua脚本,非常灵活地实现上述复杂的缓存键处理和门控逻辑。下面这段伪代码展示了在OpenResty的access阶段,如何对请求参数进行清洗并计算缓存键,同时实现一个简单的门控检查。

-- 在 access_by_lua_block 阶段
local uri = ngx.var.uri
local params = ngx.req.get_uri_args()

-- 1. 参数白名单清洗:只保留业务必需的参数
local valid_params = {}
if params["id"] and params["id"]:match("^%d+$") then
    valid_params["id"] = params["id"]
end
-- 忽略所有其他参数,如 ?cc=random123

-- 2. 构建归一化缓存键
local cache_key = uri .. ":" .. require("cjson").encode(valid_params)

-- 3. 门控检查:查询本地共享内存中的黑名单
local blacklist = ngx.shared.blacklist
local client_ip = ngx.var.binary_remote_addr
if blacklist:get(client_ip) then
    ngx.exit(403) -- 直接拒绝
    return
end

-- 4. 尝试从缓存中获取内容
local cache = ngx.shared.page_cache
local cached_content = cache:get(cache_key)
if cached_content then
    ngx.say(cached_content)
    ngx.exit(200)
    return
end

-- 5. 缓存未命中,标记需要回源处理
ngx.var.cache_key = cache_key
ngx.var.client_ip = client_ip
-- 后续由 content_by_lua_block 处理回源逻辑和慢查询计数

这段逻辑的核心价值在于,它在请求进入的毫秒级时间内,就完成了清洗、检查和缓存命中,完全在内存中操作,不产生任何磁盘I/O或数据库连接。这才是对抗CC攻击所需的极致性能。

最后,必须清醒认识到动态页面缓存的终极边界:完全个性化的、无法剥离动态内容的页面。比如一个用户登录后的个人仪表盘,上面全是该用户的待办事项、实时消息、账户余额。这类页面,你几乎无法对主体内容做任何有效的公共缓存。CC攻击如果直接针对这种页面,缓存策略基本失效。面对这种情况,缓存只能退守到辅助位置,你必须依赖其他机制来构筑防线。这包括但不限于:基于用户行为序列的挑战-应答验证(如无感验证码)、前端JavaScript计算的工作量证明、以及更上层的Anycast网络流量清洗。缓存在这里的作用,变成了保护这些验证机制本身不被绕过,以及缓存那些验证通过后生成的令牌,减轻认证服务的压力。这是动态页面缓存对抗CC攻击的最终防线,也是它的能力尽头。越过这条线,就不再是缓存的主战场了。