惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

GbyAI
GbyAI
爱范儿
爱范儿
Y
Y Combinator Blog
T
Tor Project blog
V
Visual Studio Blog
U
Unit 42
B
Blog RSS Feed
博客园 - 叶小钗
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
阮一峰的网络日志
阮一峰的网络日志
T
Tailwind CSS Blog
G
Google Developers Blog
I
InfoQ
Stack Overflow Blog
Stack Overflow Blog
IT之家
IT之家
Microsoft Azure Blog
Microsoft Azure Blog
T
The Blog of Author Tim Ferriss
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
The Cloudflare Blog
Google DeepMind News
Google DeepMind News
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
H
Hackread – Cybersecurity News, Data Breaches, AI and More
F
Fortinet All Blogs
人人都是产品经理
人人都是产品经理
Apple Machine Learning Research
Apple Machine Learning Research
The GitHub Blog
The GitHub Blog
Recorded Future
Recorded Future
博客园_首页
罗磊的独立博客
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
量子位
P
Proofpoint News Feed
Jina AI
Jina AI
博客园 - 【当耐特】
S
Security @ Cisco Blogs
I
Intezer
MyScale Blog
MyScale Blog
Simon Willison's Weblog
Simon Willison's Weblog
P
Privacy & Cybersecurity Law Blog
腾讯CDC
T
Tenable Blog
A
Arctic Wolf
T
Threat Research - Cisco Blogs
S
Securelist
Know Your Adversary
Know Your Adversary
Spread Privacy
Spread Privacy
C
Check Point Blog
NISL@THU
NISL@THU
Microsoft Security Blog
Microsoft Security Blog
V
Vulnerabilities – Threatpost

博客园 - 华安

Unity 中区别,public 和 [SerializeField] Unity中 onCollisionEnter2D与OnTriggerEnter2D 区别 asp.netCore中给动态请求路径加客户端缓存 netCore中获取客户端真实的IP引起的思考 Unity 相机:正交 (Orthographic) vs 透视 (Perspective) unity中的button中的onclick控制面板中四个参数的意思分别是什么 微信小程序开发中触发 onshow的几种特殊情况 SQL Server备份心得 MYSQL 备份数据库 微信与支付支付功能开发 微信小程序中页面配置下拉刷新 unity中 相机没有视锥效果线框了,如何打开 JSONPath表达式 C# 中的操作JSON类 JObject cookie中的 HttpOnly 、Secure、SameSite 解释 登录 用 HMAC-SHA256 实现 TwoFA(二重验证)的坑 利用Spring Boot的 filter 结合ConcurrentHashMap 实现“同一IP每分钟最多允许300个404请求,超出后禁用30分钟访问” pixi-filters中的BackdropBlurFilter使用注意事项 PixiJS中的 SplitBitmapText.chars中的每个 BitmapText的x值分析 legend隐藏不想显示的图例 spring-boot HttpServletResponse response.sendRedirect是会跳转到 http而不是https springboot获取post请求参数 Javascript如何判断是触摸屏还是PC端 JavaScript获取鼠标点一个元素,获取鼠标点击的元素内的位置 spring-boot中配置Mongodbd的问题小结 Rest Template中添加 PATCH请求。 CSS实现修改CheckBox样式 SpringBoot+Thyemleaf报错:Error resolving template Template might not exist or might not be accessible div display flex 如何出现横向滚动条
Spring-boot 中基于 IP 的限流和自动封禁 Filter
华安 · 2026-01-28 · via 博客园 - 华安
package com.idmt.difyweb.filter;

import com.idmt.difyweb.common.WebUtil;
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.*;
import org.springframework.core.annotation.Order;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.net.http.HttpResponse;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;

@FunctionalInterface
interface MyLogger {
    void apply(String message);
}

@Component
public class RateLimitFilter implements Filter {
    private static final Logger logger = LoggerFactory.getLogger(RateLimitFilter.class);
    MyLogger myLogger=logger::info; //System.out::println; //
    // 配置参数
    //统计一分钟的限流 1分钟60秒 1秒1000毫秒
    private static  final int MAX_Restricted_Milliseconds = 10000; //10 * 1000; //10秒
    private static final int MAX_REQUESTS_PER_MINUTE = 10; //150的请求,指定时间内
    private static final int MAX_404_COUNT = 10; //5次返回 404,指定时间内
    private static final long BLOCK_TIME_MILLIS = 30 * 60 * 1000; // 限制 30分钟
    private static final long CLEANUP_INTERVAL = 2 * 60 * 1000; // 5分钟清理一次
    private static class IpStats {
        private final AtomicInteger requestCount = new AtomicInteger(0);//0;
        private final AtomicInteger error404Count =  new AtomicInteger(0); //0;
        private final AtomicLong lastResetTime = new AtomicLong(System.currentTimeMillis());

        public boolean isExpired(long currentTime) {
            return currentTime - lastResetTime.get() > MAX_Restricted_Milliseconds;
        }
        public void reset(long currentTime) {
            requestCount.set(0);
            error404Count.set(0);
            lastResetTime.set(currentTime);
        }
    }

    // 存储IP的请求统计信息
    private final ConcurrentHashMap<String, IpStats> ipStatsMap = new ConcurrentHashMap<>();
    // 存储被封禁的IP及其解封时间
    private final ConcurrentHashMap<String, Long> blackListMap = new ConcurrentHashMap<>();

    private ScheduledExecutorService cleanupExecutor;

