6565import java .text .SimpleDateFormat ;
6666import java .util .List ;
6767import 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