記一次Spring Gateway 限流遇到的問題

場景

公司產品迭代,架構升級了,從 vertx 換成了 spring Cloud 全家桶,最開始微服務網關用的是 ZUUL 後來換成了 spring官網的 springcloud getway 網關,在按照官網配置限流配置文件和代碼的時候,碰到點問題,把解決過程記錄一下

最開始限流配置文件是這樣寫的

 spring:
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true
          lower-case-service-id: true
      default-filters:
        # 添加限流機器,基於令牌桶算法,當服務超出限流約束,則直接拒決請求:RequestRateLimiter(默認),CustomRequestRateLimiter(自定義)
        - name: RequestRateLimiter
          args:
            redis-rate-limiter.replenishRate: 1 #允許用戶每秒處理多少個請求
            redis-rate-limiter.burstCapacity: 1 #令牌桶的容量,允許在一秒鐘內完成的最大請求數
            redis-rate-limiter.requestedTokens: 1
            key-resolver: "#{@ipKeyResolver}"
        - DedupeResponseHeader=Access-Control-Allow-Origin, RETAIN_UNIQUE

配置文件裏,按照官網的例子,配置成了 RequestRateLimiter 限流器

具體的java代碼大體如下

/**
 * @ClassName RequestRateLimiterConfig
 * @Deacription 限流
 * @Author xuzhou
 * @Date 2020/9/1 18:17
 * @Version 1.0
 **/
@Configuration
public class RequestRateLimiterConfig {

    @Bean("apiKeyResolver")
    @Primary
    KeyResolver apiKeyResolver() {
        //按URL限流,即以每秒內請求數按URL分組統計,超出限流的url請求都將返回429狀態
        return exchange -> Mono.just(exchange.getRequest().getPath().toString());
    }

    @Bean("userKeyResolver")
    KeyResolver userKeyResolver() {
        //按用戶限流
        return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("user"));
    }

    @Bean("ipKeyResolver")
    KeyResolver ipKeyResolver() {
        //按IP來限流
        return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getHostName());
    }
}


測試

配置好之後,啓動程序測試,限流配置和代碼生效了,但是限流之後,返回的結果不友好

使用Postman測試,限流之後,HTTP狀態碼變成了 429,但是返回體裏面沒有詳細信息 後端爲了統一處理錯誤,返回友好的提示給前端,程序都會處理,但是在官方提供的 RequestRateLimiterGatewayFilterFactory 過濾器裏面,是直接返回了,沒有任何回調接口提供程序做處理。

解決

開始想的比較簡單,自己 實現 GlobalFilter 全局過濾器裏面的方法,在 Response的時候做攔截,獲取到http 429 code,重新封裝返回值,但是實踐之後發現不起作用,翻閱資料發現:局部過濾器,優先於全局過濾,所以自己實現的攔截不起作用。 後來經過社區的網友提點,自己繼承了 AbstractGatewayFilterFactory 然後複製 RequestRateLimiterGatewayFilterFactory 的代碼,重寫 apply 方法,自己實現了一份限流過濾器,然後在裏面做代碼處理

 /*
 * Copyright 2013-2019 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import com.alibaba.fastjson.JSONObject;
import com.rayeye.springgetaway.util.RespObj;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.cloud.gateway.filter.ratelimit.RateLimiter;
import org.springframework.cloud.gateway.route.Route;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;

import java.util.Map;

/**
 * User Request Rate Limiter filter. See https://stripe.com/blog/rate-limiters and
 * 重寫 RequestRateLimiterGatewayFilterFactory
 */
@ConfigurationProperties("spring.cloud.gateway.filter.request-rate-limiter")
@Component("CustomRequestRateLimiter")
public class CustomRequestRateLimiterGatewayFilterFactory extends AbstractGatewayFilterFactory<CustomRequestRateLimiterGatewayFilterFactory.Config> {

    private static Logger logger = LoggerFactory.getLogger(CustomRequestRateLimiterGatewayFilterFactory.class);
    /**
     * Key-Resolver key.
     */
    public static final String KEY_RESOLVER_KEY = "keyResolver";

    private static final String EMPTY_KEY = "____EMPTY_KEY__";

    private final RateLimiter defaultRateLimiter;

    private final KeyResolver defaultKeyResolver;

    /**
     * Switch to deny requests if the Key Resolver returns an empty key, defaults to true.
     */
    private boolean denyEmptyKey = true;

    /**
     * HttpStatus to return when denyEmptyKey is true, defaults to FORBIDDEN.
     */
    private String emptyKeyStatusCode = HttpStatus.FORBIDDEN.name();

