Skip to content

Commit 4aebb57

Browse files
committed
refactor(security): 重构防火墙实现以增强CC攻击防护
- 移除匿名访问检查中间件中的简单IP计数和封禁逻辑 - 新增专用防火墙工具类 Firewall 实现CC攻击防护 - 集成 ipset 命令进行IP封禁操作 - 实现基于时间窗口的请求计数机制 - 添加虚拟线程异步执行封禁操作 - 设置每分钟400次请求的阈值限制
1 parent cbb1daf commit 4aebb57

File tree

3 files changed

+122
-6
lines changed

3 files changed

+122
-6
lines changed

src/main/java/org/b3log/symphony/processor/BeforeRequestHandler.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import org.b3log.symphony.processor.middleware.AnonymousViewCheckMidware;
4242
import org.b3log.symphony.repository.OptionRepository;
4343
import org.b3log.symphony.service.UserQueryService;
44+
import org.b3log.symphony.util.Firewall;
4445
import org.b3log.symphony.util.Sessions;
4546
import org.b3log.symphony.util.Symphonys;
4647
import org.json.JSONObject;
@@ -83,6 +84,7 @@ public void handle(final RequestContext context) {
8384
}
8485

8586
final String ip = Requests.getRemoteAddr(context.getRequest());
87+
Firewall.recordAndMaybeBan(ip);
8688
// 黑名单判断
8789
if (AnonymousViewCheckMidware.ipBlacklistCache.getIfPresent(ip) != null && !context.requestURI().equals("/test") && !context.requestURI().equals("/validateCaptcha")) {
8890
// 已经在黑名单,强制跳转到验证码页面

src/main/java/org/b3log/symphony/processor/middleware/AnonymousViewCheckMidware.java

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -173,12 +173,6 @@ public void handle(final RequestContext context) {
173173
if (count == null) count = 0;
174174
count++;
175175
ipVisitCountCache.put(ip, count);
176-
System.out.println(ip + " 访问计数:" + count + " " + context.requestURI());
177-
178-
if (count >= 20) {
179-
String result = Execs.exec(new String[]{"sh", "-c", "ipset add fishpi " + ip}, 1000 * 3);
180-
System.out.println(ip + " 已封禁");
181-
}
182176

183177
// 判断是否需要进入验证码流程
184178
boolean needCaptcha = false;
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/*
2+
* Rhythm - A modern community (forum/BBS/SNS/blog) platform written in Java.
3+
* Modified version from Symphony, Thanks Symphony :)
4+
* Copyright (C) 2012-present, b3log.org
5+
*
6+
* This program is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU Affero General Public License as published by
8+
* the Free Software Foundation, either version 3 of the License, or
9+
* (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
* GNU Affero General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Affero General Public License
17+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
18+
*/
19+
package org.b3log.symphony.util;
20+
21+
import org.apache.commons.lang.StringUtils;
22+
import org.apache.logging.log4j.Level;
23+
import org.apache.logging.log4j.LogManager;
24+
import org.apache.logging.log4j.Logger;
25+
import org.b3log.latke.util.Execs;
26+
27+
import java.util.Map;
28+
import java.util.Set;
29+
import java.util.concurrent.ConcurrentHashMap;
30+
import java.util.concurrent.TimeUnit;
31+
32+
/**
33+
* Lightweight CC firewall: if an IP exceeds {@link #THRESHOLD} requests within a minute, ban it via ipset.
34+
*
35+
* Call {@link #recordAndMaybeBan(String)} on each request to enforce the threshold.
36+
*/
37+
public final class Firewall {
38+
39+
private static final Logger LOGGER = LogManager.getLogger(Firewall.class);
40+
41+
/**
42+
* Requests per minute threshold.
43+
*/
44+
private static final int THRESHOLD = 400;
45+
46+
/**
47+
* One-minute window length in milliseconds.
48+
*/
49+
private static final long WINDOW_MILLIS = TimeUnit.MINUTES.toMillis(1);
50+
51+
/**
52+
* Per-IP counters keyed by current minute bucket.
53+
*/
54+
private static final Map<String, Counter> COUNTERS = new ConcurrentHashMap<>();
55+
56+
/**
57+
* Already banned IPs to avoid duplicate shell calls.
58+
*/
59+
private static final Set<String> BANNED = ConcurrentHashMap.newKeySet();
60+
61+
private Firewall() {
62+
}
63+
64+
/**
65+
* Record one request from the given IP, and ban it if it crosses the threshold.
66+
*
67+
* @param ip client IP
68+
* @return {@code true} if the request is allowed, {@code false} if already banned or ban was triggered
69+
*/
70+
public static boolean recordAndMaybeBan(final String ip) {
71+
if (StringUtils.isBlank(ip)) {
72+
return true;
73+
}
74+
75+
final long nowBucket = System.currentTimeMillis() / WINDOW_MILLIS;
76+
final Counter counter = COUNTERS.compute(ip, (key, existing) -> {
77+
if (existing == null || existing.bucket != nowBucket) {
78+
return new Counter(nowBucket, 1);
79+
}
80+
existing.count++;
81+
return existing;
82+
});
83+
84+
// Small, opportunistic cleanup of stale buckets to keep the map lean.
85+
if ((COUNTERS.size() & 0xFF) == 0) {
86+
cleanupOldBuckets(nowBucket);
87+
}
88+
89+
if (counter.count > THRESHOLD && BANNED.add(ip)) {
90+
// Run ban asynchronously on a virtual thread to keep request path light.
91+
Thread.startVirtualThread(() -> {
92+
try {
93+
final String result = Execs.exec(new String[]{"sh", "-c", "ipset add fishpi " + ip}, 1000 * 3);
94+
LOGGER.log(Level.WARN, "CC firewall banned [{}], result: {}", ip, result);
95+
} catch (final Exception e) {
96+
LOGGER.log(Level.ERROR, "CC firewall ban failed for [" + ip + "]", e);
97+
}
98+
});
99+
return false;
100+
}
101+
102+
System.out.println("CC firewall allowed " + ip + " [" + counter.count + "]");
103+
104+
return !BANNED.contains(ip);
105+
}
106+
107+
private static void cleanupOldBuckets(final long currentBucket) {
108+
COUNTERS.entrySet().removeIf(entry -> entry.getValue().bucket != currentBucket);
109+
}
110+
111+
private static final class Counter {
112+
private final long bucket;
113+
private int count;
114+
115+
private Counter(final long bucket, final int count) {
116+
this.bucket = bucket;
117+
this.count = count;
118+
}
119+
}
120+
}

0 commit comments

Comments
 (0)