How to limit request frequency in Java

How to limit request frequency in Java

Certainly we want to limit the frequency of user visits, because users could get angry, and angry people will click on our app like crazy.

They could also be very bad and use some crawler to try to bring down our server.

So how to do that?

In this article, we use springboot and log the user’s information and access frequency into redis, you can also store it into memory or database.

Think about this story, from first principles

The user may not be logged in, or may already be logged in.

If the user is logged in, we limit him based on the username, otherwise, we use IP or some unique device code.

Ip is used in this article.

We want it to be simple enough to be used on multiple methods without writing additional code. So we are going to use interface cutouts.

The @interface

 1public @interface RequestRateLimit {
 2
 3	/**
 4	 * the key, we use it to distinguish user request objects, like /info1 /info2 
 5	 * @return
 6	 */
 7	String key() default "";
 8
 9	/**
10	 * just ignore this for now
11	 * @return
12	 */
13	RateType type() default RateType.PER_CLIENT;
14
15	/**
16	 * frequency, like once per minute
17	 * @return
18	 */
19	long rate() default 1;
20
21	/**
22	 * interval, like every one minute
23	 * @return
24	 */
25	long rateInterval() default 60 * 1000;
26
27	/**
28	 * interval unit, like minute
29	 * @return
30	 */
31	RateIntervalUnit timeUnit() default RateIntervalUnit.MILLISECONDS;
32
33}

The Aspect

You can just copy this code and test it.

 1public class RequestRateLimitAspect {
 2
 3	private RedissonClient redisson;
 4	private final UserService userService;
 5    
 6	@Pointcut("@annotation(RequestRateLimit)")
 7	public void findAnnotationPointCut(RequestRateLimit RequestRateLimit) {
 8	}
 9
10	@Around(value = "findAnnotationPointCut(requestRateLimit)", argNames = "joinPoint,requestRateLimit")
11	public Object around(ProceedingJoinPoint joinPoint, RequestRateLimit requestRateLimit) throws Throwable {
12		UserEntity user = userService.getCurrentRequestUser(); // just SecurityContextHolder.getContext().getAuthentication().getPrincipal();
13		String realIp = "";
14		if (user == null) {
15			RequestAttributes ra = RequestContextHolder.getRequestAttributes();
16			ServletRequestAttributes sra = (ServletRequestAttributes) ra;
17			if (null != sra) {
18				HttpServletRequest request = sra.getRequest();
19				realIp = request.getHeader("His-Real-IP");
20				if (notValidIp(realIp)) {
21					realIp = request.getHeader("His-Real-IP2");
22					if (notValidIp(realIp)) {
23						realIp = request.getRemoteAddr();
24					}
25				}
26			}
27		}
28		if (user == null && notValidIp(realIp)) {
29			return R.failed(EMPTY_USER, "login info not found");
30		}
31		// the real logic
32		String key = user == null || StrUtil.isBlank(user.getUserName()) ? realIp : user.getUserName();
33		key = key + "::" + joinPoint.getSignature().getName();
34		RRateLimiter limiter = getRateLimiter(requestRateLimit, key);
35		if (limiter.tryAcquire(1)) {
36			return joinPoint.proceed();
37		} else {
38			log.info("rate-limit: {} {} {}", user == null ? "" : user.getUserName(), realIp, joinPoint.getSignature());
39			return R.failed(REACH_REQUEST_LIMIT, String.format("too often, please retry after:%s %s", requestRateLimit.rateInterval(), requestRateLimit.timeUnit().name().toLowerCase()));
40		}
41	}
42
43	private boolean notValidIp(String ip) {
44		return StrUtil.isBlank(ip) || ip.startsWith("172.1"); // docker bridge ip
45	}
46
47	private RRateLimiter getRateLimiter(RequestRateLimit limit, String defaultKey) {
48		RRateLimiter rRateLimiter = redisson.getRateLimiter(StrUtil.isBlank(limit.key()) ? RATE_LIMITER + "::" + defaultKey : limit.key()); // RATE_LIMITER is a constant, change it to whatever you want
49		if (rRateLimiter.isExists()) {
50			RateLimiterConfig existed = rRateLimiter.getConfig();
51			if (!Objects.equals(limit.rate(), existed.getRate())
52					|| !Objects.equals(limit.timeUnit().toMillis(limit.rateInterval()), existed.getRateInterval())
53					|| !Objects.equals(limit.type(), existed.getRateType())) {
54				rRateLimiter.delete();
55				rRateLimiter.trySetRate(limit.type(), limit.rate(), limit.rateInterval(), limit.timeUnit());
56				expireByConfig(rRateLimiter, limit);
57			}
58		} else {
59			rRateLimiter.trySetRate(limit.type(), limit.rate(), limit.rateInterval(), limit.timeUnit());
60			expireByConfig(rRateLimiter, limit);
61		}
62
63		return rRateLimiter;
64	}
65
66	private void expireByConfig(RRateLimiter rRateLimiter, RequestRateLimit limit) {
67		long limitDuration = limit.timeUnit().toMillis(limit.rateInterval()) + 5000;
68		rRateLimiter.expire(Instant.now().plusMillis(limitDuration));
69	}
70}

Usage

1    @GetMapping("/info")
2	@RequestRateLimit(rate = 2, rateInterval = 1, timeUnit = RateIntervalUnit.MINUTES) // Maximum of two requests per minute
3	public R getInfo() {
4        // ...
5    }

