【基于spring-cloud-gateway实现本身的网关过滤器】

[复制链接]
发表于 2024-10-2 06:01:58 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×
基于spring-cloud-gateway实现本身的网关过滤器

spring cloud gateway custom starter

自定义非阻塞式反应网关服务,集成鉴权、限流、响应的增强处置惩罚等等



  • 环境要求
    1. <properties>
    2.       <java.version>17</java.version>
    3.       <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    4.       <maven.compiler.target>17</maven.compiler.target>
    5.       <maven.compiler.source>17</maven.compiler.source>
    6.       <java.source.version>17</java.source.version>
    7.       <java.target.version>17</java.target.version>
    8.       <spring-boot.version>3.1.12</spring-boot.version>
    9.       <spring-cloud.version>2022.0.5</spring-cloud.version>
    10.       <commons.pool2.version>2.12.0</commons.pool2.version>
    11.       <redisson.version>3.34.1</redisson.version>
    12.       <com.fastjson.jackson.version>2.17.2</com.fastjson.jackson.version>
    13.       <commons.lang3.version>3.16.0</commons.lang3.version>
    14.       <lombok.version>1.18.32</lombok.version>
    15.   </properties>
    复制代码
  • GatewayFilter 路由过滤器

    • TokenFilterGatewayFilterFactory,鉴权处置惩罚
    • 设置示例:
    1. spring:
    2.   cloud:
    3.     gateway:
    4.       routes:
    5.         - id: testRoute
    6.           uri: http://127.0.0.1:8080
    7.           predicates:
    8.             - Path=/test/**
    9.           filters:
    10.             - name: TokenFilter
    11.               args:
    12.                 requestHeaderKey: auth
    复制代码
      

    • RlimterGatewayFilterFactory,自定义key,比如:我们想根据不同用户去做自定义限流,那么我们可以在本身的网关过滤工厂内里将limterKey设置为根据哀求头自定义的用户标识,来举行自定义的设置。相比重写spring-cloud-gateway内里自定义的keyResolver和RedisLimiter相对容易一些。

      • 设置示例:
      1. spring:
      2.     cloud:
      3.         gateway:
      4.           routes:
      5.             - id: route_test
      6.               uri: http://192.168.1.1:8091
      7.               predicates:
      8.                 - Path=/from/requestIds/to/appNames
      9.               filters:
      10.                 - name: RLimter
      11.                   args:
      12.                     limterKey: v1_10 #限流所需要的key
      13.                     rate: 5  #每秒允许的请求数
      14.                     crust: 0 #每秒令牌桶的填充数
      复制代码
         

      • 更多路由过滤器扩展中。。。

    自定义网关过滤工厂实现,TokenFilterGatewayFilterFactory和RlimterGatewayFilterFactory,以自定义限流器RlimterGatewayFilterFactory为例




  • 起首定义网关过滤工厂功能接口,限流的key,速率和桶的大小,我们是按照spring-cloud-gateway内部限流的实现改编而来的,通过调用lua脚本,采用redis的令牌桶算法做限流
  1. public interface RLimter {
  2.     Mono<Response> isAllowed(String limitKey, String rate, String crust);
  3.     @Setter
  4.     @Getter
  5.     @NoArgsConstructor
  6.     @AllArgsConstructor
  7.     class Response {
  8.         private boolean allowed;
  9.     }
  10. }
复制代码


  • 限流功能组件的注入和声明,注入我们必要的Bean,注入RedisScript,调用自定义的lua脚本,以及StringRedisTemplate,因为下面这段代码我是将整个网关作为的启动器,所以限流的实现类也一并交给spring管理了,RRedisRateLimiter
  1. @Configuration
  2. public class RLimterAutoConfiguration {
  3.     @Bean(name = "rredisRequestRateLimiterScript")
  4.     public RedisScript<?> redisRequestRateLimiterScript() {
  5.         DefaultRedisScript redisScript = new DefaultRedisScript();
  6.         redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("script/request_rate_limiter.lua")));
  7.         redisScript.setResultType(List.class);
  8.         return redisScript;
  9.     }
  10.     @Bean(name = "rredisRateLimiter")
  11.     public RRedisRateLimiter redisRateLimiter(@Qualifier("rredisRequestRateLimiterScript") RedisScript<List<Long>> redisRequestRateLimiterScript, StringRedisTemplate redisTemplate) {
  12.         return new RRedisRateLimiter(redisTemplate, redisRequestRateLimiterScript);
  13.     }
  14.     @Bean
  15.     public RLimterGatewayFilterFactory rLimterGatewayFilterFactory(@Qualifier("rredisRateLimiter") RRedisRateLimiter redisRateLimiter) {
  16.         return new RLimterGatewayFilterFactory(redisRateLimiter);
  17.     }
  18. }
复制代码


  • 上面代码中提到的new ClassPathResource(“script/request_rate_limiter.lua”),工程resources目录下即可,也是摘抄自spring-cloud-gateway内部的限流脚本,原封不动
  1. redis.replicate_commands()
  2. local tokens_key = KEYS[1]
  3. local timestamp_key = KEYS[2]
  4. -- redis.log(redis.LOG_WARNING, "tokens_key " .. tokens_key)
  5. local rate = tonumber(ARGV[1])
  6. local capacity = tonumber(ARGV[2])
  7. local now = tonumber(ARGV[3])
  8. local requested = tonumber(ARGV[4])
  9. local fill_time = capacity / rate
  10. local ttl = math.floor(fill_time * 2)
  11. -- for testing, it should use redis system time in production
  12. if now == nil then
  13.   now = redis.call('TIME')[1]
  14. end
  15. --redis.log(redis.LOG_WARNING, "rate " .. ARGV[1])
  16. --redis.log(redis.LOG_WARNING, "capacity " .. ARGV[2])
  17. --redis.log(redis.LOG_WARNING, "now " .. now)
  18. --redis.log(redis.LOG_WARNING, "requested " .. ARGV[4])
  19. --redis.log(redis.LOG_WARNING, "filltime " .. fill_time)
  20. --redis.log(redis.LOG_WARNING, "ttl " .. ttl)
  21. local last_tokens = tonumber(redis.call("get", tokens_key))
  22. if last_tokens == nil then
  23.   last_tokens = capacity
  24. end
  25. --redis.log(redis.LOG_WARNING, "last_tokens " .. last_tokens)
  26. local last_refreshed = tonumber(redis.call("get", timestamp_key))
  27. if last_refreshed == nil then
  28.   last_refreshed = 0
  29. end
  30. --redis.log(redis.LOG_WARNING, "last_refreshed " .. last_refreshed)
  31. local delta = math.max(0, now-last_refreshed)
  32. local filled_tokens = math.min(capacity, last_tokens+(delta*rate))
  33. local allowed = filled_tokens >= requested
  34. local new_tokens = filled_tokens
  35. local allowed_num = 0
  36. if allowed then
  37.   new_tokens = filled_tokens - requested
  38.   allowed_num = 1
  39. end
  40. --redis.log(redis.LOG_WARNING, "delta " .. delta)
  41. --redis.log(redis.LOG_WARNING, "filled_tokens " .. filled_tokens)
  42. --redis.log(redis.LOG_WARNING, "allowed_num " .. allowed_num)
  43. --redis.log(redis.LOG_WARNING, "new_tokens " .. new_tokens)
  44. if ttl > 0 then
  45.   redis.call("setex", tokens_key, ttl, new_tokens)
  46.   redis.call("setex", timestamp_key, ttl, now)
  47. end
  48. -- return { allowed_num, new_tokens, capacity, filled_tokens, requested, new_tokens }
  49. return { allowed_num, new_tokens }
复制代码


  • 然后是限流实现类,通过传入上面lua脚本必要的四个参数即可,lua脚本中requested为每次从桶内里取出的令牌数,这个默认为1,此处不关注这个,默认值即可。
  1. public class RRedisRateLimiter implements RLimter {
  2.     private static final Logger logger = LoggerFactory.getLogger(RedisRateLimiter.class);
  3.     private final StringRedisTemplate redisTemplate;
  4.     private final RedisScript<List<Long>> script;
  5.     public RRedisRateLimiter(StringRedisTemplate redisTemplate, RedisScript<List<Long>> script) {
  6.         this.redisTemplate = redisTemplate;
  7.         this.script = script;
  8.     }
  9.     static List<String> getKeys(String id) {
  10.         String prefix = "request_rate_limiter.{" + id;
  11.         String tokenKey = prefix + "}.tokens";
  12.         String timestampKey = prefix + "}.timestamp";
  13.         return Arrays.asList(tokenKey, timestampKey);
  14.     }
  15.     @Override
  16.     public Mono<Response> isAllowed(String key, String rate, String crust) {
  17.         List<String> keys = getKeys(key);
  18.         List<Long> exec;
  19.         try {
  20.             exec = this.redisTemplate.execute(this.script, keys, rate, crust, "", "1");
  21.         } catch (Throwable throwable) {
  22.             logger.error("Error calling rate limiter lua", throwable);
  23.             exec = Arrays.asList(1L, -1L);
  24.         }
  25.         assert exec != null;
  26.         boolean allowed = exec.get(0) == 1L;
  27.         return Mono.just(new Response(allowed));
  28.     }
  29. }
复制代码


  • 最后就是定义我们本身的自定义限流网关工厂了,通过继承spring-cloud-gateway的一个父类,资助我们加载自定义的网关过滤工厂,AbstractGatewayFilterFactory,父类支持传入我们的自定义设置参数,Config,通过泛型参数定义本身的设置类,并在构造中传入。
  1. public class RLimterGatewayFilterFactory extends AbstractGatewayFilterFactory<RLimterGatewayFilterFactory.Config2> {
  2.     private final RRedisRateLimiter redisRateLimiter;
  3.     public RLimterGatewayFilterFactory(RRedisRateLimiter redisRateLimiter) {
  4.         super(Config2.class);
  5.         this.redisRateLimiter = redisRateLimiter;
  6.     }
  7.     @Override
  8.     public GatewayFilter apply(Config2 config) {
  9.         return (exchange, chain) -> redisRateLimiter.isAllowed(config.limterKey, config.rate, config.crust).flatMap(response -> {
  10.             if (response.isAllowed()) {
  11.                 return chain.filter(exchange);
  12.             } else {
  13.                 ServerWebExchangeUtils.setResponseStatus(exchange, config.getStatusCode());
  14.                 return exchange.getResponse().setComplete();
  15.             }
  16.         });
  17.     }
  18.     @Setter
  19.     @Getter
  20.     public static class Config2 implements HasRouteId {
  21.         private String routeId;
  22.         private String limterKey = "default";
  23.         private String rate = "1";
  24.         private String crust = "1";
  25.         private HttpStatus statusCode = HttpStatus.TOO_MANY_REQUESTS;
  26.         @Override
  27.         public String getRouteId() {
  28.             return routeId;
  29.         }
  30.         @Override
  31.         public void setRouteId(String routeId) {
  32.             this.routeId = routeId;
  33.         }
  34.     }
  35. }
复制代码


  • 这里我只是写了个简单的实例,假如实现自定义的限流,还必要在网关工厂内里,通过exchange拿到哀求体来举行相应的key的剖析,连合业务实现自定义的限流。设置可以参考一开始的yaml设置示例。可以测试当把令牌桶设置为0时,会给出TOO MANY REQUEST的429状态码。
全局过滤器案例



  • 起首定义本身的上下文对象
  1. @Setter
  2. @Getter
  3. public class RequestContext {
  4.     private String requestId;
  5.     private long requestStartTime;
  6.     private String requestIp;
  7. }
复制代码


  • 全局过滤器实现
  1. @Component
  2. public class RequestContextFilter implements WebFilter, Ordered {
  3.     private static final Logger logger = LoggerFactory.getLogger(RequestContextFilter.class);
  4.     @Override
  5.     public int getOrder() {
  6.         return OrderConstant.REQUEST_CONTEXT_ORDER;
  7.     }
  8.     @Override
  9.     public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
  10.         long requestStartTime = SystemClock.now();
  11.         String requestId = generateRequestId();
  12.         ServerHttpRequest request = exchange.getRequest();
  13.         String uri = request.getURI().getRawPath();
  14.         String requestIp = IpUtils.tryGetRealIp(request);
  15.         exchange.getResponse().getHeaders().add("requestId", requestId);
  16.         logger.info("request start,requestId:{}, requestUri:{},ip:{},requestStartTime:{}", requestId, uri, requestIp, requestStartTime);
  17.         RequestContext context = new RequestContext();
  18.         context.setRequestId(requestId);
  19.         context.setRequestStartTime(requestStartTime);
  20.         context.setRequestIp(requestIp);
  21.         return chain.filter(exchange).contextWrite(Context.of(RequestContext.class, context))
  22.                 .doOnEach(signal -> {
  23.                     long requestEndTime = SystemClock.now();
  24.                     if (signal.isOnComplete()) {
  25.                         logger.info("request end,requestId:{},response:{},requestEndTime:{},耗时ms:{}", requestId, exchange.getResponse().getStatusCode(), requestEndTime, (requestEndTime - requestStartTime));
  26.                     }
  27.                     if (signal.isOnError()) {
  28.                         logger.info("request end,requestId:{},error:{},requestEndTime:{},耗时ms:{}", requestId, signal.getThrowable(), requestEndTime, (requestEndTime - requestStartTime));
  29.                     }
  30.                 });
  31.     }
  32.     private String generateRequestId() {
  33.         return UUID.randomUUID().toString().replaceAll("-", "");
  34.     }
  35. }
复制代码
回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表