流量安全优化:基于 Nacos 和 BloomFilter 实现动态IP黑名单过滤

摘要:针对恶意请求问题,设计基于Nacos配置中心布隆过滤器动态IP黑名单方案。通过Web过滤器拦截请求,实时阻断黑名单IP访问,保障系统资源安全。

动态 IP 黑名单过滤

需求分析

一些恶意用户(؜可能是黑客、爬虫、DDoS ⁠攻击者)可能频繁请求服务器资‏源,导致资源占用过高。因此我‌们需要一定的手段实时阻止可疑‏或恶意的用户,减少攻击风险。

通过 IP؜ 封禁,可以有效拉⁠黑攻击者,防止资源‏被滥用,保障合法用‌户的正常访问。

对于我们的需求,不让拉进黑名单的 IP 访问任何接口。

方案设计

1 设计过程

其实前面讲到的 Sentinel 本身就支持请求来源的 黑白名单判断,但默认是对应用级别进行判断,需要改造来源的获取方式为获取请求客户端的 IP,可参考 这篇文章 自定义来源。

但其实引入 Se؜ntinel 是需要一定成本的,⁠本节主要分享更轻量的 动态 IP ‏黑白名单过滤的常用设计和实现方法‌。               ‏

想要自主实现动态 IP 黑名单,主要考虑以下几点:

  1. IP 黑名单存储在哪里?
  2. 如何便捷地动态修改 IP 黑名单?
  3. 黑白名单的判断逻辑应在哪里处理?
  4. 使用何种数据结构保存黑名单?如何快速匹配用户请求的 IP 是否在黑名单中?

下面分别设计:

1)IP 黑名单存储在哪里?

最简单的方式就是؜存储在内存中,但一般 IP 黑名⁠单是动态增加的、需要持久化保存。‏常见的持久化方式包括数据库、配置‌文件或分布式存储系统(如 Red‏is),可以根据需要选择。

2)如何便捷地动态修改 IP 黑名单?

为了方便动؜态修改 IP 黑名⁠单,通常会提供一个‏管理页面,供管理员‌进行增删改查操作。

许多企业会将配置统一放入 配置中心,通过配置中心的管理页面,开发人员可以便捷地动态修改黑名单规则。Java 项目中,常用的配置中心是Nacos

3)黑白名单的判断逻辑应在哪里处理?

黑白名单逻辑通常部署在高性能的 网关或 CDN上,能够更早地拦截非法请求,减轻后端压力。在小型项目中,也可以直接在应用程序的过滤器中处理。

4)使用何种结构保存黑名单?如何快速匹配?

为了高效判断每个用户请求的 IP 是否在黑名单中,首先建议将 IP 黑名单从持久化存储同步到本地缓存中,避免频繁查询远程数据源。对于黑名单数据较小的场景,可以使用简单的 Set 数据结构存储。而对于大规模黑名单,推荐使用 布隆过滤器或 DFA 来存储和过滤黑名单,可以节约内存空间、提高检测效率。

2 最终方案

总结一下最终方案:

1)使用 Nacos 配置中心存储和管理 IP 黑名单

2)后端服务利用 Web 过滤器判断每个用户请求的 IP

3)后端服务利用布隆过滤器过滤 IP 黑名单

3 扩展知识 - 布隆过滤器

Bloom؜ Filter 是⁠一种高效的、基于概‏率的数据结构,用于‌判断一个元素是否存在‏于集合中。

原理:利用؜多个哈希函数将元素⁠映射到固定的点位上‏(位数组中),因此‌面对海量数据它占据‏的空间也非常小。

例如某个 ؜key 通过 has⁠h-1 和 hash‏-2 两个哈希函数,‌定位到数组中的值都为‏ 1,则说明它存在。

如果布隆过滤器判断一个元素不存在集合中,那么这个元素一定不在集合中,如果判断元素存在集合中则不一定是真的,因为哈希可能会存在冲突。因此布隆过滤器 有误判的概率

而且它不好删除元素,只能新增,如果想要删除,只能重建。

