diff --git a/pom.xml b/pom.xml index 675e55a..21eb3da 100644 --- a/pom.xml +++ b/pom.xml @@ -111,18 +111,31 @@ <artifactId>jackson-databind</artifactId> </dependency> - <!-- MQ 相关--> + <!-- MQ 相关 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency> + <!-- XXL-JOB 相关 --> <dependency> <groupId>com.xuxueli</groupId> <artifactId>xxl-job-core</artifactId> <version>${xxl-job.version}</version> </dependency> + <!-- sentinel 相关 --> + <dependency> + <groupId>com.alibaba.csp</groupId> + <artifactId>sentinel-core</artifactId> + <version>1.8.4</version> + </dependency> + <dependency> + <groupId>com.alibaba.csp</groupId> + <artifactId>sentinel-parameter-flow-control</artifactId> + <version>1.8.4</version> + </dependency> + <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> 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<FlowRule> 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; + } + +}