From c628104a30cd6c29c0dadc6b37b2251a2af4f0fc Mon Sep 17 00:00:00 2001
From: xiongxiaoyang <773861846@qq.com>
Date: Wed, 1 Jun 2022 13:40:17 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E9=9B=86=E6=88=90=20Sentinel=20?=
=?UTF-8?q?=E5=AE=9E=E7=8E=B0=E6=8E=A5=E5=8F=A3=E9=98=B2=E5=88=B7=E5=92=8C?=
=?UTF-8?q?=E9=99=90=E6=B5=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
pom.xml | 15 ++-
.../core/common/constant/ErrorCodeEnum.java | 14 ++-
.../xxyopen/novel/core/config/WebConfig.java | 18 +++-
.../interceptor/FlowLimitInterceptor.java | 98 +++++++++++++++++++
4 files changed, 139 insertions(+), 6 deletions(-)
create mode 100644 src/main/java/io/github/xxyopen/novel/core/interceptor/FlowLimitInterceptor.java
diff --git a/pom.xml b/pom.xml
index 675e55a..21eb3da 100644
--- a/pom.xml
+++ b/pom.xml
@@ -111,18 +111,31 @@
jackson-databind
-
+
org.springframework.boot
spring-boot-starter-amqp
+
com.xuxueli
xxl-job-core
${xxl-job.version}
+
+
+ com.alibaba.csp
+ sentinel-core
+ 1.8.4
+
+
+ com.alibaba.csp
+ sentinel-parameter-flow-control
+ 1.8.4
+
+
mysql
mysql-connector-java
diff --git a/src/main/java/io/github/xxyopen/novel/core/common/constant/ErrorCodeEnum.java b/src/main/java/io/github/xxyopen/novel/core/common/constant/ErrorCodeEnum.java
index 0f53844..8d10836 100644
--- a/src/main/java/io/github/xxyopen/novel/core/common/constant/ErrorCodeEnum.java
+++ b/src/main/java/io/github/xxyopen/novel/core/common/constant/ErrorCodeEnum.java
@@ -82,6 +82,16 @@ public enum ErrorCodeEnum {
* */
USER_UN_AUTH("A0301","访问未授权"),
+ /**
+ * 用户请求服务异常
+ * */
+ USER_REQ_EXCEPTION("A0500","用户请求服务异常"),
+
+ /**
+ * 请求超出限制
+ * */
+ USER_REQ_MANY("A0501","请求超出限制"),
+
/**
* 用户评论异常
* */
@@ -137,11 +147,11 @@ public enum ErrorCodeEnum {
/**
* 错误码
* */
- private String code;
+ private final String code;
/**
* 中文描述
* */
- private String message;
+ private final String message;
}
diff --git a/src/main/java/io/github/xxyopen/novel/core/config/WebConfig.java b/src/main/java/io/github/xxyopen/novel/core/config/WebConfig.java
index 18f9276..a456df2 100644
--- a/src/main/java/io/github/xxyopen/novel/core/config/WebConfig.java
+++ b/src/main/java/io/github/xxyopen/novel/core/config/WebConfig.java
@@ -4,6 +4,7 @@ import io.github.xxyopen.novel.core.constant.ApiRouterConsts;
import io.github.xxyopen.novel.core.constant.SystemConfigConsts;
import io.github.xxyopen.novel.core.interceptor.AuthInterceptor;
import io.github.xxyopen.novel.core.interceptor.FileInterceptor;
+import io.github.xxyopen.novel.core.interceptor.FlowLimitInterceptor;
import io.github.xxyopen.novel.core.interceptor.TokenParseInterceptor;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
@@ -22,6 +23,8 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
+ private final FlowLimitInterceptor flowLimitInterceptor;
+
private final AuthInterceptor authInterceptor;
private final FileInterceptor fileInterceptor;
@@ -30,9 +33,16 @@ public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
+
+ // 流量限制拦截器
+ registry.addInterceptor(flowLimitInterceptor)
+ .addPathPatterns("/**")
+ .order(0);
+
// 文件访问拦截
registry.addInterceptor(fileInterceptor)
- .addPathPatterns(SystemConfigConsts.IMAGE_UPLOAD_DIRECTORY + "**");
+ .addPathPatterns(SystemConfigConsts.IMAGE_UPLOAD_DIRECTORY + "**")
+ .order(1);
// 权限认证拦截
registry.addInterceptor(authInterceptor)
@@ -45,12 +55,14 @@ public class WebConfig implements WebMvcConfigurer {
// 放行登录注册相关请求接口
.excludePathPatterns(ApiRouterConsts.API_FRONT_USER_URL_PREFIX + "/register",
ApiRouterConsts.API_FRONT_USER_URL_PREFIX + "/login",
- ApiRouterConsts.API_ADMIN_URL_PREFIX + "/login");
+ ApiRouterConsts.API_ADMIN_URL_PREFIX + "/login")
+ .order(2);
// Token 解析拦截器
registry.addInterceptor(tokenParseInterceptor)
// 拦截小说内容查询接口,需要解析 token 以判断该用户是否有权阅读该章节(付费章节是否已购买)
- .addPathPatterns(ApiRouterConsts.API_FRONT_BOOK_URL_PREFIX + "/content/*");
+ .addPathPatterns(ApiRouterConsts.API_FRONT_BOOK_URL_PREFIX + "/content/*")
+ .order(3);
}
}
diff --git a/src/main/java/io/github/xxyopen/novel/core/interceptor/FlowLimitInterceptor.java b/src/main/java/io/github/xxyopen/novel/core/interceptor/FlowLimitInterceptor.java
new file mode 100644
index 0000000..f8c22f2
--- /dev/null
+++ b/src/main/java/io/github/xxyopen/novel/core/interceptor/FlowLimitInterceptor.java
@@ -0,0 +1,98 @@
+package io.github.xxyopen.novel.core.interceptor;
+
+import com.alibaba.csp.sentinel.Entry;
+import com.alibaba.csp.sentinel.EntryType;
+import com.alibaba.csp.sentinel.SphU;
+import com.alibaba.csp.sentinel.slots.block.BlockException;
+import com.alibaba.csp.sentinel.slots.block.RuleConstant;
+import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
+import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
+import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowRule;
+import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowRuleManager;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.github.xxyopen.novel.core.common.constant.ErrorCodeEnum;
+import io.github.xxyopen.novel.core.common.resp.RestResp;
+import io.github.xxyopen.novel.core.common.util.IpUtils;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.MediaType;
+import org.springframework.stereotype.Component;
+import org.springframework.web.servlet.HandlerInterceptor;
+
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * 流量限制 拦截器
+ * 实现接口防刷和限流
+ *
+ * @author xiongxiaoyang
+ * @date 2022/6/1
+ */
+@Component
+@RequiredArgsConstructor
+@Slf4j
+public class FlowLimitInterceptor implements HandlerInterceptor {
+
+ private final ObjectMapper objectMapper;
+
+ /**
+ * 定义一个对所有的请求进行统一限制的资源
+ */
+ private static final String ALL_LIMIT_RESOURCE = "allLimitResource";
+
+ static {
+ // 接口限流规则:所有的请求,限制每秒最多只能通过 2000 个,超出限制匀速排队
+ List rules = new ArrayList<>();
+ FlowRule rule1 = new FlowRule();
+ rule1.setResource(ALL_LIMIT_RESOURCE);
+ rule1.setGrade(RuleConstant.FLOW_GRADE_QPS);
+ // Set limit QPS to 2000.
+ rule1.setCount(2000);
+ rule1.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER);
+ rules.add(rule1);
+ FlowRuleManager.loadRules(rules);
+
+ // 接口防刷规则 1:所有的请求,限制每个 IP 每秒最多只能通过 50 个,超出限制直接拒绝
+ ParamFlowRule rule2 = new ParamFlowRule(ALL_LIMIT_RESOURCE)
+ .setParamIdx(0)
+ .setCount(50);
+ // 接口防刷规则 2:所有的请求,限制每个 IP 每分钟最多只能通过 1000 个,超出限制直接拒绝
+ ParamFlowRule rule3 = new ParamFlowRule(ALL_LIMIT_RESOURCE)
+ .setParamIdx(0)
+ .setCount(1000)
+ .setDurationInSec(60);
+ ParamFlowRuleManager.loadRules(Arrays.asList(rule2, rule3));
+ }
+
+ @Override
+ public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
+ String ip = IpUtils.getRealIp(request);
+ Entry entry = null;
+ try {
+ // 若需要配置例外项,则传入的参数只支持基本类型。
+ // EntryType 代表流量类型,其中系统规则只对 IN 类型的埋点生效
+ // count 大多数情况都填 1,代表统计为一次调用。
+ entry = SphU.entry(ALL_LIMIT_RESOURCE, EntryType.IN, 1, ip);
+ // Your logic here.
+ return HandlerInterceptor.super.preHandle(request, response, handler);
+ } catch (BlockException ex) {
+ // Handle request rejection.
+ log.info("IP:{}被限流了!", ip);
+ response.setCharacterEncoding(StandardCharsets.UTF_8.name());
+ response.setContentType(MediaType.APPLICATION_JSON_VALUE);
+ response.getWriter().write(objectMapper.writeValueAsString(RestResp.fail(ErrorCodeEnum.USER_REQ_MANY)));
+ } finally {
+ // 注意:exit 的时候也一定要带上对应的参数,否则可能会有统计错误。
+ if (entry != null) {
+ entry.exit(1, ip);
+ }
+ }
+ return false;
+ }
+
+}