浏览代码

加入限流

wangliang 1 年之前
父节点
当前提交
e5bcb0d303

+ 2 - 0
themis-admin/src/main/java/com/qmth/themis/admin/api/TEExamController.java

@@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.qmth.themis.business.annotation.ApiJsonObject;
 import com.qmth.themis.business.annotation.ApiJsonProperty;
 import com.qmth.themis.business.annotation.Logs;
+import com.qmth.themis.business.annotation.RedisLimitAnnotation;
 import com.qmth.themis.business.bean.admin.CountStopBean;
 import com.qmth.themis.business.cache.ExamingDataCacheUtil;
 import com.qmth.themis.business.cache.RedisKeyHelper;
@@ -445,6 +446,7 @@ public class TEExamController {
     @ApiOperation(value = "考试属性统计接口")
     @RequestMapping(value = "/prop/count", method = RequestMethod.POST)
     @ApiResponses({@ApiResponse(code = 200, message = "考试属性信息", response = ExamPropCountDto.class)})
+    @RedisLimitAnnotation(key = "propCount", period = 1, count = 1)
     public Result propCount(@ApiParam(value = "考试id", required = true) @RequestParam Long examId,
                             @ApiParam(value = "考试场次id") @RequestParam(required = false) Long examActivityId) {
 //        long start = System.currentTimeMillis();

+ 3 - 0
themis-admin/src/main/java/com/qmth/themis/admin/api/TIeInvigilateController.java

@@ -6,6 +6,7 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.qmth.themis.business.annotation.ApiJsonObject;
 import com.qmth.themis.business.annotation.ApiJsonProperty;
+import com.qmth.themis.business.annotation.RedisLimitAnnotation;
 import com.qmth.themis.business.bean.admin.*;
 import com.qmth.themis.business.cache.ExamRecordCacheUtil;
 import com.qmth.themis.business.cache.bean.ExamActivityCacheBean;
@@ -94,6 +95,7 @@ public class TIeInvigilateController {
     @ApiOperation(value = "实时监控台视频列表接口")
     @RequestMapping(value = "/list/video", method = RequestMethod.POST)
     @ApiResponses({@ApiResponse(code = 200, message = "监考监控信息", response = InvigilateListVideoBean.class)})
+    @RedisLimitAnnotation(key = "listVideo", period = 1, count = 1)
     public Result listVideo(@ApiParam(value = "考试批次id", required = false) @RequestParam(required = false) Long examId,
                             @ApiParam(value = "考试场次id", required = false) @RequestParam(required = false) Long examActivityId,
                             @ApiParam(value = "虚拟考场代码", required = false) @RequestParam(required = false) String roomCode,
@@ -182,6 +184,7 @@ public class TIeInvigilateController {
     @ApiOperation(value = "实时监控台视频随机列表接口")
     @RequestMapping(value = "/list/video/random", method = RequestMethod.POST)
     @ApiResponses({@ApiResponse(code = 200, message = "监考监控信息", response = InvigilateListVideoBean.class)})
+    @RedisLimitAnnotation(key = "listVideoRandom", period = 1, count = 1)
     public Result listVideoRandom(@ApiParam(value = "考试批次id", required = false) @RequestParam(required = false) Long examId, @ApiParam(value = "随机数", required = true) @RequestParam Integer randomNum) {
         if (Objects.isNull(randomNum) || Objects.equals(randomNum, "")) {
             throw new BusinessException("随机数不能为空");

+ 14 - 0
themis-admin/src/main/resources/limit.lua

@@ -0,0 +1,14 @@
+local key = "rate.limit:" .. KEYS[1]
+
+local limit = tonumber(ARGV[1])
+
+local current = tonumber(redis.call('get', key) or "0")
+
+if current + 1 > limit then
+  return 0
+else
+   -- 没有超阈值,将当前访问数量+1,并设置2秒过期(可根据自己的业务情况调整)
+   redis.call("INCRBY", key,"1")
+   redis.call("expire", key,"2")
+   return current + 1
+end

+ 31 - 0
themis-business/src/main/java/com/qmth/themis/business/annotation/RedisLimitAnnotation.java

@@ -0,0 +1,31 @@
+package com.qmth.themis.business.annotation;
+
+import java.lang.annotation.*;
+
+@Target({ElementType.METHOD, ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+@Documented
+public @interface RedisLimitAnnotation {
+
+    /**
+     * key
+     */
+    String key() default "";
+
+    /**
+     * Key的前缀
+     */
+    String prefix() default "";
+
+    /**
+     * 一定时间内最多访问次数
+     */
+    int count();
+
+    /**
+     * 给定的时间范围 单位(秒)
+     */
+    int period();
+}
+

+ 75 - 0
themis-business/src/main/java/com/qmth/themis/business/aspect/LimitRestAspect.java

@@ -0,0 +1,75 @@
+package com.qmth.themis.business.aspect;
+
+import com.qmth.themis.business.annotation.RedisLimitAnnotation;
+import com.qmth.themis.business.util.ServletUtil;
+import com.qmth.themis.common.util.IpUtil;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Pointcut;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.script.DefaultRedisScript;
+import org.springframework.stereotype.Component;
+
+import java.lang.reflect.Method;
+import java.util.Collections;
+import java.util.List;
+
+@Aspect
+@Component
+public class LimitRestAspect {
+    private final static Logger log = LoggerFactory.getLogger(LimitRestAspect.class);
+
+    @Autowired
+    private RedisTemplate<String, Object> redisTemplate;
+
+    @Autowired
+    private DefaultRedisScript<Long> redisluaScript;
+
+
+    @Pointcut(value = "@annotation(com.qmth.themis.business.annotation.RedisLimitAnnotation)")
+    public void rateLimit() {
+
+    }
+
+    @Around("rateLimit()")
+    public Object interceptor(ProceedingJoinPoint joinPoint) throws Throwable {
+        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
+        Method method = signature.getMethod();
+        Class<?> targetClass = method.getDeclaringClass();
+        RedisLimitAnnotation rateLimit = method.getAnnotation(RedisLimitAnnotation.class);
+        if (rateLimit != null) {
+            String ipAddress = IpUtil.getRemoteIp(ServletUtil.getRequest());
+            StringBuffer stringBuffer = new StringBuffer();
+            stringBuffer.append(ipAddress).append("-")
+                    .append(targetClass.getName()).append("- ")
+                    .append(method.getName()).append("-")
+                    .append(rateLimit.key());
+            List<String> keys = Collections.singletonList(stringBuffer.toString());
+            //调用lua脚本,获取返回结果,这里即为请求的次数
+            Long number = redisTemplate.execute(
+                    redisluaScript,
+                    // 此处传参只要能转为Object就行(因为数字不能直接强转为String,所以不能用String序列化)
+                    //new GenericToStringSerializer<>(Object.class),
+                    // 结果的类型需要根据脚本定义,此处是数字--定义的是Long类型
+                    //new GenericToStringSerializer<>(Long.class)
+                    keys,
+                    rateLimit.count(),
+                    rateLimit.period()
+            );
+            if (number != null && number.intValue() != 0 && number.intValue() <= rateLimit.count()) {
+                log.info("限流时间段内访问了第:{} 次", number.toString());
+                return joinPoint.proceed();
+            }
+        } else {
+            return joinPoint.proceed();
+        }
+        return null;
+//        throw new RuntimeException("访问频率过快,被限流了");
+    }
+}
+

+ 11 - 1
themis-business/src/main/java/com/qmth/themis/business/config/RedisConfig.java

@@ -6,18 +6,20 @@ import com.fasterxml.jackson.annotation.PropertyAccessor;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
 import com.qmth.themis.business.constant.SystemConstant;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.context.annotation.Primary;
+import org.springframework.core.io.ClassPathResource;
 import org.springframework.data.redis.cache.RedisCacheConfiguration;
 import org.springframework.data.redis.cache.RedisCacheManager;
 import org.springframework.data.redis.cache.RedisCacheWriter;
 import org.springframework.data.redis.connection.RedisConnectionFactory;
 import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.script.DefaultRedisScript;
 import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
 import org.springframework.data.redis.serializer.RedisSerializationContext;
 import org.springframework.data.redis.serializer.StringRedisSerializer;
+import org.springframework.scripting.support.ResourceScriptSource;
 
 import java.time.Duration;
 
@@ -103,4 +105,12 @@ public class RedisConfig {
         template.afterPropertiesSet();
         return template;
     }
+
+    @Bean
+    public DefaultRedisScript<Long> redisluaScript() {
+        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
+        redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("limit.lua")));
+        redisScript.setResultType(Long.class);
+        return redisScript;
+    }
 }

+ 0 - 1
themis-exam/src/main/java/com/qmth/themis/exam/aspect/ApiExamControllerAspect.java

@@ -1,6 +1,5 @@
 package com.qmth.themis.exam.aspect;
 
-import com.qmth.boot.api.config.ApiControllerAspect;
 import com.qmth.themis.business.constant.SystemConstant;
 import com.qmth.themis.business.util.JacksonUtil;
 import com.qmth.themis.business.util.ServletUtil;