显然,它的 主要特点包括:

  1. 时间复杂度低:查询操作非常快速,通常是常数时间复杂度 O(1)
  2. 空间效率高:相比于传统的数据结构(如哈希表),它 能用较少的空间存储大量的数据。
  3. 允许误判:Bloom Filter 允许假阳性,即有时候会错误地判断某个元素在集合中,而实际该元素并不在集合中。不过,它不允许假阴性,也就是说,如果 Bloom Filter 判断某个元素不存在,那么它一定是不存在的。比如对于我们的需求,Bloom Filter 可能错误地判断一个不在黑名单中的元素为在黑名单中,导致误封。

Bloom Filter 的误判率与以下因素有关:

  • ++位数组的大小:++位数组越大,误判率越低,但空间开销会增大。(值会更离散)
  • ++哈希函数个数**:**++哈希函数越多,误判率越低,计算成本增加。(多次Hash ,减少冲突概率)
  • ++元素数量:++存入的元素越多,误判率会增加。

通过 合理设计位数组的大小和哈希函数的个数,可以 控制 Bloom Filter 的误判率在一个可接受的范围内。例如,在很多实际场景中,可以将误判率控制在 1% 或更低。

  • 场景 1:存 1000 个元素,位数组大小为 10000 位,哈希函数数量为 7。误判率约 0.8%。
  • 场景 2:存 100000 个元素,位数组大小为 1,000,000 位,哈希函数不变。误判率约 1%。
  • 场景 3:存 1,000,000 个元素,位数组大小为 10,000,000 位,哈希函数不变。误判率约 1%。

如果误判的؜代价较高,但仍想使⁠用 Bloom F‏ilter,可以采‌取一些补救措施:

  • 双层验证:在 Bloom Filter 判断元素在黑名单中后,进一步查验实际的黑名单(例如,查数据库中的黑名单详细记录)。
  • 结合其他数据结构:可以使用 Bloom Filter 进行初步筛选,如果 Bloom Filter 判断为在黑名单中,再用哈希表等精确的数据结构进行最终确认。

但这两种方؜式都无法处理攻击 ⁠IP 的大量请求,‏个人也不建议采用。

因此,布隆过؜滤器适用于对准确性要求⁠不高的、大规模数据量匹‏配的场景,比如垃圾邮件‌过滤、爬虫 URL 去‏重、缓存穿透防护等。

配置中心

为什么需要配置中心?

在分布式系统中؜,应用的配置管理变得越来越⁠复杂,特别是当系统规模和组‏件数量增加时。传统的手动配‌置(写固定配置文件)往往难‏以应对这些复杂的需求。

而配置中心的出现就是为实现分布式系统中配置的集中化管理,还提供动态更新、配置分组、版本控制、灰度发布、安全管理,简化了多环境和多实例的配置运维,确保系统的灵活性和稳定性。

一句话,专业的技术做专业的事。

配置中心支持的功能

++1)集中化配؜置管理:++所有服务的配置可⁠以在一个地方集中管理,运‏维人员和开发人员可以通过‌统一的接口修改和获取配置‏,避免了在每个实例中重复配置。

++2)动态配؜置:++ 配置中心允许在⁠不重启应用的情况下‏动态更新配置,应用‌可以实时收到配置的‏修改,进行运行时的调整。

++3)多环境配؜置管理:++配置中心,可以⁠为不同的环境配置不同的‏配置集,按需加载相应的‌环境配置文件,避免了环‏境间配置的混淆和出错。

++4)配置的؜版本控制:++配置中心一⁠般都会提供版本管理功‏能,可以查看和回滚到‌之前的配置版本,这提‏高了系统的容错性和可恢复性。

++5)配置的安全管؜理:++配置中心一般会提供加密存储⁠和权限控制功能,可以对敏感信息‏(如数据库密码、API 密钥等‌)进行加密处理,并限制访问权限‏,确保敏感配置信息的安全性。

常见的配置中心

1)Spring ؜Cloud Config:Spri⁠ng Cloud 提供的配置中心解‏决方案,支持 Git 等版本管理系‌统存储配置,适合与 Spring ‏Cloud 系统集成使用。

