防护CC攻击的核心从来不在于“封掉多少IP”,而在于能否对单IP的请求频次实现动态、精准、可自适应的管控。静态阈值策略在真实攻击面前基本无效,要么误杀正常用户,要么被低速慢速CC轻松穿透。单IP请求频次动态管控的本质,是把每一个来源IP的访问行为建模成一个实时变化的时间序列,通过滑动窗口、令牌桶、指数衰减计数、行为基线偏离度检测等手段,在请求到达业务系统之前完成风险判定和处置。

真正有效的动态频次管控,需要先解决一个基础问题:计数维度不能只按“每秒请求数”这种粗粒度指标。一个IP在1秒内发出5次请求,可能是正常浏览器加载页面资源,也可能是CC攻击的探测。更可靠的维度包括:请求间隔的分布、URL集中度、请求方法异常比例、是否携带合法Cookie、是否执行JS挑战、是否命中敏感接口、单IP并发连接数、TLS握手频率、以及单位时间内的404/403响应占比。把这些特征组合起来,才能让“频次”这个概念从简单的计数升级为行为画像。

动态阈值的核心:基线自学习与偏离度计算

固定阈值“单IP每秒超过20次请求就封禁”是很多入门级防护的做法,但实际场景中,企业内网出口、学校NAT出口、移动运营商CGNAT出口后面可能汇聚了成百上千个真实用户,单IP每秒几十次请求完全正常。反过来,攻击者可以把请求频率控制在每秒3到5次,用大量肉鸡组成低速CC,固定阈值形同虚设。动态管控的第一步,是对每个IP建立短期和长期两个时间维度的行为基线。短期基线用滑动窗口统计最近5分钟、10分钟、30分钟的请求速率、请求间隔均值与方差、热门URL集中度;长期基线则按小时和天聚合,识别该IP在对应时段的正常波动区间。当实时指标偏离基线超过一定倍数,比如请求速率超过该IP同时段历史均值的3倍且持续60秒以上,才触发升级处置。

偏离度计算不能只看单一指标。一个IP请求速率突然升高,但URL分布均匀、请求间隔稳定、全部返回200,可能是用户在批量下载或API调用;如果速率升高同时伴随URL高度集中在登录、搜索、下单等动态接口,且请求间隔呈泊松分布之外的规律性脉冲,基本可以判定为自动化攻击。实践中常用加权评分模型:给请求速率、URL熵值、请求间隔方差、非浏览器特征占比、敏感接口命中率分别赋予权重,计算综合风险分。风险分超过第一档阈值时启用JS挑战或验证码,超过第二档阈值时限制速率或降级服务,超过第三档阈值时临时封禁。每一档阈值本身也根据全局攻击态势动态调整,而不是写死在配置里。

滑动窗口与令牌桶的工程实现要点

滑动窗口计数是实现动态频次管控最基础也最容易被低估的模块。固定窗口计数存在边界突刺问题,比如窗口最后100毫秒涌入大量请求,下个窗口前100毫秒又涌入大量请求,两个固定窗口各自都没超限,但实际200毫秒内的请求量已经远超阈值。滑动窗口通过把时间轴切分为更细的桶,比如每1秒一个桶,统计最近60个桶的累计请求数,可以有效平滑边界问题。工程上一般用Redis的ZSET或者本地环形缓冲区实现。ZSET方案以IP加窗口序号为member,时间戳为score,每次请求写入后删除过期成员,再统计当前窗口内的成员数量。这种方案在分布式网关下天然共享计数,但高并发时Redis写入压力大,需要做本地预聚合,比如每100毫秒批量上报一次计数。

令牌桶算法更适合做速率平滑控制。每个IP维护一个令牌桶,容量设为该IP动态基线的1.5到2倍,令牌以基线速率匀速补充。请求到达时先取令牌,取不到则进入等待队列或直接拒绝。令牌桶的优势在于允许突发流量,只要突发量不超过桶容量,就不会误伤正常用户的集中操作。动态管控的关键在于令牌补充速率不是固定值,而是根据该IP的历史基线和当前风险分实时调整。风险分升高时,补充速率按比例下调,桶容量同步收缩;风险分回落后,速率逐步恢复。这样攻击者即使控制请求频率在固定阈值以下,也会因为令牌补充速率被动态压低而无法维持有效攻击。