When a user requests the /info method, a key such as RATE_LIMITER::his_user_name::com.package.getInfo is stored in redis.

if the user requests the /info method more than twice in a minute, he will receive an error and the getInfo method will not execute.

Note that this annotation does not work on methods annotated with @Cacheable.

More

We can implement a customized frequency limit that can restrict arbitrary methods, such as urgent emails sent to Ops staff, if the same subject has been sent, don’t send it again within 5 minutes.

  1public @interface CustomRateLimit {
  2    /**
  3     * key 的前缀,用于一组相同功能限流的标记
  4     * @return
  5     */
  6    String prefix() default "";
  7
  8    /**
  9     * 限流的 key,要求不为空,支持从参数中读取
 10     * @return
 11     */
 12    String key() default "#key";
 13
 14    /**
 15     * 限流模式,默认单机
 16     * @return
 17     */
 18    RateType type() default RateType.PER_CLIENT;
 19
 20    /**
 21     * 限流速率,1次/分钟
 22     * @return
 23     */
 24    long rate() default 1;
 25
 26    /**
 27     * 限流速率,每分钟
 28     * @return
 29     */
 30    long rateInterval() default 60 * 1000;
 31
 32    /**
 33     * 限流速率单位
 34     * @return
 35     */
 36    RateIntervalUnit timeUnit() default RateIntervalUnit.MILLISECONDS;
 37
 38}
 39
 40public class CustomRateLimitAspect {
 41
 42	private final RedissonClient redisson;
 43	/**
 44	 * 根据自定义注解获取切点
 45	 *
 46	 * @param CustomRateLimit 注解接口
 47	 */
 48	@Pointcut("@annotation(CustomRateLimit)")
 49	public void findAnnotationPointCut(CustomRateLimit CustomRateLimit) {
 50	}
 51
 52	@Around(value = "findAnnotationPointCut(customRateLimit)", argNames = "joinPoint,customRateLimit")
 53	public Object around(ProceedingJoinPoint joinPoint, CustomRateLimit customRateLimit) throws Throwable {
 54		// 限流拦截器
 55		String key = getKey(joinPoint, customRateLimit);
 56		RRateLimiter limiter = getRateLimiter(customRateLimit, key);
 57		if (limiter.tryAcquire(1)) {
 58			return joinPoint.proceed();
 59		} else {
 60			log.info("skip method cause violate rate limit, key is {}", key);
 61			return R.failed(REACH_REQUEST_LIMIT, String.format("请求过于频繁,请于以下时间后重试:%s %s", customRateLimit.rateInterval(), customRateLimit.timeUnit().name().toLowerCase()));
 62		}
 63	}
 64
 65	/**
 66	 * 获取限流拦截器
 67	 *
 68	 * @param limit  在要限流的方法上的配置
 69	 * @return 限流器
 70	 */
 71	private RRateLimiter getRateLimiter(CustomRateLimit limit, String key) {
 72		RRateLimiter rRateLimiter = redisson.getRateLimiter(CUSTOM_RATE_LIMITER_PREFIX + "::" + limit.prefix() + "::" + key);
 73		// 设置限流
 74		if (rRateLimiter.isExists()) {
 75			RateLimiterConfig existed = rRateLimiter.getConfig();
 76			// 判断配置是否更新,如果更新,重新加载限流器配置
 77			if (!Objects.equals(limit.rate(), existed.getRate())
 78					|| !Objects.equals(limit.timeUnit().toMillis(limit.rateInterval()), existed.getRateInterval())
 79					|| !Objects.equals(limit.type(), existed.getRateType())) {
 80				rRateLimiter.delete();
 81				rRateLimiter.trySetRate(limit.type(), limit.rate(), limit.rateInterval(), limit.timeUnit());
 82				expireByConfig(rRateLimiter, limit);
 83			}
 84		} else {
 85			rRateLimiter.trySetRate(limit.type(), limit.rate(), limit.rateInterval(), limit.timeUnit());
 86			expireByConfig(rRateLimiter, limit);
 87		}
 88
 89		return rRateLimiter;
 90	}
 91
 92	private void expireByConfig(RRateLimiter rRateLimiter, CustomRateLimit limit) {
 93		long limitDuration = limit.timeUnit().toMillis(limit.rateInterval()) + 5000;
 94		rRateLimiter.expire(Instant.now().plusMillis(limitDuration));
 95	}
 96
 97	// el表达式支持
 98	private String getKey(JoinPoint joinPoint, CustomRateLimit customRateLimit) {
 99		ExpressionParser expressionParser = new SpelExpressionParser();
100		Expression expression = expressionParser.parseExpression(customRateLimit.key());
101		CodeSignature methodSignature = (CodeSignature) joinPoint.getSignature();
102		String[] sigParamNames = methodSignature.getParameterNames();
103		EvaluationContext context = new StandardEvaluationContext();
104		Object[] args = joinPoint.getArgs();
105		for (int i = 0; i < sigParamNames.length; i++) {
106			context.setVariable(sigParamNames[i], args[i]);
107		}
108		return (String) expression.getValue(context);
109	}
110}

使用

1	@Override
2	@CustomRateLimit(prefix = Constants.Cache.EMAIL_RATE_LIMITER, rateInterval = 5, timeUnit = RateIntervalUnit.MINUTES) // 5分钟最多一次
3	public void sendToMaintainersWithFrequencyLimit(String key, String subject, String... content) {
4		sendToMaintainers("[紧急通知]",  subject, content);
5	}