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; + } + +}