基于行为特征的频次动态调整策略

单纯按请求数量做频次管控,在复杂攻击面前会显得很笨拙。一个更有效的思路是把“频次”拆解为多个子频次:动态接口请求频次、静态资源请求频次、POST请求频次、未登录状态请求频次、验证失败后的重试频次、同一会话内的异常跳转频次。攻击者往往集中在动态接口上施压,而正常用户的请求分布相对均衡。可以对动态接口设置更严格的独立频次阈值,比如登录接口单IP每分钟不超过10次,搜索接口单IP每分钟不超过30次,下单接口单IP每分钟不超过5次。这些阈值同样需要动态化,根据该IP的历史行为基线自动调整。一个过去30天每天只登录1次的IP,突然1分钟内尝试登录20次,即使全局阈值是10次,也应该触发更严格的管控。

请求间隔的分布特征是非常有效的判别信号。正常用户的操作间隔通常服从指数分布或对数正态分布,存在明显的随机性;而CC攻击工具产生的请求间隔往往过于均匀,或者呈现固定周期的脉冲模式。可以在网关层记录每个IP最近N次请求的时间戳,计算间隔序列的变异系数、自相关系数、以及间隔分布的熵值。变异系数过低说明间隔过于均匀,自相关系数出现周期性峰值说明存在固定频率的脉冲攻击。这些统计特征的计算开销很小,可以在内存中增量维护,不需要存储完整的时间戳序列。一旦检测到间隔分布异常,即使请求总量不高,也应立即提升该IP的风险等级并缩短频次阈值。

动态处置的阶梯化与自动恢复机制

动态频次管控的处置手段不能只有“放行”和“封禁”两个极端。合理的阶梯化处置包括:正常放行、观察标记、JS无感挑战、验证码挑战、速率限制、降级响应、临时封禁、长期封禁。每一级处置的触发条件由风险分和频次偏离度共同决定,且处置强度与偏离程度成正比。比如风险分在60到75之间时,对该IP的后续请求插入JS挑战,验证浏览器环境是否真实;风险分在75到90之间时,启用验证码并同时将该IP的动态频次阈值下调50%;风险分超过90时,直接返回429或跳转到拦截页面,封禁时长从5分钟起步,随重复触发次数递增。

自动恢复机制与处置同样重要。很多防护系统的问题在于封禁后缺乏合理的解封策略,导致正常用户被长时间误封。动态管控要求每一次处置都带有时效性,封禁时长到达后自动解除,并进入观察期。观察期内该IP的频次阈值仍然保持较低水平,比如正常阈值的50%,如果观察期内行为恢复正常,阈值逐步回升;如果再次触发异常,封禁时长翻倍。同时需要支持基于行为的主动解封,比如被封禁IP如果通过了验证码挑战,可以提前解除封禁但保留风险标记。这样既保证了攻击者无法通过更换IP轻易绕过,又最大限度减少对正常用户的干扰。

分布式架构下的单IP频次一致性保障

在多层负载均衡和分布式网关架构下,单IP的请求可能被分发到不同节点,如果每个节点独立计数,攻击者可以利用请求分散来绕过频次管控。解决这个问题通常有两种路径:集中式计数和分布式一致性计数。集中式计数把所有频次数据写入Redis或专门的计数服务,网关节点每次请求都查询和更新。这种方案实现简单,但高并发下计数服务的性能和可用性会成为瓶颈。更优的做法是本地计数加定期同步:每个网关节点在本地维护滑动窗口计数,每100毫秒到500毫秒将增量数据异步推送到中央聚合服务,同时从中央服务拉取该IP的全局近似计数。本地计数用于实时快速判定,全局计数用于修正本地偏差。这种方案在一致性和性能之间取得了较好平衡。

