Skip to content

Commit bb03e65

Browse files
committed
feat(article): 添加文章访问限流功能
- 引入 ConcurrentHashMap 实现文章访客限流 - 实现单 IP 每秒仅允许一次访问的限流机制 - 访客和登录用户共用限流规则 - 防止内存表过大,自动清理 60 秒前的记录 - 在文章详情和渲染接口中集成限流检查 - 超出限流时返回 503 服务不可用错误
1 parent 043cdee commit bb03e65

File tree

1 file changed

+40
-0
lines changed

1 file changed

+40
-0
lines changed

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

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
import java.text.SimpleDateFormat;
6666
import java.util.List;
6767
import java.util.*;
68+
import java.util.concurrent.ConcurrentHashMap;
6869

6970
/**
7071
* Article processor.
@@ -99,6 +100,11 @@ public class ArticleProcessor {
99100
*/
100101
private static final Logger LOGGER = LogManager.getLogger(ArticleProcessor.class);
101102

103+
/**
104+
* 文章访客限流:单 IP 每秒仅允许一次。
105+
*/
106+
private static final ConcurrentHashMap<String, Long> ARTICLE_RATE_LIMIT = new ConcurrentHashMap<>();
107+
102108
/**
103109
* Revision query service.
104110
*/
@@ -422,6 +428,30 @@ public void showArticleMdApi(final RequestContext context) {
422428
context.getResponse().sendBytes(md.getBytes());
423429
}
424430

431+
/**
432+
* 简单的每 IP 1 秒一次限流,访客和登录用户共用。
433+
*
434+
* @param request the specified request
435+
* @return true if limited
436+
*/
437+
private boolean hitArticleRateLimit(final Request request) {
438+
final String ip = Requests.getRemoteAddr(request);
439+
final long now = System.currentTimeMillis();
440+
final Long last = ARTICLE_RATE_LIMIT.get(ip);
441+
if (null != last && now - last < 1000) {
442+
return true;
443+
}
444+
445+
ARTICLE_RATE_LIMIT.put(ip, now);
446+
447+
// 防止表过大,简单清理 60 秒前的记录
448+
if (ARTICLE_RATE_LIMIT.size() > 10000) {
449+
final long cutoff = now - 60_000;
450+
ARTICLE_RATE_LIMIT.entrySet().removeIf(e -> e.getValue() < cutoff);
451+
}
452+
return false;
453+
}
454+
425455
/**
426456
* api for get article details
427457
*
@@ -435,6 +465,11 @@ public void showArticleApi(final RequestContext context) {
435465
final String articleId = context.pathVar("id");
436466
final Request request = context.getRequest();
437467

468+
if (hitArticleRateLimit(request)) {
469+
context.renderCodeMsg(503, "访问过快,请稍候再试");
470+
return;
471+
}
472+
438473
final JSONObject article = articleQueryService.getArticleById(articleId);
439474
if (null == article) {
440475
context.renderCodeMsg(404, "帖子不存在!");
@@ -1180,6 +1215,11 @@ public void showArticle(final RequestContext context) {
11801215
final String articleId = context.pathVar("articleId");
11811216
final Request request = context.getRequest();
11821217

1218+
if (hitArticleRateLimit(request)) {
1219+
context.sendError(503);
1220+
return;
1221+
}
1222+
11831223
final AbstractFreeMarkerRenderer renderer = new SkinRenderer(context, "article.ftl");
11841224
final Map<String, Object> dataModel = renderer.getDataModel();
11851225

0 commit comments

Comments
 (0)