feat: 集成 Sentinel 实现接口防刷和限流

This commit is contained in:
xiongxiaoyang 2022-06-01 13:40:17 +08:00
parent 9894814fe4
commit c628104a30
4 changed files with 139 additions and 6 deletions

15
pom.xml
View File

@ -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>

View File

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

View File

@ -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);
}
}

View File

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