如何防止Java Web应用被CC攻击?Servlet过滤器限流实现
摘要:# Java Web防CC攻击,别光靠WAF了,Servlet过滤器自己动手更靠谱 先说个可能让你有点意外的大实话:**很多Java Web应用被CC攻击打趴,问题真不在“没上防护”,而在于防护方案和你的业务“拧巴”了。** 我见过不少团队,一提到防C…
Java Web防CC攻击,别光靠WAF了,Servlet过滤器自己动手更靠谱
先说个可能让你有点意外的大实话:很多Java Web应用被CC攻击打趴,问题真不在“没上防护”,而在于防护方案和你的业务“拧巴”了。
我见过不少团队,一提到防CC,第一反应就是“上高防IP”、“买WAF”。这当然没错,但就像你买了个超大号的防盗门,结果窗户没关严实——攻击者稍微换个思路,从你应用层逻辑的“窗户”摸进来,你花大价钱买的“门”就成了摆设。
尤其是那种针对登录口、验证码接口、商品查询API的慢速、高频CC攻击,WAF的通用规则有时候真反应不过来。等你调好规则,业务可能已经卡了半小时了。这时候,在应用代码层面,自己给自己加一道“内门”,往往能起到奇效。
而这道“内门”,一个非常经典、直接且可控的实现,就是 Servlet过滤器(Filter)限流。
Servlet过滤器:你的“家门口第一道安检”
别被“过滤器”这个名字唬住,你可以把它理解成你们公司大楼的前台安检。每一个HTTP请求,就像每一个访客,在进入你的核心办公区(Servlet、Controller)之前,都必须先经过它。
它的工作就是:
- 查证件(解析请求):看看这个访客(IP、Session、User-Agent)是谁。
- 查记录(访问计数):看看他今天来第几次了,频率是不是高得离谱。
- 做决定(拦截或放行):如果他一分钟内想刷一百次门禁(访问某个关键接口),对不起,请您先在旁边休息会儿(返回429 Too Many Requests或者跳验证码)。
说白了,这就是把防护的粒度,从网络层、机房层,细化到了每一个具体的URL和业务动作上。 攻击者的流量再大,到了你这儿,也得按你定的“家规”一个一个排队接受检查。
自己动手,实现一个“不绕弯子”的限流过滤器
理论听着都挺好,咱们来点实在的。下面这个实现思路,你拿过去改改就能用。我尽量避开那些教科书式的代码堆砌,说点实际开发中你会遇到的坎儿。
核心逻辑其实就三步:
- 识别你是谁: 确定以什么作为限流的“键”。最常用的是IP,但对于有登录态的应用,结合用户ID会更精准,防止攻击者换代理IP来绕。
- 计数与判断: 用一个内存缓存(比如Google Guava的
CacheLoader或 Caffeine),记录这个“键”在指定时间窗口内的访问次数。超了?那就触发规则。 - 处置与响应: 别只是粗暴地返回404。对于疑似攻击,可以返回429状态码;对于普通用户误操作,可以跳一个简单的验证码页面,做一层人机验证。
来,看一段我简化后的核心逻辑伪代码,咱们边看边聊:
// 这不是完整可编译代码,是帮你理解逻辑的“骨架”
public class RateLimitFilter implements Filter {
// 用个缓存存计数,设置自动过期,省得自己清理
private LoadingCache<String, AtomicInteger> requestCounts;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
HttpServletRequest httpReq = (HttpServletRequest) request;
String key = buildKey(httpReq); // 组合IP+请求URI作为键
AtomicInteger count = requestCounts.get(key);
if (count.incrementAndGet() > THRESHOLD) { // 超过阈值
// **关键来了:这里别一刀切**
if (isSuspiciousRequest(httpReq)) { // 加个判断:请求特征是否可疑?
// 可疑攻击,直接限制
((HttpServletResponse)response).setStatus(429);
response.getWriter().write("请求过于频繁,请稍后再试。");
return;
} else {
// 像正常用户,给个“改过自新”的机会,比如跳验证码
redirectToCaptcha(httpReq, (HttpServletResponse)response);
return;
}
}
// 没超阈值,放心去处理业务吧
chain.doFilter(request, response);
}
private String buildKey(HttpServletRequest request) {
// 举例:用"IP:URI"做键,你可以加上用户ID、SessionID等
return request.getRemoteAddr() + ":" + request.getRequestURI();
}
private boolean isSuspiciousRequest(HttpServletRequest request) {
// 这里就是体现你业务经验的地方了:
// - User-Agent是不是空或者特别奇怪?
// - 请求头是否极其标准(像机器生成的)?
// - 对特定接口(如/login)的POST请求,但Referer却是站外?
// 这些点都可以作为辅助判断,增加“人味儿”,减少误伤。
return false; // 默认实现,你需要自己填充
}
}
几个容易“踩坑”的实战细节(血泪经验)
-
别只限IP,小心“误伤一片”:公司、学校、小区往往共用同一个出口IP。如果你对
/product/list这样的页面做IP限流,一个办公室的人可能只有第一个能刷出来。解决方案:对关键写操作(登录、提交订单)用IP+行为限流;对普通读操作放宽,或者结合轻量级验证码。 -
“计数器”放哪儿?单机与集群的抉择:上面例子用的本地缓存,意味着每台服务器各自为战。如果用户请求通过负载均衡打到不同机器,限流就失效了。对于集群部署,你需要把计数器放到一个集中式存储里,比如 Redis。但这又会引入网络开销和Redis单点风险。我的建议是:对中小型应用,先用本地限流,它能扛住大部分“傻”CC攻击。真到了需要集群限流的规模,你大概率也有专门的架构团队来搞了。
-
阈值怎么定?拍脑袋可不行:
THRESHOLD这个值,不是百度来的。最好的办法是,先给你的核心接口加上埋点,看看正常业务高峰期的QPS(每秒查询率)是多少。 比如,登录接口平时峰值是50次/分钟,那你把阈值设为80或100,就比较合理了。设得太低,用户抱怨;设得太高,形同虚设。 -
别忘了“白名单”:千万别把自己、把公司同事、把监控系统、把搜索引擎爬虫给限了!维护一个IP或账号的白名单列表,在过滤器最前面判断一下,直接放行。
说到底,Servlet过滤器限流只是“组合拳”的一招
我必须得说,没有任何单一技术是银弹。Servlet过滤器限流,是你防御体系里非常出色、成本极低的一道应用层防线。它特别适合应对那些有明确特征(比如针对特定URL、参数固定)的CC攻击。
但它的局限也很明显:对付海量分布式IP的低速攻击、或者消耗服务器资源的复杂攻击(比如慢速HTTP),就有点力不从心了。这时候,还是需要和前端的WAF、网络层的高防IP/CDN配合起来,形成纵深防御。
所以,正确的姿势是:
- 网络层/边界: 用高防IP、高防CDN扛住大流量冲击,做初步清洗。
- 应用层入口: 用WAF防御通用Web漏洞(SQL注入、XSS等)。
- 应用逻辑内部: 用我们今天聊的Servlet过滤器这类自定义代码,保护核心业务接口,实现精细化管控。
这就好比你家小区:物业有大门保安(高防IP),楼栋有门禁系统(WAF),而你自己家里,还装了个智能摄像头和报警器(自定义过滤器)——三层防护,心里才踏实。
最后,如果你正准备在项目里加这个功能,我建议你先从一两个最核心、最容易被攻击的接口开始,比如用户登录和短信发送接口。把这里面的逻辑跑通、参数调优,观察一段时间日志,看看有没有误拦。效果立竿见影之后,再逐步推广到其他敏感接口。
防护这件事,最怕的就是想着一口吃成个胖子,方案设计得巨复杂,最后哪一层都没配好。从最痛的点入手,用一个简单可控的技术先解决80%的问题,剩下的,咱们再慢慢迭代。
行了,思路和坑都给你摆这儿了,代码框架也有了,接下来,就看你自己的了。