    private void printRresult(ConcurrentHashMap<String, Long> hashMap){
        for (Map.Entry<?, ?> entry : hashMap.entrySet()) {
            myLogger.apply("Key: " + entry.getKey() + ", Value: " + entry.getValue());
        }
    }
    private void printRresultIPStats(ConcurrentHashMap<String, IpStats> hashMap){
        for (Map.Entry<?, ?> entry : hashMap.entrySet()) {
            IpStats ipStats=(IpStats)entry.getValue();
            myLogger.apply("Key: " + entry.getKey() +
                    ", requestCount: " + ipStats.requestCount.get()+
                    ", error404Count: " + ipStats.error404Count.get()+
                    ", lastResetTime:"+ipStats.lastResetTime.get()
                    );
        }
    }
    private void printALl(){
        myLogger.apply("---封闭的IP----");
        printRresult(blackListMap);
        myLogger.apply("---IP的404次数----");
        printRresultIPStats(ipStatsMap);
    }
    //封闭IP
    private void blockIP(String ip,long currentTime){
        myLogger.apply("开始封闭IP");
        blackListMap.put(ip, currentTime + BLOCK_TIME_MILLIS);
        // 可选: 清理统计数据
        ipStatsMap.remove(ip);
    }
    /**
     * 清理过期数据,防止内存泄漏
     */
    private void cleanup() {
        myLogger.apply("execute cleanup");
        long currentTime = System.currentTimeMillis();
        // 清理过期IP统计
        ipStatsMap.entrySet().removeIf(entry ->
                entry.getValue().isExpired(currentTime)
        );
        // 清理已解封的IP
        blackListMap.entrySet().removeIf(entry ->
                entry.getValue() <= currentTime
        );
        printALl();
    }
    @Override
    public void init(FilterConfig filterConfig) {

        // 定期清理过期IP和黑名单
        cleanupExecutor = Executors.newSingleThreadScheduledExecutor(r -> {
            Thread t = new Thread(r, "RateLimit-Cleanup");
            t.setDaemon(true);
            return t;
        });
        //cleanupExecutor.scheduleAtFixedRate(this::cleanup, CLEANUP_INTERVAL, CLEANUP_INTERVAL, TimeUnit.MILLISECONDS);
        cleanupExecutor.scheduleWithFixedDelay(this::cleanup, CLEANUP_INTERVAL, CLEANUP_INTERVAL, TimeUnit.MILLISECONDS);
    }
    @Override
    public void destroy() {
        myLogger.apply("--开始清理--");
        if (cleanupExecutor != null) {
            cleanupExecutor.shutdown();
        }
    }
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
       /* myLogger.apply("--开始过滤--");*/
        printALl();
        HttpServletRequest request1=(HttpServletRequest)request;
        String ip = WebUtil.getClientIp(request1); //request.getRemoteAddr();
        long currentTime = System.currentTimeMillis();

        // 检查是否被封禁
        Long blockUntil = blackListMap.get(ip);
        if (blockUntil != null && blockUntil > currentTime) {
            HttpServletResponse res=(HttpServletResponse) response;
            res.setStatus(429); // 太多请求
            response.setContentType("text/plain;charset=UTF-8");
            response.getWriter().write("Access restricted, please try again later。");
            return;
        } else if (blockUntil != null && blockUntil <= currentTime) {
            // 解封
            blackListMap.remove(ip);
        }
        // 统计请求信息
        IpStats stats = ipStatsMap.computeIfAbsent(ip, k -> new IpStats());

        // 存在跨分钟的情况,重置统计
        if (stats.isExpired(currentTime)) {
            stats.reset(currentTime);
        }
        // 请求计数
        stats.requestCount.incrementAndGet();
        ipStatsMap.put(ip, stats);

        // 3. 检查限流(在 请求处理前 检查)
        if (stats.requestCount.get() > MAX_REQUESTS_PER_MINUTE) {
            blockIP(ip,currentTime);
            HttpServletResponse res=(HttpServletResponse) response;
            res.setStatus(429);
            res.setContentType("text/plain;charset=UTF-8");
            res.getWriter().write("Requests are too frequent, please try again later");
            return;
        }

        // 处理请求
        // 使用自定义Response包装器捕获状态码
        StatusCaptureResponseWrapper wrappedResponse = new StatusCaptureResponseWrapper((HttpServletResponse) response);
        try {
            chain.doFilter(request, wrappedResponse);
        }
        finally {
            // 判断响应状态码
            if (wrappedResponse.getStatus() == 404) {
                stats.error404Count.incrementAndGet();
            }

            // 超过404阈值,加入黑名单
            if (stats.error404Count.get() >= MAX_404_COUNT) {
                blockIP(ip,currentTime);
            }
        }
    }

    /**
     * 自定义ResponseWrapper,用于捕获状态码
     */
    private static class StatusCaptureResponseWrapper extends HttpServletResponseWrapper {
        private int httpStatus = 200;

        public StatusCaptureResponseWrapper(HttpServletResponse response) {
            super(response);
        }

        @Override
        public void setStatus(int sc) {
            super.setStatus(sc);
            this.httpStatus = sc;
        }

        @Override
        public void sendError(int sc) throws IOException {
            super.sendError(sc);
            this.httpStatus = sc;
        }

        @Override
        public void sendError(int sc, String msg) throws IOException {
            super.sendError(sc, msg);
            this.httpStatus = sc;
        }

        public int getStatus() {
            return this.httpStatus;
        }
    }
}