【问题标题】:Rate limiting based on user plan in Spring Cloud GatewaySpring Cloud Gateway 中基于用户计划的速率限制
【发布时间】:2018-12-01 22:34:52
【问题描述】:

假设我的用户订阅了一个计划。那么是否可以使用 Spring Cloud Gateway 根据订阅计划对用户请求进行速率限制?鉴于有 Silver 和 Gold 计划,是否允许 Silver 订阅具有 5/10 和 Gold 50/100 的补充率/burstCapacity?

我天真地想过将 RedisRateLimiter 的新实例(见下文,我用 5/10 设置构造一个新实例)传递给过滤器,但我需要以某种方式从请求中获取有关用户的信息,以便能够了解它是否是白银计划和黄金计划。

@Bean
public RouteLocator myRoutes(RouteLocatorBuilder builder) {
    return builder.routes()
        .route(p -> p
            .path("/get")
            .filters(f ->
              f.requestRateLimiter(r -> {
                  r.setRateLimiter(new RedisRateLimiter(5, 10))
              })
            .uri("http://httpbin.org:80"))
            .build();
}

我是否正在尝试通过 Spring Cloud Gateway 实现甚至可能实现的目标?如果有的话,你会推荐哪些其他产品来检查这个目的?

谢谢!

【问题讨论】:

    标签: rate-limiting spring-cloud-gateway


    【解决方案1】:

    好的,可以通过在RedisRateLimiter 类之上创建自定义速率限制器。不幸的是,该类尚未针对可扩展性进行架构设计,因此该解决方案有点“hacky”,我只能装饰普通的RedisRateLimiter 并在其中复制它的一些代码:

    @Primary
    @Component
    public class ApiKeyRateLimiter implements RateLimiter {
    
        private Log log = LogFactory.getLog(getClass());
    
        // How many requests per second do you want a user to be allowed to do?
        private static final int REPLENISH_RATE = 1;
        // How much bursting do you want to allow?
        private static final int BURST_CAPACITY = 1;
    
        private final RedisRateLimiter rateLimiter;
        private final RedisScript<List<Long>> script;
        private final ReactiveRedisTemplate<String, String> redisTemplate;
    
        @Autowired
        public ApiKeyRateLimiter(
            RedisRateLimiter rateLimiter,
            @Qualifier(RedisRateLimiter.REDIS_SCRIPT_NAME) RedisScript<List<Long>> script,
            ReactiveRedisTemplate<String, String> redisTemplate) {
    
            this.rateLimiter = rateLimiter;
            this.script = script;
            this.redisTemplate = redisTemplate;
        }
    
        // These two methods are the core of the rate limiter
        // Their purpose is to come up with a rate limits for given API KEY (or user ID)
        // It is up to implementor to return limits based up on the api key passed
        private int getBurstCapacity(String routeId, String apiKey) {
            return BURST_CAPACITY;
        }
        private int getReplenishRate(String routeId, String apiKey) {
            return REPLENISH_RATE;
        }
    
        public Mono<Response> isAllowed(String routeId, String apiKey) {
    
            int replenishRate = getReplenishRate(routeId, apiKey);
            int burstCapacity = getBurstCapacity(routeId, apiKey);
    
            try {
                List<String> keys = getKeys(apiKey);
    
                // The arguments to the LUA script. time() returns unixtime in seconds.
                List<String> scriptArgs = Arrays.asList(replenishRate + "", burstCapacity + "",
                    Instant.now().getEpochSecond() + "", "1");
                Flux<List<Long>> flux = this.redisTemplate.execute(this.script, keys, scriptArgs);
    
                return flux.onErrorResume(throwable -> Flux.just(Arrays.asList(1L, -1L)))
                    .reduce(new ArrayList<Long>(), (longs, l) -> {
                        longs.addAll(l);
                        return longs;
                    }) .map(results -> {
                        boolean allowed = results.get(0) == 1L;
                        Long tokensLeft = results.get(1);
    
                        Response response = new Response(allowed, getHeaders(tokensLeft, replenishRate, burstCapacity));
    
                        if (log.isDebugEnabled()) {
                            log.debug("response: " + response);
                        }
                        return response;
                    });
            }
            catch (Exception e) {
                /*
                 * We don't want a hard dependency on Redis to allow traffic. Make sure to set
                 * an alert so you know if this is happening too much. Stripe's observed
                 * failure rate is 0.01%.
                 */
                log.error("Error determining if user allowed from redis", e);
            }
            return Mono.just(new Response(true, getHeaders(-1L, replenishRate, burstCapacity)));
        }
    
        private static List<String> getKeys(String id) {
            String prefix = "request_rate_limiter.{" + id;
            String tokenKey = prefix + "}.tokens";
            String timestampKey = prefix + "}.timestamp";
            return Arrays.asList(tokenKey, timestampKey);
        }
    
        private HashMap<String, String> getHeaders(Long tokensLeft, Long replenish, Long burst) {
            HashMap<String, String> headers = new HashMap<>();
            headers.put(RedisRateLimiter.REMAINING_HEADER, tokensLeft.toString());
            headers.put(RedisRateLimiter.REPLENISH_RATE_HEADER, replenish.toString());
            headers.put(RedisRateLimiter.BURST_CAPACITY_HEADER, burst.toString());
            return headers;
        }
    
        @Override
        public Map getConfig() {
            return rateLimiter.getConfig();
        }
    
        @Override
        public Class getConfigClass() {
            return rateLimiter.getConfigClass();
        }
    
        @Override
        public Object newConfig() {
            return rateLimiter.newConfig();
        }
    }
    

    所以,路线应该是这样的:

    @Component
    public class Routes {
    
        @Autowired
        ApiKeyRateLimiter rateLimiter;
    
        @Autowired
        ApiKeyResolver apiKeyResolver;
    
        @Bean
        public RouteLocator theRoutes(RouteLocatorBuilder b) {
            return b.routes()
                .route(p -> p
                        .path("/unlimited")
                        .uri("http://httpbin.org:80/anything?route=unlimited")
                )
                .route(p -> p
                        .path("/limited")
                        .filters(f ->
                                f.requestRateLimiter(r -> {
                                    r.setKeyResolver(apiKeyResolver);
                                    r.setRateLimiter(rateLimiter);
                                } )
                        )
                        .uri("http://httpbin.org:80/anything?route=limited")
                )
                .build();
        }
    
    }
    

    希望它能为某人节省一个工作日......

    【讨论】:

    • 出现错误,r.setKeyResolver() 需要 KeyResolver 实现
    猜你喜欢
    • 1970-01-01
    • 2019-08-16
    • 1970-01-01
    • 2020-11-12
    • 2021-08-03
    • 1970-01-01
    • 2018-10-10
    • 2015-06-21
    • 2019-07-14
    相关资源
    最近更新 更多