2)Nacos؜:阿里巴巴开源的服务注册中⁠心和配置中心,支持动态配置‏、服务治理,适合微服务架构‌和 Dubbo、Sprin‏g Cloud 的深度集成。

3)Apol؜lo:由携程开源的配置⁠中心,支持多环境、多集‏群的配置管理,配置实时‌生效且具有权限控制,适‏合大规模分布式系统。

4)Cons؜ul:由 HashiC⁠orp 提供的服务注册‏与配置中心,具有强一致‌性和健康检查功能,适用‏于服务网格和容器化应用。

一般业务上,؜我们会选择使用 Naco⁠s 或 Apollo 来‏作为配置中心,因为这两个‌提供了比较丰富的控制台管‏理页面,便于我们修改维护配置。

本项目使用 Nacos 作为配置中心的实现。

Nacos 入门

什么是 Nacos?

Nacos 是 Dynamic Naming and Configuration Service 的首字母简称,一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。

它提供了一؜组简单易用的特性集⁠,帮助我们快速实现‏动态服务发现、服务‌配置、服务元数据及‏流量管理。

实际上,N؜acos 不仅支持⁠配置管理,它还支持‏服务发现(作为注册‌中心),以下是官网‏总结的 Nacos 地图:

我们当前的项目主要使用它的配置管理功能。

Nacos 配置管理的核心概念

1 Namespace(命名空间)

命名空间用于隔离不同的配置集؜。它允许在同一个 Nacos 集群中将不同的环境(如开发、测试、生⁠产)或者不同的业务线的配置进行隔离。(默认提供了一个 publ‏ic 命名空间) 使用场景:在多租户系统中,或者需要区分不同的‌环境时,可以使用命名空间。例如,开发环境的配置和生产环境的配置‏完全隔离,可以通过不同的命名空间来管理。

2 Group(组)

配置组是用于将多个相关的配置؜项进行分类管理的逻辑分组机制。每个配置项可以属于不同的⁠组,以便于配置管理。 使用场景:当一个应用有多个模块,‏且不同模块之间共享部分配置时,可以用组来对这些模块的配‌置进行分类和管理。例如,一个系统中的"支付服务"和"订‏单服务"可能需要用不同的组来存储各自的配置。5vPWD56ll5kkUnG0ja5sSNqkp2tHgsJHiSpPWWLD64g=

3 Data ID

Data I؜D 是一个唯一的配置标识⁠符,通常与具体的应用程序‏相关。通过 Data I‌D,Nacos 知道如何‏获取特定应用的某个具体配置。 使用场景:每个应用的配置都会有一个独特的 Data ID。例如,一个支付系统可能有一个配置文件叫 com.payment.pay-service.yaml,这就是它的 Data ID。

4 Config Listener(配置监听器)

配置监听器用于让客户端实؜时监听 Nacos 配置中心中的配置变化,可以⁠自动感知配置的更新并做出相应的处理。 使用场景‏:在需要动态调整配置的场景下使用,例如调整缓存‌大小、切换不同的服务端点等,应用可以通过监听器‏及时感知这些变化并应用新的配置。

推送和监听

推送方法:Nacos 控制台(推荐) 应用程序 SDK Open API

监听方法:使用 SDK 配置 Config Listener。

示例代码如下:

java 复制代码
String serverAddr = "{serverAddr}";
String dataId = "{dataId}";
String group = "{group}";
Properties properties = new Properties();
properties.put("serverAddr", serverAddr);
ConfigService configService = NacosFactory.createConfigService(properties);
String content = configService.getConfig(dataId, group, 5000);
System.out.println(content);
configService.addListener(dataId, group, new Listener() {
	@Override
	public void receiveConfigInfo(String configInfo) {
		System.out.println("recieve1:" + configInfo);
	}
	@Override
	public Executor getExecutor() {
		return null;
	}
});

// 测试让主线程不退出,因为订阅配置是守护线程,主线程退出守护线程就会退出。 正式代码中无需下面代码
while (true) {
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

或者直接通؜过注解读取 val⁠ue,能够实时获取‏到最新的配置值:

后端开发

1 下载并启动 Nacos Server

Windows 启动命令:(进入Bin目录)

bash 复制代码
startup.cmd -m standalone

启动成功,如图:

2 通过 Nacos 控制台添加配置

1)访问:http://127.0.0.1:8848/nacos ,默认用户名和密码都是 nacos

