CC攻击的对抗早已不是简单的频率限制能解决的问题。攻击者利用庞大的代理池和肉鸡网络,每一次请求的源IP都可能不同,或者以极低频率混杂在正常流量中。在这种背景下,IP黑白名单依然是成本最低、效果最直接的防御手段之一。但问题在于,传统的静态IP列表根本无法应对这种动态变化的攻击源。你需要一套能够实时更新、快速查询且高并发下不拖垮业务系统的存储与更新机制。把IP黑白名单的动态更新与Redis存储结构设计好,是提升Web应用层防御能力的关键一步。

为什么选择Redis作为IP名单的存储核心

在高并发场景下,CC防护的IP判罚必须发生在请求进入业务逻辑之前,通常是网关层或中间件层。这意味着每次请求都需要进行一次IP查询。如果把这个查询放到关系型数据库里,磁盘I/O带来的延迟会直接拖垮整个系统。Redis基于内存,单线程处理命令,读写延迟在亚毫秒级别,天然适合这种高频查询场景。更重要的是,Redis提供了丰富的数据结构,可以针对不同的黑白名单策略选择最合适的存储方式,而不是简单地把IP存成字符串。

把IP名单放在Redis里,还能解决多节点部署下的数据一致性问题。如果你的Web服务部署了多个实例,攻击流量打到任何一台机器上,都需要基于同一份黑名单进行拦截。Redis作为共享存储,所有实例都从同一个地方读取判罚结果,避免了本地内存缓存不一致导致的防御漏洞。同时,Redis的过期机制可以让临时封禁的IP自动解封,不需要额外开发定时清理任务。

IP名单的数据结构选型与设计细节

很多人第一反应是用Redis的String类型,把IP作为Key,把“black”或“white”作为Value存进去。这种方式最简单,但在存储大量IP时内存占用很高,而且无法做范围查询或网段匹配。一个更高效的做法是使用Set集合。把黑名单IP存入一个Set,白名单IP存入另一个Set。查询时直接用SISMEMBER命令,时间复杂度O(1)。Set内部使用哈希表实现,单个IP的存储开销比String小很多。如果你的黑名单数量达到百万级别,Set依然能保持稳定的查询性能。

但Set无法解决网段封禁的问题。比如你需要封禁一个C段IP,比如192.168.1.0/24,Set只能逐个添加256个IP,这显然不现实。这时候可以引入Sorted Set,将IP转换为数值进行范围存储。把每个IP地址转成一个32位整数,用这个整数作为Score,Member可以存储原始IP或留空。查询时把目标IP也转成整数,通过ZRANGEBYSCORE找出落在黑名单网段内的记录。这种方式需要额外维护一个网段与整数范围的映射表,但能实现精确的网段匹配。

还有一种更灵活的设计,是使用Redis的Hash结构。以IP的前三段作为Key,比如“ip_black:192.168.1”,Hash的field用最后一段数字,value存储封禁原因或过期时间戳。这种结构在查询时先根据IP前三段定位到Hash,再用HEXISTS判断最后一段是否存在。它的优势在于内存利用率极高,而且可以方便地针对某个C段进行批量操作,比如一次性移除整个C段的封禁。如果你的攻击源经常集中在某些特定网段,这种分片存储的方式查询效率会非常高。

动态更新的触发机制与数据同步策略

黑白名单的动态更新来源通常有三个:一是基于流量分析引擎的自动判罚,二是安全运营人员的手动添加,三是第三方威胁情报的同步。流量分析引擎检测到某个IP在短时间内发起大量异常请求后,需要立即将这个IP加入黑名单。这个动作不能异步处理,必须是同步的,否则攻击窗口期内会有大量请求绕过防御。但同步写入Redis也存在风险,如果攻击者伪造大量不同IP触发封禁逻辑,写入操作可能压垮Redis。所以需要在写入前加一层本地布隆过滤器,先判断这个IP是否已经被处理过,避免重复写入。

手动添加的黑白名单通常来自安全人员的分析结果,这些数据需要持久化保存,不能因为Redis重启就丢失。建议在MySQL或PostgreSQL中保留一份全量名单,Redis作为热数据的查询层。当运营人员在管理后台添加一条记录时,同时写入数据库和Redis。系统启动时从数据库全量加载到Redis,运行期间通过消息队列或数据库的binlog监听实现增量同步。这种双写方案虽然增加了复杂度,但保证了数据的可靠性。

第三方威胁情报的同步一般是定时拉取,比如每小时从云端拉取一次最新的恶意IP列表。拉取到的数据量可能很大,一次写入几万条IP。如果直接循环写入Redis,会产生大量网络往返,耗时很长。正确的做法是使用Redis的管道或批量命令,将多个写入操作打包发送。更高效的方式是直接把情报数据格式化成Redis的协议文本,通过redis-cli的pipe模式导入,几万条数据可以在秒级完成加载。

过期策略与自动解封的精细化控制

不是所有被封禁的IP都应该永久关在黑名单里。大多数CC攻击源是临时被控制的肉鸡或动态分配的家用IP,封禁一段时间后攻击行为就会消失。如果一直不释放,可能误伤后续使用这个IP的正常用户。Redis的EXPIRE命令可以给Key设置生存时间,到期自动删除。但简单的Key过期无法满足复杂场景。你需要根据攻击的严重程度设置不同的封禁时长。轻度扫描可能封禁10分钟,明显的CC攻击封禁1小时,持续攻击的IP则封禁24小时甚至更长。

