【 Say little 】

1. How to limit the frequency of accessing the same interface from the same IP address per second, minute, or hour

Without further ado, let’s start by writing an annotation class


import java.lang.annotation.*;

/** * Interface traffic limiting *@author rs
 *
 */
@Inherited
@Documented
@Target({ElementType.FIELD,ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface VisitLimit {
    // Identifies the access limit for a specified SEC period
    int limit(a) default 5;  
    // Identify the time range
    long sec(a) default 5;
}
Copy the code

The reason for using annotations is that we use interceptors to check whether a request interface has an annotation before processing the request, and if so, to get access times and time periods (e.g., once in 1s). Let’s write an interceptor

import org.test.annotation.VisitLimit;
import org.test.exception.BusinessException;
import org.test.redis.RedisCache;
import org.test.service.redis.RedisService;
import org.test.util.IPUtils;
import org.test.util.RedisUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Method;

@Component
public class VisitLimitInterceptor extends HandlerInterceptorAdapter {
	 
    @Autowired
	private RedisUtils redisService;

    /** * is called * before the request is processed@param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (handler instanceof HandlerMethod) {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            Method method = handlerMethod.getMethod();
            if(! method.isAnnotationPresent(VisitLimit.class)) {return true;
            }
            VisitLimit accessLimit = method.getAnnotation(VisitLimit.class);
            if (accessLimit == null) {
                return true;
            }
            int limit = accessLimit.limit();
            long sec = accessLimit.sec();
            String key = IPUtils.getIpAddr(request) + request.getRequestURI();
            Integer maxLimit =null;
            Object value =redisService.get(key);
            if(value! =null && !value.equals("")) {
            	maxLimit = Integer.valueOf(String.valueOf(value));
            }
            if (maxLimit == null) {
            	redisService.set(key, "1", sec);
            } else if (maxLimit < limit) {
            	Integer i = maxLimit+1;
            	redisService.set(key, i.toString(), sec);
            } else {
// output(response, "Too many requests!") );
// return false;
                throw new BusinessException(500."Too many requests!"); }}return true;
    }
 
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {}@Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {}Copy the code

The key (IP+URL) of redis records the number of visits to an interface from an IP address, and the value stores the number of visits plus an expiration time, which is the value assigned to the annotation.

Part of the redis code here is also posted

@Service
public class RedisUtils {
    @Resource
    private RedisTemplate redisTemplate;

  /** * Write cache set aging time **@param key
     * @param value
     * @paramExpireTime Indicates the validity time, in seconds@return* /
    public boolean set(final String key, Object value, Long expireTime) {
        boolean result = false;
        try {
            ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
            operations.set(key, value);
            redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);
            result = true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        returnresult; }}Copy the code

How to obtain the user’s real IP address?? The following

import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.servlet.http.HttpServletRequest; /** * IP Utils * @author rs * */ public class IPUtils { private static Logger logger = LoggerFactory.getLogger(IPUtils.class); If Nginx is used, you cannot obtain an IP address from Request.getremoteaddr (). If this forwarder is forwarded-to x-Forwarded-for, the value of THIS forwarded-to x-Forwarded-for is not a single forwarded-to address. X-forwarded-for specifies the first valid IP string that is not unknown. Public static String getIpAddr(HttpServletRequest Request) {String IP = null; try { ip = request.getHeader("x-forwarded-for"); if (ip ! = null && ip.length() ! = 0 &&!" Unknown ".equalsignorecase (IP)) {// If (ip.indexof (",")! =-1 ){ ip = ip.split(",")[0]; } } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("Proxy-Client-IP"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("WL-Proxy-Client-IP"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("HTTP_CLIENT_IP"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("HTTP_X_FORWARDED_FOR"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("X-Real-IP"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getRemoteAddr(); } } catch (Exception e) { logger.error("IPUtils ERROR ", e); } return ip; }}Copy the code

Now let’s use it formally

@VisitLimit(limit = 1, sec = 1)
@RequestMapping(value = "/close", method = RequestMethod.POST)
Copy the code

This is not a good way to deal with sudden requests, need to smooth this kind of situation, such as 200ms processing a request, the next to the token bucket appear!

Two, the actual combat introduction of the token bucket

2.1 To summarize, let us distinguish when to use token buckets and when to use leaky buckets

  • Token bucket: Produces one token and consumes one token
  • Leaky bucket: handle heavy flow and smooth it out at a fixed speed

Usage scenario: Geteway gateway

Bucket4j is implemented based on the token bucket algorithm

package org.test.gateway.filter.limit;

import java.time.Duration;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;

import io.github.bucket4j.Bandwidth;
import io.github.bucket4j.Bucket;
import io.github.bucket4j.Bucket4j;
import io.github.bucket4j.Refill;
import reactor.core.publisher.Mono;

@Component
public class RateLimitByIpFilter implements GatewayFilter.Ordered {

	private final static Logger logger = LoggerFactory.getLogger(RateLimitByIpFilter.class);

	private int capacity;

	private int refillTokens;

	private Duration refillDuration;

	private static final Map<String, Bucket> CACHE = new ConcurrentHashMap<>();

	public RateLimitByIpFilter(a) {}public RateLimitByIpFilter(int capacity, int refillTokens, Duration refillDuration) {
		this.capacity = capacity;
		this.refillTokens = refillTokens;
		this.refillDuration = refillDuration;
	}

	@Override
	public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
		String ip = getIpAddr(exchange.getRequest());
		if (ip.indexOf("192.168") != -1 || ip.indexOf("172.31.202") != -1) {
			return chain.filter(exchange);
		}
		Bucket bucket = CACHE.computeIfAbsent(ip, k -> createNewBucket());
		logger.info("Frequency-limited visiting IP:" + ip + ", number of available tokens: + bucket.getAvailableTokens());
		if (bucket.tryConsume(1)) {
			return chain.filter(exchange);
		} else {
			// Invalid (exceeding the current limit)
			ServerHttpResponse response = exchange.getResponse();
			/ / set the headers
			HttpHeaders httpHeaders = response.getHeaders();
			httpHeaders.add("Content-Type"."application/json; charset=UTF-8");
			httpHeaders.add("Cache-Control"."no-store, no-cache, must-revalidate, max-age=0");
			/ / set the body
			String warningStr = "{\"code\":\"500\",\"message\":\" Overflow stream \"}";
			DataBuffer bodyDataBuffer = response.bufferFactory().wrap(warningStr.getBytes());
			returnresponse.writeWith(Mono.just(bodyDataBuffer)); }}@Override
	public int getOrder(a) {
		return -1000;
	}

	private Bucket createNewBucket(a) {
		Refill refill = Refill.of(refillTokens, refillDuration);
		Bandwidth limit = Bandwidth.classic(capacity, refill);
		return Bucket4j.builder().addLimit(limit).build();
	}

	public static String getIpAddr(ServerHttpRequest request) {
		String ip = "";
		String str = request.getHeaders().getFirst("x-forwarded-for");
		if (StringUtils.isEmpty(str)) {
			ip = request.getRemoteAddress().getAddress().getHostAddress();
			logger.info("The IP address obtained through traffic limiting in address mode is:" + ip);
			return ip;
		} else {
			String[] ips = str.split(",");
			for (String s : ips) {
				if (s.indexOf("192.168") != -1 || s.indexOf("172.31.202") != -1) {
					continue;
				}
				ip = ip + s + ",";
			}
			ip = ip.substring(0, ip.length() - 1);
			logger.info("This IP address is forwarded-to x-Forwarded-for, and is filtered out of the Intranet." + ip);
			if (ip.indexOf(",") != -1) {
				ip = ip.substring(0, ip.indexOf(","));
				return ip.trim();
			}
			returnip.trim(); }}}Copy the code

The route: test-auth /** API will be routed to the test-auth service

@SpringBootApplication
public class GateWayApplication {

	public static void main(String[] args) {
		SpringApplication.run(GateWayApplication.class, args);
	}

	@Bean
	public RouteLocator routeLocator(RouteLocatorBuilder builder) {
		return builder.routes()
				// Certification center
				.route(r -> r.path("/TEST-AUTH/**")
						.filters(f -> f.stripPrefix(1).filter(new RateLimitByIpFilter(1.1, Duration.ofSeconds(1))))
						.uri("lb://TEST-AUTH").id("TEST-AUTH")) .build(); }}Copy the code

3. Distributed flow limiting (excerpted here from JD.com’s buying business)

Use Redis+Lua to achieve

local key = "rate.limit:" .. KEYS[1] -- local limit = tonumber(ARGV[1]) -- local current = tonumber(redis. Call ('get', Key) or "0") if current +1 > limit then Call ("expire", key,"1") redis. Call ("expire", key,"1") return 1 endCopy the code
public static boolean accquire(a) throws IOException, URISyntaxException {
    Jedis jedis = new Jedis("127.0.0.1");
    File luaFile = new File(RedisLimitRateWithLUA.class.getResource("/").toURI().getPath() + "limit.lua");
    String luaScript = FileUtils.readFileToString(luaFile);

    String key = "ip:" + System.currentTimeMillis()/1000; / / the second
    String limit = "5"; // Maximum limit
    List<String> keys = new ArrayList<String>();
    keys.add(key);
    List<String> args = new ArrayList<String>();
    args.add(limit);
    Long result = (Long)(jedis.eval(luaScript, keys, args)); // Execute the lua script and pass in the parameters
    return result == 1;
}
Copy the code

Redis k = rate.limit: IP: current second V: 5

Welcome to correct communication!!

Thank you for reading this, if this article is well written and if you feel there is something to it

Ask for a thumbs up 👍 ask for attention ❤️ ask for share 👥 for 8 abs I really very useful!!

If there are any mistakes in this blog, please comment, thank you very much! ❤ ️ ❤ ️ ❤ ️ ❤ ️