前言
Spring Cloud Gateway自带RequestRateLimiterGatewayFilterFactory限流方案,可基于Redis和RedisRateLimiter实现默认算法为令牌桶的请求限流。作为自带的该限流方案,其可与Spring生态的其它各项组件无缝集成,并且自身实现也相对完善/好用,因此在没有特殊/复杂需求的情况下,该方案是实现基础限流的首选。
依赖
在pom.xml文件中添加以下依赖。
xml
<!-- Spring Boot Redis响应式起步依赖:用于实现请求限流 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
<!-- Spring Cloud网关起步依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
继承&改造UsernamePasswordAuthenticationToken类以创建免密授权的鉴权类,该类会置null密码以逃脱密码校验。此外该类还可限定免密授权的处理器不会作用在其它授权模式上,因此其功能虽然完全可以使用UsernamePasswordAuthenticationToken代替,但其存在依然是不可省略的。
java
/**
* @Author: 说淑人
* @Date: 2025/5/8 21:45
* @Description: 免密鉴权令牌类
*/
public static class PasswordLessAuthenticationToken extends UsernamePasswordAuthenticationToken {
private static final long serialVersionUID = -2798549339574220892L;
public PasswordLessAuthenticationToken(Object principal) {
// ---- credentials即为密码,为null表示不进行校验。
super(principal, null);
setAuthenticated(false);
}
public PasswordLessAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
super(principal, null, authorities);
}
}
实现AuthenticationProvider接口以创建免密授权的实际处理器。
java
/**
* @Author: 说淑人
* @Date: 2025/5/8 19:38
* @Description: 免密鉴权供应者类
*/
@Component
public class PasswordLessAuthenticationProvider implements AuthenticationProvider {
private final UserDetailsService userDetailsService;
public PasswordLessAuthenticationProvider(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// ---- 获取用户名,并调用我们实现的userDetailsService.loadUserByUsername(username)
String username = (String) authentication.getPrincipal();
UserDetails user = userDetailsService.loadUserByUsername(username);
// ---- 如果用户不存在,按框架逻辑抛出原样异常以统一格式。
if (user == null) {
throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
}
return new PasswordLessAuthenticationToken(user, user.getAuthorities());
}
@Override
public boolean supports(Class<?> authentication) {
// ---- 该方法用于限定PasswordLessAuthenticationProvider只对
// PasswordLessAuthenticationToken生效,这个可以避免
// PasswordLessAuthenticationProvider作用于其它授权模式,而这会导致混乱。
return PasswordLessAuthenticationToken.class.isAssignableFrom(authentication);
}
}
配置
在application.yml文件中添加以下Redis与Gateway配置。
# ---- Spring Config
spring:
# ---- Redis Config
redis:
host: 127.0.0.1
port: 6379
# ---- Gateway Config
gateway:
routes:
# ---- 转发的服务。
- id: world-biz-manage
uri: lb://world-biz-manage
predicates:
- Path=/api/manage/**
# ---- 添加RequestRateLimiter过滤器实现限流。因为过滤器是注册在具体服
# 务下的,因此也只会对当前服务进行限流。
filters:
- name: RequestRateLimiter
args:
# ---- 键解析器:定义限流数据键的生成规则,常用的生成规则有基于用
# 户/IP/接口,而这里使用IP进行区分,即各IP限流数据是独立的。
key-resolver: "#{@ipKeyResolver}"
# ---- 每秒生成15个令牌,因此在令牌桶无令牌的情况下,一个IP每秒
# 最多请求15次。
redis-rate-limiter.replenishRate: 15
# ---- 令牌桶中最多保存30个令牌,可支持单个IP最多30个请求/秒的流
# 量高发。
redis-rate-limiter.burstCapacity: 30
# ---- 一次请求消耗一个令牌。
redis-rate-limiter.requestedTokens: 1
创建WebConfig(名称自定)类,用于内部创建/注册IP键解析器实例。
java
package com.ssr.world.frame.gateway.tool.config.web;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import reactor.core.publisher.Mono;
/**
* @Author: 说淑人
* @Date: 2025/4/22 22:39
* @Description: 网络配置类
*/
@Configuration
public class WebConfig {
/**
* IP键解析器
*
* @return IP键解析器
*/
@Bean
// ---- 你没看错,没有public。
KeyResolver ipKeyResolver() {
// ---- 以IP地址作为限流键的区分标志,即每个IP都有自己独立的令牌桶。
return exchange -> Mono.just(
// ---- 此处也可以获取其它参数作为限流键的区分表示,例如:
// 头信息中携带的用户ID
// 请求的接口
// ---- 将上述参数混合使用也是不错的做法。
exchange.getRequest().getRemoteAddress().getAddress().getHostAddress()
);
}
}
启动&测试
# ---- 为了方便测试,我们暂时修改令牌的生成/存储上限为1/1。
redis-rate-limiter.replenishRate: 1
redis-rate-limiter.burstCapacity: 1
使用Postman进行连续快速地多次调用后返回以下内容,表示请求被限流:

