CC攻击打的就是动态接口,因为静态资源靠CDN就能扛住大半,而登录、注册、支付回调、短信发送、搜索、评论提交这类接口每一次请求都要消耗数据库连接、内存计算或第三方API配额。攻击者用几千个代理IP、每秒几万次请求集中打一个接口,不需要多大带宽就能把后端打到崩溃。所以防护重点不是“限制总流量”,而是对动态接口做精细化的限流策略,让正常用户在突发流量中还能完成核心操作。

先定位哪些接口必须做限流

不是所有接口都值得限流,限流本身也会消耗资源。优先保护三类接口:第一类是会产生外部计费或资源消耗的,比如短信验证码、邮件发送、对象存储上传凭证获取;第二类是计算密集型的,比如复杂查询、报表导出、全文搜索、批量数据转换;第三类是写操作密集型的,比如下单、支付回调、库存扣减、评论发布。这些接口的共同特点是单次请求成本高,或者被打挂后业务损失大。

一个常见的判断标准是:如果某个接口在正常业务高峰期QPS只有50,但攻击时能瞬间拉到5000,且这5000请求都能穿透到数据库或触发外部调用,那这个接口就必须做独立限流,而不是依赖全站统一阈值。

动态接口限流的几个核心维度

只按IP限流在CC攻击面前基本无效,因为攻击者会不断更换代理IP,甚至一个IP只发几个请求。有效的动态接口限流要组合多个维度:

第一,按用户身份限流。已登录用户按用户ID限流,未登录用户按设备指纹或会话ID限流。攻击者可以换IP,但很难批量伪造大量不同的有效用户身份。比如一个用户ID每分钟最多请求短信接口3次,超过就拒绝并触发风控标记。

第二,按接口加业务参数限流。同一个接口,不同的业务参数消耗的资源差异巨大。比如搜索接口,搜索“a”和搜索一个长尾复杂条件对数据库的压力完全不同。可以对参数做哈希或提取关键字段,针对高消耗参数组合单独设置更严格的阈值。

第三,按设备指纹和行为特征限流。结合前端采集的设备指纹、浏览器指纹、鼠标轨迹、按键间隔等特征,识别出自动化脚本。即使IP和用户ID都在变,设备指纹相同或行为模式高度一致,也能触发限流或验证码。

第四,按全局资源水位动态调整。当数据库连接池使用率超过80%,或者某个接口的P99响应时间突然飙升,就自动收紧所有动态接口的限流阈值,优先保证核心交易链路可用。

限流算法的选择与实现

固定窗口计数器最简单,但有临界突刺问题,比如前一个窗口最后一秒和后一个窗口第一秒各放行大量请求,实际两秒内通过的请求数可能翻倍。滑动窗口能解决突刺问题,但实现复杂度和内存占用更高。令牌桶和漏桶算法适合平滑流量,但对突发攻击的响应不够即时。

实际生产环境建议用“滑动窗口计数+令牌桶”的组合:滑动窗口做粗粒度控制,比如每分钟最大请求数;令牌桶做细粒度平滑,比如每秒允许的突发量。对于特别敏感的接口,可以再加一层基于Redis的分布式限流,保证多台应用服务器之间的阈值一致。

下面是一个基于Redis的滑动窗口限流示例,用Lua脚本保证原子性:

-- KEYS[1] 限流key,ARGV[1] 窗口大小(秒),ARGV[2] 最大请求数
local key = KEYS[1]
local window = tonumber(ARGV[1])
local limit = tonumber(ARGV[2])
local now = tonumber(redis.call('TIME')[1])
local window_start = now - window

redis.call('ZREMRANGEBYSCORE', key, 0, window_start)
local current = redis.call('ZCARD', key)

if current < limit then
    redis.call('ZADD', key, now, now .. '-' .. math.random(1000000))
    redis.call('EXPIRE', key, window + 1)
    return 1
else
    return 0
end

这个脚本把每次请求的时间戳作为有序集合的成员,窗口外的记录自动清理,窗口内的数量就是当前请求数。超过阈值直接拒绝,没超过就放行并记录。相比固定窗口,它能精确控制任意时间窗口内的请求量,不会出现临界突刺。

针对代理IP池攻击的识别与对抗

CC攻击者常用的手法是购买大量代理IP,每个IP只发少量请求,让基于IP频率的限流完全失效。对付这种攻击,单靠限流不够,要配合风险识别:

一是建立IP信誉库。把历史上出现过恶意行为的IP段、IDC机房IP段、云服务器IP段标记为高风险。这些IP段不是正常家庭宽带用户,访问动态接口时直接提高限流门槛或要求额外验证。

