Distributed-Redis-based Interface IP Current Limitation
In order to prevent our interfaces from being accessed maliciously, for example, when someone accesses our interfaces frequently through JMeter tools, the interface response becomes slower or even crashes, so we need to limit the IP current of some specific interfaces, that is, the number of accesses to the same IP at a certain time is limited.
The implementation principle uses Redis as the core principle of current limiting component. The user's IP address is regarded as Key, and the number of visits is value in a period of time. At the same time, the key expiration time is set.
For example, an interface with the same IP10 seconds requests five times, more than five times do not allow access to the interface.
1. The first time the IP address is stored in redis, the key value is IP address, the value is 1, and the expiration time of the key value is set to 10 seconds. 2. The second time the IP address is stored in redis, if the key does not expire, the update value is 2. 3. By analogy, when value is already 5, if the next IP address is stored in redis and the key has not expired, then the IP can not be accessed. 4. When 10 seconds later, the key value expires, then the IP address comes in again, value starts from 1, and the expiration time is 10 seconds, which repeats and repeats.
It can be seen from the above logic that the number of visits is limited in a period of time, not that the IP access interface is totally excluded.
Technical framework SpringBoot + RedisTemplate (done with custom annotations)
This can be used for real project development scenarios.
I. Code
1. Custom Annotations
The purpose of using custom annotations here is to use custom annotations on the interface to make the code look very neat.
IpLimiter
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface IpLimiter { /** * Current limiting ip */ String ipAdress() ; /** * Number of requests per unit time limit */ long limit() default 10; /** * Unit time, unit second */ long time() default 1; /** * Tips for reaching current limit */ String message(); }
2. Test Interface
Used a custom annotation @IpLimiter on the interface
@Controller public class IpController { private static final Logger LOGGER = LoggerFactory.getLogger(IpController.class); private static final String MESSAGE = "request was aborted,Your IP Too frequent visits"; //Instead of getting the requested ip, write to death an IP @ResponseBody @RequestMapping("iplimiter") @IpLimiter(ipAdress = "127.198.66.01", limit = 5, time = 10, message = MESSAGE) public String sendPayment(HttpServletRequest request) throws Exception { return "Successful request"; } @ResponseBody @RequestMapping("iplimiter1") @IpLimiter(ipAdress = "127.188.145.54", limit = 4, time = 10, message = MESSAGE) public String sendPayment1(HttpServletRequest request) throws Exception { return "Successful request"; } }
3. AOP Processing IpLimter Annotations
This side deals with custom annotations in a slice-wise manner. In order to ensure atomicity, the redis script ipLimiter.lua is written here to execute the redis command to ensure operation atomicity.
@Aspect @Component public class IpLimterHandler { private static final Logger LOGGER = LoggerFactory.getLogger(IpLimterHandler.class); @Autowired RedisTemplate redisTemplate; /** * getRedisScript Read script tool class * This is set to Long because the ipLimiter.lua script returns a number type */ private DefaultRedisScript<Long> getRedisScript; @PostConstruct public void init() { getRedisScript = new DefaultRedisScript<>(); getRedisScript.setResultType(Long.class); getRedisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("ipLimiter.lua"))); LOGGER.info("IpLimterHandler[Distributed Current Limiting Processor]Script loading completed"); } /** * This cut-off point can be avoided, because the following itself is a commentary */ // @Pointcut("@annotation(com.jincou.iplimiter.annotation.IpLimiter)") // public void rateLimiter() {} /** * If the above cut point is retained, then here it can be written as * @Around("rateLimiter()&&@annotation(ipLimiter)") */ @Around("@annotation(ipLimiter)") public Object around(ProceedingJoinPoint proceedingJoinPoint, IpLimiter ipLimiter) throws Throwable { if (LOGGER.isDebugEnabled()) { LOGGER.debug("IpLimterHandler[Distributed Current Limiting Processor]Start current limiting operation"); } Signature signature = proceedingJoinPoint.getSignature(); if (!(signature instanceof MethodSignature)) { throw new IllegalArgumentException("the Annotation @IpLimter must used on method!"); } /** * Getting annotation parameters */ // Current Limiting Module IP String limitIp = ipLimiter.ipAdress(); Preconditions.checkNotNull(limitIp); // Current Limiting Threshold long limitTimes = ipLimiter.limit(); // Current Limiting Overtime long expireTime = ipLimiter.time(); if (LOGGER.isDebugEnabled()) { LOGGER.debug("IpLimterHandler[Distributed Current Limiting Processor]The parameter value is-limitTimes={},limitTimeout={}", limitTimes, expireTime); } // Current Limiting Tips String message = ipLimiter.message(); /** * Executing Lua scripts */ List<String> ipList = new ArrayList(); // Set the key value to the value in the comment ipList.add(limitIp); /** * Call the script and execute it */ Long result = (Long) redisTemplate.execute(getRedisScript, ipList, expireTime, limitTimes); if (result == 0) { String msg = "Because of exceeding unit time=" + expireTime + "-Permissible number of requests=" + limitTimes + "[Triggered current limiting]"; LOGGER.debug(msg); // Reaching the current limit and returning to the front-end information return message; } if (LOGGER.isDebugEnabled()) { LOGGER.debug("IpLimterHandler[Distributed Current Limiting Processor]Current Limiting Execution Results-result={},request[normal]response", result); } return proceedingJoinPoint.proceed(); } }
4. RedisCacheConfig (configuration class)
@Configuration public class RedisCacheConfig { private static final Logger LOGGER = LoggerFactory.getLogger(RedisCacheConfig.class); @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(factory); //Use Jackson 2Json RedisSerializer to serialize and deserialize the value of redis (default JDK serialization) Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer(Object.class); ObjectMapper mapper = new ObjectMapper(); mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); serializer.setObjectMapper(mapper); template.setValueSerializer(serializer); //Use String RedisSerializer to serialize and deserialize the key value of redis template.setKeySerializer(new StringRedisSerializer()); template.afterPropertiesSet(); LOGGER.info("Springboot RedisTemplate Loading completed"); return template; } }
5. ipLimiter.lua script
Advantage
Reduce network overhead: scripts are executed only once, and multiple requests are not needed to reduce network transmission;
Assuring atomic operations: The entire script is executed as an atom without worrying about concurrency.
--Obtain KEY local key1 = KEYS[1] local val = redis.call('incr', key1) local ttl = redis.call('ttl', key1) --Obtain ARGV Parameters inside and print local expire = ARGV[1] local times = ARGV[2] redis.log(redis.LOG_DEBUG,tostring(times)) redis.log(redis.LOG_DEBUG,tostring(expire)) redis.log(redis.LOG_NOTICE, "incr "..key1.." "..val); if val == 1 then redis.call('expire', key1, tonumber(expire)) else if ttl == -1 then redis.call('expire', key1, tonumber(expire)) end end if val > tonumber(times) then return 0 end return 1
6,application.properties
#redis spring.redis.hostName= spring.redis.host= spring.redis.port=6379 spring.redis.jedis.pool.max-active=8 spring.redis.jedis.pool.max-wait= spring.redis.jedis.pool.max-idle=8 spring.redis.jedis.pool.min-idle=10 spring.redis.timeout=100ms spring.redis.password= logging.path= /Users/xub/log logging.level.com.jincou.iplimiter=DEBUG server.port=8888
7. SpringBoot Startup Class
@SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
8. Testing
Perfectly, this test is in line with our expectations. The first five accesses to the interface were successful, and then failed. It was not accessible until 10 seconds later, so it was repeated and repeated.
The other side is not shown one by one. Attached is the source code of the project.
Github address https://github.com/yudiandemingzi/spring-boot-redis-ip-limiter
Reference resources
This design is really good when I brush github. I just made some changes on the basis of it. Thank you very much for the author's sharing.
github address: https://github.com/TaXueWWL/shleld-ratelimter
There are good articles about AOP: The use of @ annotation() in spring aop
As long as you become excellent, everything else will follow (Lieutenant General 1)