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