二是分析请求时序和并发特征。代理IP池攻击虽然IP分散,但请求到达的时间间隔往往高度规律,或者同一批IP在短时间内集中出现。通过统计每个IP的首次出现时间、请求间隔的方差、User-Agent分布等特征,可以识别出机器生成的流量模式。

三是对动态接口启用渐进式验证。当某个IP或用户ID触发限流阈值后,不直接拒绝,而是返回一个需要JavaScript计算或人机交互的挑战。正常用户无感知或只需点一下,自动化脚本如果没有完整的浏览器环境就会失败。这种方式能把攻击流量挡在业务逻辑之外,同时不误伤真实用户。

动态接口限流的架构部署位置

限流放在哪一层,效果差别很大。最外层是CDN或高防IP,适合拦截明显的流量型攻击,但对动态接口的精细限流能力有限。中间层是反向代理或网关,比如Nginx、OpenResty、Kong、APISIX,这一层可以做基于URI、Header、Cookie的限流,性能高,适合作为第一道动态接口防线。

最内层是应用层限流,在业务代码里实现,能拿到用户ID、业务参数等上下文信息,做最精准的控制。但应用层限流有性能开销,而且每个服务都要实现一遍,维护成本高。推荐的做法是:网关层做粗粒度限流和IP信誉过滤,应用层只对核心敏感接口做细粒度限流,两层配合,既能扛住大部分攻击,又不会拖慢正常业务。

对于微服务架构,可以把限流逻辑下沉到服务网格或独立的限流服务中,通过RPC或共享存储统一管理限流规则和计数器。这样不同服务之间的限流策略可以联动,比如用户在某一个服务触发风控后,其他服务也能同步感知。

限流后的响应策略与用户体验

直接返回429或403状态码是最简单的做法,但对用户体验不友好,而且会暴露限流策略给攻击者。更好的做法是分情况处理:对于疑似攻击流量,返回一个空响应或固定内容的200响应,让攻击者误以为请求成功,浪费其资源;对于真实用户被误限的情况,返回带Retry-After头的429响应,并提示稍后重试;对于高风险请求,返回验证码页面或跳转到人机验证。

同时要记录限流触发的详细日志,包括用户ID、IP、接口路径、触发时间、请求参数摘要。这些日志是后续分析攻击特征、调整阈值的重要依据。日志里不要记录敏感信息,比如密码、完整手机号、支付金额等,避免合规风险。

动态调整阈值与自动化运维

限流阈值不是一成不变的。业务高峰期和低谷期的正常流量差异很大,固定阈值要么在高峰期误伤用户,要么在低谷期形同虚设。需要根据历史流量数据建立基线,结合当前系统负载动态调整。比如用过去7天同一时段的流量均值乘以一个系数作为基准阈值,再根据实时数据库连接数、CPU使用率、接口响应时间做微调。

自动化运维方面,可以把限流触发率、拒绝率、误伤率做成监控指标。当某个接口的限流拒绝率突然升高,但系统负载没有明显上升,可能是正常流量增长被误限,需要调高阈值;当拒绝率升高且系统负载也同步飙升,说明攻击正在发生,需要进一步收紧阈值或启用更严格的验证手段。

常见误区与注意事项

第一,不要只依赖全局限流。全局限流只能保护整体系统不被完全打垮,但攻击者集中打一个接口时,其他接口可能完全正常,全局限流根本不会触发,而目标接口已经被打挂。

第二,不要对静态资源做过度限流。静态资源应该走CDN缓存,回源量本身很小,如果对静态资源也做严格的动态限流,反而增加网关负担,还可能误伤正常用户。

第三,限流规则要考虑业务逻辑。比如登录接口,同一个用户连续输错密码5次后锁定,这本身就是一种业务层面的限流。如果再加上IP每分钟只能请求10次的限制,正常用户在一个共享办公环境里可能集体无法登录。业务规则和防护限流要协调,不能简单叠加。

第四,分布式限流要注意数据一致性。多台服务器之间共享限流计数时,Redis是常用方案,但要考虑Redis本身的性能和可用性。如果Redis挂了,限流策略是降级为本地限流还是直接放行,需要提前设计好,避免限流组件成为新的单点故障。

第五,测试和演练必不可少。限流规则上线前要在测试环境模拟真实流量和攻击流量,验证阈值是否合理、误伤率是否可接受。上线后要定期做压测和攻防演练,因为业务增长和架构调整都会影响原有限流策略的有效性。

动态接口限流不是一劳永逸的配置,而是一个需要持续观察、调整、对抗的过程。攻击者的手法在变,业务形态在变,限流策略也要跟着演进。把限流和风控、验证码、IP信誉、行为分析结合起来,才能形成完整的动态接口防护体系。