对于大规模CC攻击,攻击IP数量可能达到数十万甚至百万级,此时计数数据的存储和查询压力巨大。需要引入分级存储和淘汰策略:活跃IP的计数数据保留在内存中,非活跃IP的数据定期刷入磁盘或直接淘汰。对于从未出现过的新IP,首次请求时先赋予一个默认的宽松阈值,同时启动快速学习机制,在短时间内根据其行为特征快速收敛到合理阈值。对于NAT出口或企业出口这类汇聚IP,需要启用更复杂的子标识策略,比如结合User-Agent、Cookie、TLS指纹、HTTP头顺序等特征生成设备指纹,在IP维度之下再做一层设备维度的频次管控,避免误伤同一出口下的其他正常用户。

频次动态管控与业务场景的深度结合

脱离业务场景谈频次管控,很容易陷入“技术正确但业务错误”的困境。不同业务接口对频次的敏感度完全不同:静态资源接口可以容忍很高的频次,登录接口必须严格限制,搜索接口需要平衡用户体验和资源消耗,API接口则需要区分不同调用方的合理配额。动态管控系统应该允许业务方为不同接口配置不同的频次基线和风险权重,同时支持根据业务实时状态自动调整。比如大促期间,下单接口的正常频次会显著上升,此时如果仍然沿用日常阈值,必然导致大量误杀。系统需要能够识别业务高峰,自动放宽对应接口的频次阈值,同时对非业务高峰时段的异常保持警惕。

另一个容易被忽视的点是响应层面的动态调整。当某个IP的频次超过阈值但尚未达到封禁条件时,可以对该IP的响应内容做降级处理:静态资源返回压缩版本或缓存版本,动态接口返回简化数据结构,搜索接口减少返回条数,图片接口降低分辨率。这种降级策略既保护了后端资源,又避免了直接拒绝请求带来的用户体验断崖式下降。攻击者即使维持一定频率的请求,也无法对后端造成实质性压力,因为每个请求消耗的资源被大幅压缩。同时,降级响应本身也可以作为风险信号的一部分,正常用户在遇到降级后通常会降低操作频率,而攻击工具不会感知到降级,仍然保持原有频率,这反过来又强化了行为异常的判定。

# 单IP动态频次管控核心逻辑伪代码
def dynamic_rate_control(ip, request):
    # 更新滑动窗口计数
    sliding_window.add(ip, request.timestamp)
    
    # 计算实时特征
    req_rate = sliding_window.count(ip, window=60)
    url_entropy = calculate_url_entropy(ip, window=300)
    interval_cv = calculate_interval_cv(ip, window=300)
    sensitive_hit = count_sensitive_hits(ip, window=60)
    
    # 获取该IP的动态基线
    baseline = get_baseline(ip, request.hour)
    threshold = baseline.rate * dynamic_factor(ip)
    
    # 计算风险分
    risk_score = weighted_score(
        rate_ratio=req_rate / threshold,
        entropy_anomaly=baseline.entropy - url_entropy,
        interval_anomaly=abs(interval_cv - baseline.interval_cv),
        sensitive_ratio=sensitive_hit / max(req_rate, 1)
    )
    
    # 阶梯化处置
    if risk_score < 40:
        return allow(request)
    elif risk_score < 60:
        return js_challenge(request)
    elif risk_score < 80:
        reduce_threshold(ip, factor=0.5)
        return rate_limit(request)
    else:
        ban_duration = 300 * (violation_count[ip] + 1)
        ban_ip(ip, duration=ban_duration)
        return block(request)

动态频次管控的最终目标不是追求绝对精确的阈值,而是构建一个能够随攻击态势、业务波动、用户行为变化而自动调整的闭环系统。这个系统需要在误杀率和漏杀率之间找到动态平衡点,并且具备快速响应和自动恢复的能力。实现上,滑动窗口和令牌桶解决计数与限速问题,行为基线偏离度检测解决动态阈值问题,阶梯化处置和自动恢复解决用户体验问题,分布式一致性计数解决架构扩展问题。把这几个模块有机组合起来,才能形成真正有效的单IP请求频次动态管控能力。