Redis数据结构如下所示。注意!由于限流数据键每秒都会更新/删除,所以想看到的话需要频繁请求接口以保持限流状态。
自定义回应
Spring Cloud Gateway通过返回429的异常状态来表示限流异常/情况,但显然这种行为并无法兼容进异常的统一返回格式,因此此处会展示自定义限流异常回应信息的完整流程...这通过自定义限流过滤器的方式实现:
创建WebRequestRateLimiterGatewayFilterFactory类并重写apply(Config config)方法以自定义限流回应的内容/格式。
java
import com.alibaba.fastjson2.JSONObject;
import com.ssr.world.tool.toft.model.bo.result.ResultBox;
import com.ssr.world.tool.toft.model.eo.web.WebResultEnum;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.RequestRateLimiterGatewayFilterFactory;
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.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
@Component
public class WebRequestRateLimiterGatewayFilterFactory extends RequestRateLimiterGatewayFilterFactory {
// ---- defaultKeyResolver会因为多实例而出现无法注入的情况,可通过
// 在ipKeyResolver()方法上添加@Primary注解的方式处理。
public WebRequestRateLimiterGatewayFilterFactory(RateLimiter defaultRateLimiter, KeyResolver defaultKeyResolver) {
super(defaultRateLimiter, defaultKeyResolver);
}
@Override
public GatewayFilter apply(Config config) {
// ---- 获取健解析器/速率限制器。
KeyResolver resolver = config.getKeyResolver() != null ? config.getKeyResolver() : getDefaultKeyResolver();
RateLimiter<Object> limiter = config.getRateLimiter() != null ? config.getRateLimiter() : getDefaultRateLimiter();
return (exchange, chain) -> resolver.resolve(exchange).flatMap(key -> {
String routeId = config.getRouteId();
if (routeId == null) {
Route route = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
assert route != null;
routeId = route.getId();
}
return limiter.isAllowed(routeId, 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);
httpResponse.getHeaders().setContentType(MediaType.APPLICATION_JSON_UTF8);
httpResponse.getHeaders().setContentLength(REQUEST_TOO_QUICKLY_BYTES.length);
// ---- 重设回应体。
Map<String, Object> map = new HashMap<>();
map.put("code", "自定义错误码");
map.put("message", "请求频率过高,请稍后重试...");
map.put("data", "自定义数据");
map.put("success", false);
// ---- 回应内容是固定的,因此可以bytes直接在static中预加载,这能一定程度提升性能。
byte[] bytes = JSONObject.toJSONString(map).getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = httpResponse.bufferFactory().wrap(bytes);
return httpResponse.writeWith(Mono.just(buffer));
});
});
}
}
修改application.yml文件中指定的限流过滤器为自定义的限流过滤器。
filters:
# ---- 将原本的RequestRateLimiter改为自定义的WebRequestRateLimiter,即略去名称尾部的GatewayFilterFactory部分。
- name: WebRequestRateLimiter
args:
key-resolver: "#{@ipKeyResolver}"
redis-rate-limiter.replenishRate: 1
redis-rate-limiter.burstCapacity: 1
redis-rate-limiter.requestedTokens: 1
启动测试。