    public CustomRequestRateLimiterGatewayFilterFactory(RateLimiter defaultRateLimiter,
                                                        KeyResolver defaultKeyResolver) {
        super(Config.class);
        this.defaultRateLimiter = defaultRateLimiter;
        this.defaultKeyResolver = defaultKeyResolver;
    }

    public KeyResolver getDefaultKeyResolver() {
        return defaultKeyResolver;
    }

    public RateLimiter getDefaultRateLimiter() {
        return defaultRateLimiter;
    }

    public boolean isDenyEmptyKey() {
        return denyEmptyKey;
    }

    public void setDenyEmptyKey(boolean denyEmptyKey) {
        this.denyEmptyKey = denyEmptyKey;
    }

    public String getEmptyKeyStatusCode() {
        return emptyKeyStatusCode;
    }

    public void setEmptyKeyStatusCode(String emptyKeyStatusCode) {
        this.emptyKeyStatusCode = emptyKeyStatusCode;
    }

    @SuppressWarnings("unchecked")
    @Override
    public GatewayFilter apply(Config config) {
        KeyResolver resolver = (config.keyResolver == null) ? defaultKeyResolver : config.keyResolver;
        RateLimiter<Object> limiter = (config.rateLimiter == null) ? defaultRateLimiter : config.rateLimiter;
        return (exchange, chain) -> {
            Route route = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
            return resolver.resolve(exchange).flatMap(key ->
                    limiter.isAllowed(route.getId(), key).flatMap(response -> {
                        for (Map.Entry<String, String> header : response.getHeaders().entrySet()) {
                            exchange.getResponse().getHeaders().add(header.getKey(), header.getValue());
                        }
                        if (response.isAllowed()) {
                            return chain.filter(exchange);
                        }

                        ServerHttpResponse httpResponse = exchange.getResponse();
                        httpResponse.setStatusCode(HttpStatus.OK);
                        logger.error("訪問已限流,請稍候再請求");
                        RespObj respObj = RespObj.fail("訪問已限流,請稍候再請求", HttpStatus.INTERNAL_SERVER_ERROR.value());
                        DataBuffer buffer = httpResponse.bufferFactory().wrap(JSONObject.toJSONBytes(respObj));
                        return httpResponse.writeWith(Mono.just(buffer));
                    }));
        };
    }

    private <T> T getOrDefault(T configValue, T defaultValue) {
        return (configValue != null) ? configValue : defaultValue;
    }

    public static class Config {

        private KeyResolver keyResolver;

        private RateLimiter rateLimiter;

        private HttpStatus statusCode = HttpStatus.TOO_MANY_REQUESTS;

        private Boolean denyEmptyKey;

        private String emptyKeyStatus;

        public KeyResolver getKeyResolver() {
            return keyResolver;
        }

        public Config setKeyResolver(KeyResolver keyResolver) {
            this.keyResolver = keyResolver;
            return this;
        }

        public RateLimiter getRateLimiter() {
            return rateLimiter;
        }

        public Config setRateLimiter(RateLimiter rateLimiter) {
            this.rateLimiter = rateLimiter;
            return this;
        }

        public HttpStatus getStatusCode() {
            return statusCode;
        }

        public Config setStatusCode(HttpStatus statusCode) {
            this.statusCode = statusCode;
            return this;
        }

        public Boolean getDenyEmptyKey() {
            return denyEmptyKey;
        }

        public Config setDenyEmptyKey(Boolean denyEmptyKey) {
            this.denyEmptyKey = denyEmptyKey;
            return this;
        }

        public String getEmptyKeyStatus() {
            return emptyKeyStatus;
        }

        public Config setEmptyKeyStatus(String emptyKeyStatus) {
            this.emptyKeyStatus = emptyKeyStatus;
            return this;
        }

    }

}


重寫配置文件

自定義 CustomRequestRateLimiter 攔截器

spring:
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true
          lower-case-service-id: true
      default-filters:
        # 添加限流機器,基於令牌桶算法,當服務超出限流約束,則直接拒決請求:RequestRateLimiter(默認),CustomRequestRateLimiter(自定義)
        - name: CustomRequestRateLimiter
          args:
            redis-rate-limiter.replenishRate: 1 #允許用戶每秒處理多少個請求
            redis-rate-limiter.burstCapacity: 1 #令牌桶的容量,允許在一秒鐘內完成的最大請求數
            redis-rate-limiter.requestedTokens: 1
            key-resolver: "#{@ipKeyResolver}"
        - DedupeResponseHeader=Access-Control-Allow-Origin, RETAIN_UNIQUE

最終運行結果如下

可以看到,限流起作用了,並且返回值也是自定義的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章