场景
公司产品迭代,架构升级了,从 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
最终运行结果如下
可以看到,限流起作用了,并且返回值也是自定义的。