正在显示
8 个修改的文件
包含
225 行增加
和
15 行删除
| @@ -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.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 | // 回显数据字典 |
-
请 注册 或 登录 后发表评论