作者 RuoYi

支持自定义注解实现接口限流

@@ -14,5 +14,5 @@ import java.lang.annotation.Target; @@ -14,5 +14,5 @@ import java.lang.annotation.Target;
14 @Retention(RetentionPolicy.RUNTIME) 14 @Retention(RetentionPolicy.RUNTIME)
15 public @interface Excels 15 public @interface Excels
16 { 16 {
17 - Excel[] value(); 17 + public Excel[] value();
18 } 18 }
  1 +package com.ruoyi.common.annotation;
  2 +
  3 +import java.lang.annotation.Documented;
  4 +import java.lang.annotation.ElementType;
  5 +import java.lang.annotation.Retention;
  6 +import java.lang.annotation.RetentionPolicy;
  7 +import java.lang.annotation.Target;
  8 +import com.ruoyi.common.constant.Constants;
  9 +import com.ruoyi.common.enums.LimitType;
  10 +
  11 +/**
  12 + * 限流注解
  13 + *
  14 + * @author ruoyi
  15 + */
  16 +@Target(ElementType.METHOD)
  17 +@Retention(RetentionPolicy.RUNTIME)
  18 +@Documented
  19 +public @interface RateLimiter
  20 +{
  21 + /**
  22 + * 限流key
  23 + */
  24 + public String key() default Constants.RATE_LIMIT_KEY;
  25 +
  26 + /**
  27 + * 限流时间,单位秒
  28 + */
  29 + public int time() default 60;
  30 +
  31 + /**
  32 + * 限流次数
  33 + */
  34 + public int count() default 100;
  35 +
  36 + /**
  37 + * 限流类型
  38 + */
  39 + public LimitType limitType() default LimitType.DEFAULT;
  40 +}
