Nacos + springboot + 布隆过滤器实现 动态添加 IP 黑名单

Nacos + springboot + 布隆过滤器实现 动态添加 IP 黑名单

  • 下载 Nacos

    https://nacos.io/download/release-history/ 按照 nacos 与 jdk 下载适合的版本

  • 启动 Nacos

    • 解压文件进入 bin 目录

    • 在 bin 目录下输入 cmd 进入终端 (standalone 代表着单机模式运行,非集群模式)

      windows 启动命令

      startup.cmd -m standalone

      linux 启动命令

      sh startup.sh -m standalone

  • 使用 Nacos

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

    • 为应用创建一个配置

      配置格式推荐 选 Yaml 更容易理解,读取较方便

      配置内容为: (代表 黑名单列表中有两个IP 分别为 192.168.1.143 和 192.168.1.144)

      写完配置点击发布就 OK

      blackIpList:
          - "192.168.1.143"
          - "192.168.1.144"
      
      

      至此,Nacos 应用就可以使用了,下面继续介绍如果在 springboot 项目中实现 动态添加 IP 黑名单。

  • 项目中使用 Nacos

    • 引入依赖

      <dependency>
          <groupId>com.alibaba.boot</groupId>
          <artifactId>nacos-config-spring-boot-starter</artifactId>
          <version>0.2.12</version>
      </dependency>
      
      
    • 添加配置

      # 配置中心
      nacos:
        config:
          server-addr: 127.0.0.1:8848  # nacos 地址
          bootstrap:
            enable: true  # 预加载
          data-id: shuatiba # 控制台填写的 Data ID
          group: DEFAULT_GROUP # 控制台填写的 group
          type: yaml  # 选择的文件格式
          auto-refresh: true # 开启自动刷新
      
    • 添加 nacos 配置的 监听器

      可以参照 naocs 平台给出的示例代码,也可以自行编写

      在这 我写了一个 NacosListener 希望在springboot 项目启动时就监听 Naocs 所以需要实现InitializingBean 这个类。

      ConfigService 为 Naocs 的配置类

      group、dataId 都是从 项目的配置文件中读取

      代码的关键是 configService.getConfigAndSignListener()这个方法的实现。

      @Component
      @Slf4j
      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 监听器启动");
      
      
              configService.getConfigAndSignListener(dataId, group, 3000L, new Listener() {
      
      
                  // 线程池,返回 null 也会有默认的线程池
                  @Override
                  public Executor getExecutor() {
                      return null;
                  }
      
                  // 监听到 naocs 的配置有改动需要做的操作在这写
                  // (Nacos 配置文件的监听的粒度比较粗,只能知晓配置有变更,
                  // 无法知晓是新增、删除还是修改)
                  // 这里 String s 就代表着 nacos 配置文件内容
                  @Override
                  public void receiveConfigInfo(String s) {
                      log.info("Nacos 监听器 监听到配置文件有改动");
                  }
              });
          }
      }
      
    • 创建黑名单过滤工具类

      使用到 Hutool 的 布隆过滤器存储 ip 黑名单信息 和 判断是否属于 IP黑名单

      @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);
                  }
              }
          }
      }
      
    • 创建 IP 黑名单过滤器

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

      请求进入时的顺序:

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

      WebFilter 是Servlet 层面的组件。属于是请求的遇到的第一层拦截器,既然是黑名单,不提供任何服务,应该就在第一层就过滤掉,没必要后续判断,节省资源

      @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);
          }
      
      }
      

      ​ 也许你会觉得这样的写法很繁琐,很不习惯,不太像一般的过滤器有通过,不通过的情况处理,有先进入这层再进入这层过滤的处理,返回结果还学要自己拼接成json,自己设置返回结果类型…… 但其实 这就是最原生的 javaWeb Servlet 的样子,Spring真的是帮我们简化了我们很多看不到的东西,使用起来更方便。

      ​ 也正是因为这样,springboot 项目默认是不支持 Servlet 组件的,需要我们在启动类上加个注解让项目支持

      @ServletComponentScan
      
    • 改写 Naocs 配置文件 监听器

      @Component
      @Slf4j
      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() {
                  // 线程池,返回 null 也会有默认的线程池
                  @Override
                  public Executor getExecutor() {
                      return null;
                  }
      
                  // 监听到 naocs 的配置有改动需要做的操作在这写
                  // (Nacos 配置文件的监听的粒度比较粗,只能知晓配置有变更,
                  // 无法知晓是新增、删除还是修改)
                  // 这里 String s 就代表着 nacos 配置文件内容
                  @Override
                  public void receiveConfigInfo(String s) {
                      log.info("Nacos 监听器 监听到配置文件有改动");
                      BlackIpUtils.rebuildBlackIp(s);
                      log.info("Ip 黑名单相关配置已重新读取");
                  }
              });
      
              // 初始化黑名单
              BlackIpUtils.rebuildBlackIp(config);
          }
      }
      
    • 测试

      使用debug 模式把断点打在 BlackIpFilter 的 doFilter 中启动项目。

      随意发送任何yu请求 BlackIpUtils.isBlackIp()会判断发送请求IP 是否在黑名单列表中。

      第一次可以使用本机IP 发送请求,成功正常响应。

      这时,不需要重启项目,直接到 Naocs 的配置文件中的 BlackIpList 属性添加自己的本机IP并发布。

      第二次再用本机IP 发送请求,发现返回 黑名单 禁止访问

      完成 动态添加黑名单功能。

    • NetUtis 工具类

      import java.net.InetAddress;
      import javax.servlet.http.HttpServletRequest;
      
      /**
       * 网络工具类
       *
      
       */
      public class NetUtils {
      
          /**
           * 获取客户端 IP 地址
           *
           * @param request
           * @return
           */
          public static String getIpAddress(HttpServletRequest request) {
              String ip = request.getHeader("x-forwarded-for");
              if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                  ip = request.getHeader("Proxy-Client-IP");
              }
              if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                  ip = request.getHeader("WL-Proxy-Client-IP");
              }
              if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                  ip = request.getRemoteAddr();
                  if (ip.equals("127.0.0.1")) {
                      // 根据网卡取本机配置的 IP
                      InetAddress inet = null;
                      try {
                          inet = InetAddress.getLocalHost();
                      } catch (Exception e) {
                          e.printStackTrace();
                      }
                      if (inet != null) {
                          ip = inet.getHostAddress();
                      }
                  }
              }
              // 多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
              if (ip != null && ip.length() > 15) {
                  if (ip.indexOf(",") > 0) {
                      ip = ip.substring(0, ip.indexOf(","));
                  }
              }
              if (ip == null) {
                  return "127.0.0.1";
              }
              return ip;
          }
      
      }
      
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值