完成后端移除Redis改造
This commit is contained in:
@@ -59,6 +59,12 @@
|
||||
<artifactId>ruoyi-system</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
</project>
|
||||
|
||||
@@ -1,22 +1,18 @@
|
||||
package com.ruoyi.framework.aspectj;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.aspectj.lang.JoinPoint;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.aspectj.lang.annotation.Before;
|
||||
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.RedisScript;
|
||||
import org.springframework.stereotype.Component;
|
||||
import com.ruoyi.common.annotation.RateLimiter;
|
||||
import com.ruoyi.common.core.redis.RedisCache;
|
||||
import com.ruoyi.common.enums.LimitType;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.common.utils.ip.IpUtils;
|
||||
|
||||
/**
|
||||
@@ -30,20 +26,11 @@ public class RateLimiterAspect
|
||||
{
|
||||
private static final Logger log = LoggerFactory.getLogger(RateLimiterAspect.class);
|
||||
|
||||
private RedisTemplate<Object, Object> redisTemplate;
|
||||
private final RedisCache redisCache;
|
||||
|
||||
private RedisScript<Long> limitScript;
|
||||
|
||||
@Autowired
|
||||
public void setRedisTemplate1(RedisTemplate<Object, Object> redisTemplate)
|
||||
public RateLimiterAspect(RedisCache redisCache)
|
||||
{
|
||||
this.redisTemplate = redisTemplate;
|
||||
}
|
||||
|
||||
@Autowired
|
||||
public void setLimitScript(RedisScript<Long> limitScript)
|
||||
{
|
||||
this.limitScript = limitScript;
|
||||
this.redisCache = redisCache;
|
||||
}
|
||||
|
||||
@Before("@annotation(rateLimiter)")
|
||||
@@ -53,15 +40,14 @@ public class RateLimiterAspect
|
||||
int count = rateLimiter.count();
|
||||
|
||||
String combineKey = getCombineKey(rateLimiter, point);
|
||||
List<Object> keys = Collections.singletonList(combineKey);
|
||||
try
|
||||
{
|
||||
Long number = redisTemplate.execute(limitScript, keys, count, time);
|
||||
if (StringUtils.isNull(number) || number.intValue() > count)
|
||||
long number = redisCache.increment(combineKey, time, TimeUnit.SECONDS);
|
||||
if (number > count)
|
||||
{
|
||||
throw new ServiceException("访问过于频繁,请稍候再试");
|
||||
}
|
||||
log.info("限制请求'{}',当前请求'{}',缓存key'{}'", count, number.intValue(), combineKey);
|
||||
log.info("限制请求'{}',当前请求'{}',缓存key'{}'", count, number, combineKey);
|
||||
}
|
||||
catch (ServiceException e)
|
||||
{
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
package com.ruoyi.framework.config;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||
import org.springframework.data.redis.serializer.SerializationException;
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONReader;
|
||||
import com.alibaba.fastjson2.JSONWriter;
|
||||
import com.alibaba.fastjson2.filter.Filter;
|
||||
import com.ruoyi.common.constant.Constants;
|
||||
|
||||
/**
|
||||
* Redis使用FastJson序列化
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
|
||||
{
|
||||
public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
|
||||
|
||||
static final Filter AUTO_TYPE_FILTER = JSONReader.autoTypeFilter(Constants.JSON_WHITELIST_STR);
|
||||
|
||||
private Class<T> clazz;
|
||||
|
||||
public FastJson2JsonRedisSerializer(Class<T> clazz)
|
||||
{
|
||||
super();
|
||||
this.clazz = clazz;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] serialize(T t) throws SerializationException
|
||||
{
|
||||
if (t == null)
|
||||
{
|
||||
return new byte[0];
|
||||
}
|
||||
return JSON.toJSONString(t, JSONWriter.Feature.WriteClassName).getBytes(DEFAULT_CHARSET);
|
||||
}
|
||||
|
||||
@Override
|
||||
public T deserialize(byte[] bytes) throws SerializationException
|
||||
{
|
||||
if (bytes == null || bytes.length <= 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
String str = new String(bytes, DEFAULT_CHARSET);
|
||||
|
||||
return JSON.parseObject(str, clazz, AUTO_TYPE_FILTER);
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
package com.ruoyi.framework.config;
|
||||
|
||||
import org.springframework.cache.annotation.CachingConfigurerSupport;
|
||||
import org.springframework.cache.annotation.EnableCaching;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
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.StringRedisSerializer;
|
||||
|
||||
/**
|
||||
* redis配置
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
@Configuration
|
||||
@EnableCaching
|
||||
public class RedisConfig extends CachingConfigurerSupport
|
||||
{
|
||||
@Bean
|
||||
@SuppressWarnings(value = { "unchecked", "rawtypes" })
|
||||
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory)
|
||||
{
|
||||
RedisTemplate<Object, Object> template = new RedisTemplate<>();
|
||||
template.setConnectionFactory(connectionFactory);
|
||||
|
||||
FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
|
||||
|
||||
// 使用StringRedisSerializer来序列化和反序列化redis的key值
|
||||
template.setKeySerializer(new StringRedisSerializer());
|
||||
template.setValueSerializer(serializer);
|
||||
|
||||
// Hash的key也采用StringRedisSerializer的序列化方式
|
||||
template.setHashKeySerializer(new StringRedisSerializer());
|
||||
template.setHashValueSerializer(serializer);
|
||||
|
||||
template.afterPropertiesSet();
|
||||
return template;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public DefaultRedisScript<Long> limitScript()
|
||||
{
|
||||
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
|
||||
redisScript.setScriptText(limitScriptText());
|
||||
redisScript.setResultType(Long.class);
|
||||
return redisScript;
|
||||
}
|
||||
|
||||
/**
|
||||
* 限流脚本
|
||||
*/
|
||||
private String limitScriptText()
|
||||
{
|
||||
return "local key = KEYS[1]\n" +
|
||||
"local count = tonumber(ARGV[1])\n" +
|
||||
"local time = tonumber(ARGV[2])\n" +
|
||||
"local current = redis.call('get', key);\n" +
|
||||
"if current and tonumber(current) > count then\n" +
|
||||
" return tonumber(current);\n" +
|
||||
"end\n" +
|
||||
"current = redis.call('incr', key)\n" +
|
||||
"if tonumber(current) == 1 then\n" +
|
||||
" redis.call('expire', key, time)\n" +
|
||||
"end\n" +
|
||||
"return tonumber(current);";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.ruoyi.framework.aspectj;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import org.aspectj.lang.JoinPoint;
|
||||
import org.aspectj.lang.Signature;
|
||||
import org.aspectj.lang.reflect.MethodSignature;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import com.ruoyi.common.annotation.RateLimiter;
|
||||
import com.ruoyi.common.core.cache.InMemoryCacheStore;
|
||||
import com.ruoyi.common.core.redis.RedisCache;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
|
||||
class RateLimiterAspectTest
|
||||
{
|
||||
@Test
|
||||
void shouldRejectThirdRequestWithinWindow() throws Throwable
|
||||
{
|
||||
RateLimiterAspect aspect = new RateLimiterAspect(new RedisCache(new InMemoryCacheStore()));
|
||||
JoinPoint joinPoint = mockJoinPoint(TestRateLimitTarget.class.getDeclaredMethod("limited"));
|
||||
RateLimiter rateLimiter = TestRateLimitTarget.class.getDeclaredMethod("limited").getAnnotation(RateLimiter.class);
|
||||
|
||||
assertDoesNotThrow(() -> aspect.doBefore(joinPoint, rateLimiter));
|
||||
assertDoesNotThrow(() -> aspect.doBefore(joinPoint, rateLimiter));
|
||||
assertThrows(ServiceException.class, () -> aspect.doBefore(joinPoint, rateLimiter));
|
||||
}
|
||||
|
||||
private JoinPoint mockJoinPoint(Method method)
|
||||
{
|
||||
MethodSignature signature = mock(MethodSignature.class);
|
||||
when(signature.getMethod()).thenReturn(method);
|
||||
|
||||
JoinPoint joinPoint = mock(JoinPoint.class);
|
||||
when(joinPoint.getSignature()).thenReturn((Signature) signature);
|
||||
return joinPoint;
|
||||
}
|
||||
|
||||
static class TestRateLimitTarget
|
||||
{
|
||||
@RateLimiter(count = 2, time = 60)
|
||||
public void limited()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package com.ruoyi.framework.web.service;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
import com.ruoyi.common.annotation.RepeatSubmit;
|
||||
import com.ruoyi.common.constant.CacheConstants;
|
||||
import com.ruoyi.common.core.cache.InMemoryCacheStore;
|
||||
import com.ruoyi.common.core.domain.entity.SysUser;
|
||||
import com.ruoyi.common.core.domain.model.LoginUser;
|
||||
import com.ruoyi.common.core.redis.RedisCache;
|
||||
import com.ruoyi.framework.interceptor.impl.SameUrlDataInterceptor;
|
||||
import com.ruoyi.system.service.ISysConfigService;
|
||||
|
||||
class TokenServiceLocalCacheTest
|
||||
{
|
||||
@AfterEach
|
||||
void clearRequestContext()
|
||||
{
|
||||
RequestContextHolder.resetRequestAttributes();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldStoreLoginUserWithTokenTtl()
|
||||
{
|
||||
RedisCache redisCache = new RedisCache(new InMemoryCacheStore());
|
||||
TokenService tokenService = new TokenService();
|
||||
ReflectionTestUtils.setField(tokenService, "redisCache", redisCache);
|
||||
ReflectionTestUtils.setField(tokenService, "header", "Authorization");
|
||||
ReflectionTestUtils.setField(tokenService, "secret", "unit-test-secret");
|
||||
ReflectionTestUtils.setField(tokenService, "expireTime", 30);
|
||||
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.setRemoteAddr("127.0.0.1");
|
||||
request.addHeader("User-Agent", "Mozilla/5.0");
|
||||
RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request));
|
||||
|
||||
SysUser user = new SysUser();
|
||||
user.setUserName("admin");
|
||||
user.setPassword("password");
|
||||
LoginUser loginUser = new LoginUser();
|
||||
loginUser.setUser(user);
|
||||
|
||||
String jwt = tokenService.createToken(loginUser);
|
||||
|
||||
assertNotNull(jwt);
|
||||
assertNotNull(redisCache.getCacheObject(CacheConstants.LOGIN_TOKEN_KEY + loginUser.getToken()));
|
||||
assertTrue(redisCache.getExpire(CacheConstants.LOGIN_TOKEN_KEY + loginUser.getToken()) > 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDeleteCaptchaAfterValidation()
|
||||
{
|
||||
RedisCache redisCache = new RedisCache(new InMemoryCacheStore());
|
||||
SysLoginService loginService = new SysLoginService();
|
||||
ISysConfigService configService = mock(ISysConfigService.class);
|
||||
when(configService.selectCaptchaEnabled()).thenReturn(true);
|
||||
ReflectionTestUtils.setField(loginService, "redisCache", redisCache);
|
||||
ReflectionTestUtils.setField(loginService, "configService", configService);
|
||||
|
||||
redisCache.setCacheObject(CacheConstants.CAPTCHA_CODE_KEY + "uuid", "ABCD", 1, TimeUnit.MINUTES);
|
||||
|
||||
loginService.validateCaptcha("admin", "ABCD", "uuid");
|
||||
|
||||
assertFalse(redisCache.hasKey(CacheConstants.CAPTCHA_CODE_KEY + "uuid"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSupportMillisecondRepeatSubmitWindow() throws Exception
|
||||
{
|
||||
RedisCache redisCache = new RedisCache(new InMemoryCacheStore());
|
||||
SameUrlDataInterceptor interceptor = new SameUrlDataInterceptor();
|
||||
ReflectionTestUtils.setField(interceptor, "redisCache", redisCache);
|
||||
ReflectionTestUtils.setField(interceptor, "header", "Authorization");
|
||||
|
||||
RepeatSubmit repeatSubmit = RepeatSubmitTarget.class.getDeclaredMethod("submit")
|
||||
.getAnnotation(RepeatSubmit.class);
|
||||
|
||||
MockHttpServletRequest firstRequest = createRepeatRequest();
|
||||
assertFalse(interceptor.isRepeatSubmit(firstRequest, repeatSubmit));
|
||||
|
||||
MockHttpServletRequest secondRequest = createRepeatRequest();
|
||||
assertTrue(interceptor.isRepeatSubmit(secondRequest, repeatSubmit));
|
||||
|
||||
Thread.sleep(70);
|
||||
MockHttpServletRequest thirdRequest = createRepeatRequest();
|
||||
assertFalse(interceptor.isRepeatSubmit(thirdRequest, repeatSubmit));
|
||||
}
|
||||
|
||||
private MockHttpServletRequest createRepeatRequest()
|
||||
{
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.setRequestURI("/loan/pricing");
|
||||
request.addHeader("Authorization", "token-1");
|
||||
request.addParameter("loanId", "1001");
|
||||
return request;
|
||||
}
|
||||
|
||||
static class RepeatSubmitTarget
|
||||
{
|
||||
@RepeatSubmit(interval = 50)
|
||||
public void submit()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user