@@ -75,6 +75,11 @@ public class Constants @@ -75,6 +75,11 @@ public class Constants
75 public static final String REPEAT_SUBMIT_KEY = "repeat_submit:"; 75 public static final String REPEAT_SUBMIT_KEY = "repeat_submit:";
76 76
77 /** 77 /**
  78 + * 限流 redis key
  79 + */
  80 + public static final String RATE_LIMIT_KEY = "rate_limit:";
  81 +
  82 + /**
78 * 验证码有效期(分钟) 83 * 验证码有效期(分钟)
79 */ 84 */
80 public static final Integer CAPTCHA_EXPIRATION = 2; 85 public static final Integer CAPTCHA_EXPIRATION = 2;
  1 +package com.ruoyi.common.enums;
  2 +
  3 +/**
  4 + * 限流类型
  5 + *
  6 + * @author ruoyi
  7 + */
  8 +
  9 +public enum LimitType
  10 +{
  11 + /**
  12 + * 默认策略全局限流
  13 + */
  14 + DEFAULT,
  15 +
  16 + /**
  17 + * 根据请求者IP进行限流
  18 + */
  19 + IP
  20 +}
  1 +package com.ruoyi.framework.aspectj;
  2 +
  3 +import java.lang.reflect.Method;
  4 +import java.util.Collections;
  5 +import java.util.List;
  6 +import org.aspectj.lang.JoinPoint;
  7 +import org.aspectj.lang.Signature;
  8 +import org.aspectj.lang.annotation.Aspect;
  9 +import org.aspectj.lang.annotation.Before;
  10 +import org.aspectj.lang.annotation.Pointcut;
  11 +import org.aspectj.lang.reflect.MethodSignature;
  12 +import org.slf4j.Logger;
  13 +import org.slf4j.LoggerFactory;
  14 +import org.springframework.beans.factory.annotation.Autowired;
  15 +import org.springframework.data.redis.core.RedisTemplate;
  16 +import org.springframework.data.redis.core.script.RedisScript;
  17 +import org.springframework.stereotype.Component;
  18 +import com.ruoyi.common.annotation.RateLimiter;
  19 +import com.ruoyi.common.enums.LimitType;
  20 +import com.ruoyi.common.exception.ServiceException;
  21 +import com.ruoyi.common.utils.ServletUtils;
  22 +import com.ruoyi.common.utils.StringUtils;
  23 +import com.ruoyi.common.utils.ip.IpUtils;
  24 +
  25 +/**
  26 + * 限流处理
  27 + *
  28 + * @author ruoyi
  29 + */
  30 +@Aspect
  31 +@Component
  32 +public class RateLimiterAspect
  33 +{
  34 + private static final Logger log = LoggerFactory.getLogger(RateLimiterAspect.class);
  35 +
  36 + private RedisTemplate<Object, Object> redisTemplate;
  37 +
  38 + private RedisScript<Long> limitScript;
  39 +
  40 + @Autowired
  41 + public void setRedisTemplate1(RedisTemplate<Object, Object> redisTemplate)
  42 + {
  43 + this.redisTemplate = redisTemplate;
  44 + }
  45 +
  46 + @Autowired
  47 + public void setLimitScript(RedisScript<Long> limitScript)
  48 + {
  49 + this.limitScript = limitScript;
  50 + }
  51 +
  52 + // 配置织入点
  53 + @Pointcut("@annotation(com.ruoyi.common.annotation.RateLimiter)")
  54 + public void rateLimiterPointCut()
  55 + {
  56 + }
  57 +
  58 + @Before("rateLimiterPointCut()")
  59 + public void doBefore(JoinPoint point) throws Throwable
  60 + {
  61 + RateLimiter rateLimiter = getAnnotationRateLimiter(point);
  62 + String key = rateLimiter.key();
  63 + int time = rateLimiter.time();
  64 + int count = rateLimiter.count();
  65 +
  66 + String combineKey = getCombineKey(rateLimiter, point);
  67 + List<Object> keys = Collections.singletonList(combineKey);
  68 + try
  69 + {
  70 + Long number = redisTemplate.execute(limitScript, keys, count, time);
  71 + if (StringUtils.isNull(number) || number.intValue() > count)
  72 + {
  73 + throw new ServiceException("访问过于频繁,请稍后再试");
  74 + }
  75 + log.info("限制请求'{}',当前请求'{}',缓存key'{}'", count, number.intValue(), key);
  76 + }
  77 + catch (ServiceException e)
  78 + {
  79 + throw e;
  80 + }
  81 + catch (Exception e)
  82 + {
  83 + throw new RuntimeException("服务器限流异常,请稍后再试");
  84 + }
  85 + }
  86 +
  87 + /**
  88 + * 是否存在注解,如果存在就获取
  89 + */
  90 + private RateLimiter getAnnotationRateLimiter(JoinPoint joinPoint)
  91 + {
  92 + Signature signature = joinPoint.getSignature();
  93 + MethodSignature methodSignature = (MethodSignature) signature;
  94 + Method method = methodSignature.getMethod();
  95 +
  96 + if (method != null)
  97 + {
  98 + return method.getAnnotation(RateLimiter.class);
  99 + }
  100 + return null;
  101 + }
  102 +
  103 + public String getCombineKey(RateLimiter rateLimiter, JoinPoint point)
  104 + {
  105 + StringBuffer stringBuffer = new StringBuffer(rateLimiter.key());
  106 + if (rateLimiter.limitType() == LimitType.IP)
  107 + {
  108 + stringBuffer.append(IpUtils.getIpAddr(ServletUtils.getRequest()));
  109 + }
  110 + MethodSignature signature = (MethodSignature) point.getSignature();
  111 + Method method = signature.getMethod();
  112 + Class<?> targetClass = method.getDeclaringClass();
  113 + stringBuffer.append("-").append(targetClass.getName()).append("- ").append(method.getName());
  114 + return stringBuffer.toString();
  115 + }
  116 +}
@@ -6,6 +6,7 @@ import org.springframework.context.annotation.Bean; @@ -6,6 +6,7 @@ import org.springframework.context.annotation.Bean;
6 import org.springframework.context.annotation.Configuration; 6 import org.springframework.context.annotation.Configuration;
7 import org.springframework.data.redis.connection.RedisConnectionFactory; 7 import org.springframework.data.redis.connection.RedisConnectionFactory;
8 import org.springframework.data.redis.core.RedisTemplate; 8 import org.springframework.data.redis.core.RedisTemplate;
  9 +import org.springframework.data.redis.core.script.DefaultRedisScript;
