Spring Cloud & 以Gateway实现限流(自定义返回内容)

发布于:2025-05-13 ⋅ 阅读:(9) ⋅ 点赞:(0)

前言


    Spring Cloud Gateway自带RequestRateLimiterGatewayFilterFactory限流方案,可基于Redis和RedisRateLimiter实现默认算法为令牌桶的请求限流。作为自带的该限流方案,其可与Spring生态的其它各项组件无缝集成,并且自身实现也相对完善/好用,因此在没有特殊/复杂需求的情况下,该方案是实现基础限流的首选。
 
 

依赖


    在pom.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代替,但其存在依然是不可省略的。

/**
 * @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接口以创建免密授权的实际处理器。

/**
 * @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键解析器实例。

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进行连续快速地多次调用后返回以下内容,表示请求被限流:

![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/c1b593acd672465bb9cb74f1dd24bda1.png
    Redis数据结构如下所示。注意!由于限流数据键每秒都会更新/删除,所以想看到的话需要频繁请求接口以保持限流状态。
在这里插入图片描述
 
 

自定义回应


    Spring Cloud Gateway通过返回429的异常状态来表示限流异常/情况,但显然这种行为并无法兼容进异常的统一返回格式,因此此处会展示自定义限流异常回应信息的完整流程…这通过自定义限流过滤器的方式实现:
    创建WebRequestRateLimiterGatewayFilterFactory类并重写apply(Config config)方法以自定义限流回应的内容/格式。

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

    启动测试。
在这里插入图片描述
在这里插入图片描述