2)点击创建配置:

填写配置,推荐 yaml 格式:

bash 复制代码
blackIpList:
    - "1.1.1.1"
    - "2.2.2.2"
3 项目引入 Nacos 依赖

可以直接使用 Spring Boot Starter 快速引入 Nacos。

1)在项目 pom.xml 文件中,引入以下依赖配置:

XML 复制代码
<dependency>
    <groupId>com.alibaba.boot</groupId>
    <artifactId>nacos-config-spring-boot-starter</artifactId>
    <version>0.2.12</version>
</dependency>

注意:版本 0.2.x.RELEASE 对应的是 Spring Boot 2.x 版本,版本 0.1.x.RELEASE 对应的是 Spring Boot 1.x 版本。(经测试,本项目可使用 0.2.12 版本)

2)修改 application.yml 配置文件,添加 Nacos Server 地址等配置:

bash 复制代码
# 配置中心
nacos:
  config:
    server-addr: 127.0.0.1:8848  # nacos 地址
    bootstrap:
      enable: true  # 预加载
    data-id: mianshiya # 控制台填写的 Data ID
    group: DEFAULT_GROUP # 控制台填写的 group
    type: yaml  # 选择的文件格式
    auto-refresh: true # 开启自动刷新
4 创建黑名单过滤工具类

新建 bl؜ackfilter⁠ 包,黑名单过滤相‏关的代码都放到该包下,‌模块化。

可以用 Hutool 或 Guava 库自带的 bloomfilter,如果是分布式,还可以考虑 Redisson。

此处由于项؜目已经使用了 Hu⁠tool 工具库,‏就用其自带的 Bi‌tMapBloom‏Filter 即可。

示例代码如下:

java 复制代码
@Slf4j
public class BlackIpUtils {

    private static BitMapBloomFilter bloomFilter;

    // 判断 ip 是否在黑名单内
    public static boolean isBlackIp(String ip) {
        return bloomFilter.contains(ip);
    }

    // 重建 ip 黑名单
    public static void rebuildBlackIp(String configInfo) {
        if (StrUtil.isBlank(configInfo)) {
            configInfo = "{}";
        }
        // 解析 yaml 文件
        Yaml yaml = new Yaml();
        Map map = yaml.loadAs(configInfo, Map.class);
        // 获取 ip 黑名单
        List<String> blackIpList = (List<String>) map.get("blackIpList");
        // 加锁防止并发
        synchronized (BlackIpUtils.class) {
            if (CollectionUtil.isNotEmpty(blackIpList)) {
                // 注意构造参数的设置
                BitMapBloomFilter bitMapBloomFilter = new BitMapBloomFilter(958506);
                for (String ip : blackIpList) {
                    bitMapBloomFilter.add(ip);
                }
                bloomFilter = bitMapBloomFilter;
            } else {
                bloomFilter = new BitMapBloomFilter(100);
            }
        }
    }
}

注意,BitMapBloomFilter 接受的参数比较特殊,关于如何计算 BloomFilter 参数值,可在 GitHub 中可以自行测试:https://github.com/dromara/hutool/issues/3356

💡 注意,因为 ؜Nacos 配置文件的监听的粒度比⁠较粗,只能知晓配置有变更,无法知晓‏是新增、删除还是修改,因此不论是选‌择布隆过滤器还是 HashSet ‏最方便的处理逻辑就是重建。

5 创建 Nacos 配置监听类

可以直接通过 Nacos 控制台获取示例代码:

在 bla؜ckfilter ⁠包中新增监听器代码‏,追求性能的话可以‌自定义线程池:

java 复制代码
@Slf4j
@Component
public class NacosListener implements InitializingBean {

    @NacosInjected
    private ConfigService configService;

    @Value("${nacos.config.data-id}")
    private String dataId;

    @Value("${nacos.config.group}")
    private String group;