9 import org.springframework.data.redis.serializer.StringRedisSerializer; 10 import org.springframework.data.redis.serializer.StringRedisSerializer;
10 import com.fasterxml.jackson.annotation.JsonAutoDetect; 11 import com.fasterxml.jackson.annotation.JsonAutoDetect;
11 import com.fasterxml.jackson.annotation.JsonTypeInfo; 12 import com.fasterxml.jackson.annotation.JsonTypeInfo;
@@ -47,4 +48,32 @@ public class RedisConfig extends CachingConfigurerSupport @@ -47,4 +48,32 @@ public class RedisConfig extends CachingConfigurerSupport
47 template.afterPropertiesSet(); 48 template.afterPropertiesSet();
48 return template; 49 return template;
49 } 50 }
  51 +
  52 + @Bean
  53 + public DefaultRedisScript<Long> limitScript()
  54 + {
  55 + DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
  56 + redisScript.setScriptText(limitScriptText());
  57 + redisScript.setResultType(Long.class);
  58 + return redisScript;
  59 + }
  60 +
  61 + /**
  62 + * 限流脚本
  63 + */
  64 + private String limitScriptText()
  65 + {
  66 + return "local key = KEYS[1]\n" +
  67 + "local count = tonumber(ARGV[1])\n" +
  68 + "local time = tonumber(ARGV[2])\n" +
  69 + "local current = redis.call('get', key);\n" +
  70 + "if current and tonumber(current) > count then\n" +
  71 + " return current;\n" +
  72 + "end\n" +
  73 + "current = redis.call('incr', key)\n" +
  74 + "if tonumber(current) == 1 then\n" +
  75 + " redis.call('expire', key, time)\n" +
  76 + "end\n" +
  77 + "return current;";
  78 + }
50 } 79 }
@@ -29,9 +29,9 @@ service.interceptors.request.use(config => { @@ -29,9 +29,9 @@ service.interceptors.request.use(config => {
29 if (typeof value === 'object') { 29 if (typeof value === 'object') {
30 for (const key of Object.keys(value)) { 30 for (const key of Object.keys(value)) {
31 if (value[key] !== null && typeof (value[key]) !== 'undefined') { 31 if (value[key] !== null && typeof (value[key]) !== 'undefined') {
32 - let params = propName + '[' + key + ']'  
33 - let subPart = encodeURIComponent(params) + '='  
34 - url += subPart + encodeURIComponent(value[key]) + '&' 32 + let params = propName + '[' + key + ']';
  33 + let subPart = encodeURIComponent(params) + '=';
  34 + url += subPart + encodeURIComponent(value[key]) + '&';
35 } 35 }
36 } 36 }
37 } else { 37 } else {
@@ -55,17 +55,17 @@ export function resetForm(refName) { @@ -55,17 +55,17 @@ export function resetForm(refName) {
55 55
56 // 添加日期范围 56 // 添加日期范围
57 export function addDateRange(params, dateRange, propName) { 57 export function addDateRange(params, dateRange, propName) {
58 - let search = params  
59 - search.params = typeof (search.params) === 'object' && search.params !== null && !Array.isArray(search.params) ? search.params : {}  
60 - dateRange = Array.isArray(dateRange) ? dateRange : []  
61 - if (typeof (propName) === 'undefined') {  
62 - search.params['beginTime'] = dateRange[0]  
63 - search.params['endTime'] = dateRange[1]  
64 - } else {  
65 - search.params['begin' + propName] = dateRange[0]  
66 - search.params['end' + propName] = dateRange[1]  
67 - }  
68 - return search 58 + let search = params;
  59 + search.params = typeof (search.params) === 'object' && search.params !== null && !Array.isArray(search.params) ? search.params : {};
  60 + dateRange = Array.isArray(dateRange) ? dateRange : [];
  61 + if (typeof (propName) === 'undefined') {
  62 + search.params['beginTime'] = dateRange[0];
  63 + search.params['endTime'] = dateRange[1];
  64 + } else {
  65 + search.params['begin' + propName] = dateRange[0];
  66 + search.params['end' + propName] = dateRange[1];
  67 + }
  68 + return search;
69 } 69 }
70 70
71 // 回显数据字典 71 // 回显数据字典