为了实现这种分级过期,可以在Value中存储封禁的截止时间戳,而不是简单依赖Redis的Key过期。查询时先判断IP是否在黑名单中,如果存在则取出时间戳与当前时间对比。如果已经过期,主动删除这个记录并放行请求。这样做的另一个好处是,当运营人员在后台查看黑名单列表时,可以直观看到每个IP的剩余封禁时间。缺点是需要应用层多一步时间判断,略微增加查询复杂度。折中方案是仍然使用Redis的Key过期,但设置不同时长的Key对应不同的业务规则,比如key名加后缀“_10m”、“_1h”来区分。

白名单通常不需要过期,但需要防止无限增长。有些业务场景会给合作伙伴或特定爬虫开放白名单,合作关系结束后需要及时移除。建议白名单也设置一个较长的过期时间,比如三个月,到期前由运营人员确认是否续期。这样既避免了僵尸白名单带来的安全隐患,也减少了人工维护成本。

高并发查询下的性能优化与降级方案

当CC攻击发生时,网关层的QPS可能瞬间飙升到几十万甚至更高。每一次请求都要查询Redis,对Redis本身的压力也不小。虽然Redis单机可以支撑10万以上的QPS,但如果你的业务本身也重度依赖Redis做缓存,攻击流量可能会挤占Redis的带宽和连接数。优化查询的第一步是减少网络开销。在网关层使用本地内存缓存,将最近查询过的IP和判罚结果缓存在本地,比如使用LRU算法缓存最近1万条记录。大部分攻击IP在短时间内会被反复请求,本地缓存命中率会非常高,大幅减少对Redis的请求量。

本地缓存需要解决一致性问题。当一个IP被新加入黑名单时,所有网关实例的本地缓存都需要感知到这个变化。可以在Redis中发布订阅一个频道,封禁动作完成后发布一条消息,所有网关实例订阅这个频道,收到消息后清除本地缓存中对应的条目。如果对实时性要求不高,也可以让本地缓存设置一个较短的过期时间,比如5秒,这样最多5秒后所有实例都能拉取到最新的黑名单数据。

还需要考虑Redis不可用时的降级策略。如果Redis因为网络抖动或自身故障无法连接,网关不能因为查不到黑名单就直接放行所有请求,这等于防御体系瞬间崩塌。降级方案是每个网关实例在本地磁盘上维护一份黑名单的快照文件,定期从Redis同步更新。当Redis连接失败时,切换到读取本地快照文件。虽然实时性会下降,但至少保证核心的防御能力不丢失。快照文件可以用SQLite存储,查询性能远高于直接读文本文件,而且支持SQL语句,方便做网段匹配。

IP地址转换与存储压缩的工程技巧

IPv4地址在存储时如果直接存字符串,比如“192.168.1.100”,占用长度在11到15字节不等。Redis的Key如果使用这种字符串,百万级的IP会占用大量内存。把IP转成32位整数存储,只需要4个字节。转换方法很简单,把IP按点分十进制拆成四段,每段左移相应的位数后相加。查询时把请求来源IP同样转成整数,用整数去匹配。Redis的Set和Hash都支持存储整数,内存占用会大幅下降。如果使用Hash分片存储,Key的长度也能缩短,比如“b:192.168”代表黑名单中192.168这个B段的IP,Hash的field只存最后16位的主机部分。

对于IPv6地址,128位的长度无法直接映射成一个整数,Redis也没有原生的128位整数支持。但实际业务中IPv6流量占比通常还不高,可以先用字符串存储,或者只存储IPv6地址的前64位网络前缀。大部分CC攻击源目前仍以IPv4为主,把IPv4的优化做到极致,就能覆盖绝大多数场景。

还有一种压缩思路是利用布隆过滤器。在Redis 4.0之后,Redis提供了布隆过滤器模块。它可以用极小的内存判断一个元素是否可能存在于集合中。把黑名单IP全部加入布隆过滤器,查询时先经过布隆过滤器。如果返回不存在,说明这个IP一定不在黑名单中,直接放行。如果返回可能存在,再去查准确的黑名单集合。布隆过滤器会有很小的误判率,但不会漏判。这种前置过滤可以拦截掉绝大多数正常流量对Redis的查询,极大降低负载。布隆过滤器本身占用的内存极小,存储1亿个IP只需要几百MB。

运营后台与日志审计的必要设计

IP黑白名单系统不能是一个黑盒。安全运营人员需要知道当前黑名单中有多少IP,哪些是自动添加的,哪些是手动添加的,封禁原因是什么,剩余封禁时间还有多久。这些信息如果只存在Redis里,查询起来很不方便。建议设计一张MySQL表存储每一条封禁记录的元数据,包括IP、封禁类型、来源、操作人、创建时间、过期时间等。Redis只负责热数据的快速查询,MySQL负责历史记录和审计追溯。运营后台展示列表时从MySQL查询,封禁和解封动作同时更新MySQL和Redis。

日志方面,每一次封禁和解封动作都应该记录详细日志,包括触发条件、命中规则、来源IP的请求样本等。这些日志在追溯攻击事件、优化防御规则时至关重要。日志可以输出到Elasticsearch中,方便后续做聚合分析。比如分析最近一周被自动封禁的IP主要集中在哪些网段,哪些规则触发的封禁最多,封禁后是否还有来自同一网段的其他IP继续攻击。这些数据能指导你不断优化黑白名单的生成策略,让防御越来越精准。

动态IP黑白名单与Redis存储的结合,本质上是在性能、实时性和准确性之间寻找平衡。没有一套方案能适用所有业务场景,你需要根据自己的请求量级、攻击特征和运维能力,选择合适的数据结构和更新策略。但核心原则是不变的:查询要快、更新要准、数据要稳、成本要低。把这四点做到位,你的CC防护体系就拥有了一个非常扎实的基础底座。