    @Override
    public void afterPropertiesSet() throws Exception {
        log.info("nacos 监听器启动");

        String config = configService.getConfigAndSignListener(dataId, group, 3000L, new Listener() {
            final ThreadFactory threadFactory = new ThreadFactory() {
                private final AtomicInteger poolNumber = new AtomicInteger(1);
                @Override
                public Thread newThread(@NotNull Runnable r) {
                    Thread thread = new Thread(r);
                    thread.setName("refresh-ThreadPool" + poolNumber.getAndIncrement());
                    return thread;
                }
            };
            final ExecutorService executorService = Executors.newFixedThreadPool(1, threadFactory);

            // 通过线程池异步处理黑名单变化的逻辑
            @Override
            public Executor getExecutor() {
                return executorService;
            }

            // 监听后续黑名单变化
            @Override
            public void receiveConfigInfo(String configInfo) {
                log.info("监听到配置信息变化:{}", configInfo);
                BlackIpUtils.rebuildBlackIp(configInfo);
            }
        });
        // 初始化黑名单
        BlackIpUtils.rebuildBlackIp(config);
    }
}
6 创建黑名单过滤器

黑名单应该对所有请求生؜效(不止是 Controller 的接口),⁠所以基于 WebFilter 实现而不是 A‏OP 切面。WebFilter 的优先级高于‌ @Aspect 切面,因为它在整个 Web‏ 请求生命周期中更早进行处理。

请求进入时的顺序:

  • WebFilter:首先,WebFilter 拦截 HTTP 请求,并可以根据逻辑决定是否继续执行请求。
  • Spring AOP 切面(@Aspect):如果请求经过过滤器并进入 Spring 管理的 Bean(例如 Controller 层),此时切面生效,对匹配的 Bean 方法进行拦截。
  • Controller 层:如果 @Aspect 没有阻止执行,最终请求到达 @Controller 或 @RestController 的方法。

代码如下:

java 复制代码
@WebFilter(urlPatterns = "/*", filterName = "blackIpFilter")
public class BlackIpFilter implements Filter {

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {

        String ipAddress = NetUtils.getIpAddress((HttpServletRequest) servletRequest);
        if (BlackIpUtils.isBlackIp(ipAddress)) {
            servletResponse.setContentType("text/json;charset=UTF-8");
            servletResponse.getWriter().write("{\"errorCode\":\"-1\",\"errorMsg\":\"黑名单IP,禁止访问\"}");
            return;
        }
        filterChain.doFilter(servletRequest, servletResponse);
    }

}

需要在启动类上加上 @ServletComponentScan,这样过滤器才会被扫描到。

java 复制代码
@SpringBootApplication(exclude = {RedisAutoConfiguration.class})
@MapperScan("com.yupi.mianshiya.mapper")
@EnableScheduling
@EnableAspectJAutoProxy(proxyTargetClass = true, exposeProxy = true)
@ServletComponentScan
public class MainApplication {

    public static void main(String[] args) {
        SpringApplication.run(MainApplication.class, args);
    }

}

大功告成!

相关推荐
非凡ghost6 小时前
PixPin截图工具(支持截长图截动图) 中文绿色版
前端·javascript·后端
武子康6 小时前
大数据-133 ClickHouse 概念与基础|为什么快?列式 + 向量化 + MergeTree 对比
大数据·后端·nosql
脚踏实地的大梦想家6 小时前
【Go】P11 掌握 Go 语言函数(二):进阶玩转高阶函数、闭包与 Defer/Panic/Recover
开发语言·后端·golang
小小爱大王7 小时前
AI 编码效率提升 10 倍的秘密:Prompt 工程 + 工具链集成实战
java·javascript·人工智能
用户68545375977697 小时前
🔥 服务熔断降级:微服务的"保险丝"大作战!
后端
Tech有道7 小时前
拼多多「面试官问我:LRU 和 LFU 你选谁?」我:看场景啊哥!😂
后端
用户68545375977697 小时前
🎬 开场:RPC框架的前世今生
后端
王中阳Go背后的男人7 小时前
Docker磁盘满了?这样清理高效又安全
后端·docker
用户68545375977697 小时前
🎛️ 分布式配置中心:让配置管理不再是噩梦!
后端