場景
公司產品迭代,架構升級了,從 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
最終運行結果如下
可以看到,限流起作用了,並且返回值也是自定義的。