All Downloads are FREE. Search and download functionalities are using the official Maven repository.

rebue.wheel.vertx.web.LimitRateHandler Maven / Gradle / Ivy

The newest version!
package rebue.wheel.vertx.web;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;

import org.apache.commons.lang3.StringUtils;

import io.vertx.core.Future;
import io.vertx.core.http.HttpServerRequest;
import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.handler.SecurityPolicyHandler;
import io.vertx.redis.client.Command;
import io.vertx.redis.client.RedisAPI;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import rebue.wheel.api.dic.HttpStatusCodeDic;
import rebue.wheel.vertx.config.WebProperties;

/**
 * 限流处理器
 */
@Slf4j
public class LimitRateHandler implements SecurityPolicyHandler {
    private final RedisAPI redisApi;
    private final String   redisPrefix;
    private String         blackListRedisPrefix;
    private String         scriptSha;

    @SneakyThrows
    public LimitRateHandler(WebProperties.LimitRateProperties limitRateProps,
            WebProperties.BlackListProperties blackListProperties, RedisAPI redisApi) {
        log.info("初始化限流处理器");
        if (redisApi == null) {
            throw new IllegalArgumentException("限流处理器依赖 Redis,必须保证 Redis 正确加载");
        }
        this.redisApi = redisApi;

        if (StringUtils.isBlank(limitRateProps.getRedisPrefix())) {
            throw new IllegalArgumentException("必须配置 redisPrefix 参数");
        }
        this.redisPrefix = limitRateProps.getRedisPrefix();

        if (blackListProperties.getEnabled()) {
            blackListRedisPrefix = blackListProperties.getRedisPrefix();
        }

        String script;
        try (InputStream inputStream = LimitRateHandler.class.getResourceAsStream(
                "/script/TimeSlidingWindowRateLimiter.lua")) {
            assert inputStream != null;
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(
                    inputStream, StandardCharsets.UTF_8))) {
                StringBuilder sb = new StringBuilder();
                String        line;
                while ((line = reader.readLine()) != null) {
                    sb.append(line).append(System.lineSeparator());
                }
                script = sb.toString();
            }
        }

        this.redisApi.send(Command.SCRIPT, "load", script).onSuccess(res -> {
            scriptSha = res.toString();
            log.info("Redis 加载限流的 Lua 脚本: {}", scriptSha);
        }).onFailure(err -> log.error("Redis 加载限流的 Lua 脚本出错", err));
    }

    @Override
    public void handle(RoutingContext routingContext) {
        log.debug("进入限流处理器");
        HttpServerRequest request = routingContext.request();
        if (!request.isEnded()) {
            log.debug("暂停请求");
            request.pause();
        }

        limitRate(redisPrefix + "size{" + redisPrefix + "global}",
                redisPrefix + "limit{" + redisPrefix + "global}",
                redisPrefix + "window{" + redisPrefix + "global}",
                redisPrefix + "expires{" + redisPrefix + "global}").onSuccess(globalRes -> {
                    if (globalRes) {
                        String srcIp = request.remoteAddress().host();
                        String[] args;
                        if (blackListRedisPrefix == null) {
                            args = new String[] {
                                    redisPrefix + srcIp + "-size{" + srcIp + "}",
                                    redisPrefix + srcIp + "-limit{" + srcIp + "}",
                                    redisPrefix + srcIp + "-window{" + srcIp + "}",
                                    redisPrefix + srcIp + "-expires{" + srcIp + "}" };
                        } else {
                            args = new String[] {
                                    redisPrefix + srcIp + "-size{" + srcIp + "}",
                                    redisPrefix + srcIp + "-limit{" + srcIp + "}",
                                    redisPrefix + srcIp + "-window{" + srcIp + "}",
                                    redisPrefix + srcIp + "-expires{" + srcIp + "}",
                                    blackListRedisPrefix + srcIp + "{" + srcIp + "}" };
                        }

                        limitRate(args).onSuccess(ipRes -> {
                            if (!request.isEnded()) {
                                log.debug("恢复请求");
                                request.resume();
                            }
                            if (ipRes) {
                                routingContext.next();
                            } else {
                                routingContext.fail(HttpStatusCodeDic.TOO_MANY_REQUESTS.getCode());
                            }
                        }).onFailure(err -> {
                            log.error("Redis 执行IP限流的 Lua 脚本出错", err);
                            if (!request.isEnded()) {
                                log.debug("恢复请求");
                                request.resume();
                            }
                            routingContext.fail(HttpStatusCodeDic.BAD_GATEWAY.getCode());
                        });
                    } else {
                        if (!request.isEnded()) {
                            log.debug("恢复请求");
                            request.resume();
                        }
                        routingContext.fail(HttpStatusCodeDic.TOO_MANY_REQUESTS.getCode());
                    }
                }).onFailure(err -> {
                    log.error("Redis 执行全局限流的 Lua 脚本出错", err);
                    if (!request.isEnded()) {
                        log.debug("恢复请求");
                        request.resume();
                    }
                    routingContext.fail(HttpStatusCodeDic.BAD_GATEWAY.getCode());
                });
    }

    private Future limitRate(String... args) {
        List list = new LinkedList<>();
        list.add(scriptSha);
        list.add(String.valueOf(args.length));
        Collections.addAll(list, args);
        return this.redisApi.evalsha(list).compose(res -> res.toInteger().equals(1)
                ? Future.succeededFuture(true)
                : Future.succeededFuture(false));
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy