26、Spring Cloud Gateway限流

第一步:pom配置文件

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0http://maven.apache.org/xsd/maven-4.0.0.xsd">

 

  <modelVersion>4.0.0modelVersion>

  <artifactId>wx_takeout_gatewayartifactId>

  <name>网关服务name>

  <description>提供路由、限流、安全策略、单点认证等功能description>

 

 

  <parent>

    <groupId>com.easystudygroupId>

    <artifactId>wx_takeout_cloudartifactId>

    <version>0.0.1-SNAPSHOTversion>

  parent>

 

 

  <dependencies>

     

     <dependency>

          <groupId>com.easystudygroupId>

          <artifactId>wx_takeout_commonartifactId>

          <version>0.0.1-SNAPSHOTversion>

     dependency>

     

     <dependency>

          <groupId>org.springframework.bootgroupId>

          <artifactId>spring-boot-starter-webartifactId>

          <exclusions>

             <exclusion>

                 <groupId>*groupId>

                 <artifactId>*artifactId>

             exclusion>

         exclusions>

     dependency>

     

     <dependency>

          <groupId>org.springframework.cloudgroupId>

          <artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>

     dependency>

   

    <dependency>

        <groupId>org.springframework.cloudgroupId>

        <artifactId>spring-cloud-starter-gatewayartifactId>

    dependency>

   

    <dependency>

         <groupId>org.springframework.bootgroupId>

         <artifactId>spring-boot-starter-data-redis-reactiveartifactId>

     dependency>

     

     <dependency>

         <groupId>org.springframework.cloudgroupId>

         <artifactId>spring-cloud-starter-configartifactId>

     dependency>

   dependencies>

project>

 

第二步:自定义限流器

package com.easystudy.limiter;



import java.time.Instant;

import java.util.ArrayList;

import java.util.Arrays;

import java.util.HashMap;

import java.util.List;

import java.util.concurrent.atomic.AtomicBoolean;



import javax.validation.constraints.Min;



import org.apache.commons.logging.Log;

import org.apache.commons.logging.LogFactory;

import org.springframework.beans.BeansException;

import org.springframework.cloud.gateway.filter.ratelimit.AbstractRateLimiter;

import org.springframework.context.ApplicationContext;

import org.springframework.context.ApplicationContextAware;

import org.springframework.context.annotation.Primary;

import org.springframework.data.redis.core.ReactiveRedisTemplate;

import org.springframework.data.redis.core.script.RedisScript;

import org.springframework.stereotype.Component;

import org.springframework.validation.Validator;

import org.springframework.validation.annotation.Validated;



import reactor.core.publisher.Flux;

import reactor.core.publisher.Mono;



/**

* 自定义限流器

* @author Administrator

*

*/

@Component

@Primary

public class RateCheckRedisRateLimiter extends AbstractRateLimiter

        implements ApplicationContextAware {



    public static final String CONFIGURATION_PROPERTY_NAME = "redis-rate-limiter";

    public static final String REDIS_SCRIPT_NAME = "redisRequestRateLimiterScript";

    

    public static final String REMAINING_HEADER = "X-RateLimit-Remaining";

    public static final String REPLENISH_RATE_HEADER = "X-RateLimit-Replenish-Rate";

    public static final String BURST_CAPACITY_HEADER = "X-RateLimit-Burst-Capacity";

    

    private Log log = LogFactory.getLog(getClass());

    private ReactiveRedisTemplate redisTemplate;

    // Lua搅拌额

    private RedisScript> script;

    // 初始化标识

    private AtomicBoolean initialized = new AtomicBoolean(false);

    // 系统默认配置

    private Config defaultConfig;

    

    private boolean includeHeaders = true;

    private String remainingHeader = REMAINING_HEADER;

    private String replenishRateHeader = REPLENISH_RATE_HEADER;

    private String burstCapacityHeader = BURST_CAPACITY_HEADER;



    public RateCheckRedisRateLimiter() {

        super(Config.class, CONFIGURATION_PROPERTY_NAME, null);

    }

    

    public RateCheckRedisRateLimiter(ReactiveRedisTemplate redisTemplate, RedisScript> script, Validator validator) {

        super(Config.class, CONFIGURATION_PROPERTY_NAME, validator);

        this.redisTemplate = redisTemplate;

        this.script = script;

initialized.compareAndSet(false, true);

    }



    public RateCheckRedisRateLimiter(int defaultReplenishRate, int defaultBurstCapacity) {

        super(Config.class, CONFIGURATION_PROPERTY_NAME, null);

        this.defaultConfig = new Config().setReplenishRate(defaultReplenishRate).setBurstCapacity(defaultBurstCapacity);

    }



    public boolean isIncludeHeaders() {

        return includeHeaders;

    }



    public void setIncludeHeaders(boolean includeHeaders) {

        this.includeHeaders = includeHeaders;

    }



    public String getRemainingHeader() {

        return remainingHeader;

    }



    public void setRemainingHeader(String remainingHeader) {

        this.remainingHeader = remainingHeader;

    }



    public String getReplenishRateHeader() {

        return replenishRateHeader;

    }



    public void setReplenishRateHeader(String replenishRateHeader) {

        this.replenishRateHeader = replenishRateHeader;

    }



    public String getBurstCapacityHeader() {

        return burstCapacityHeader;

    }



    public void setBurstCapacityHeader(String burstCapacityHeader) {

        this.burstCapacityHeader = burstCapacityHeader;

    }



    /**

     * 根据key(接口)找到对应的限流配置

     * @param key

     * @return

     */

    @SuppressWarnings("unused")

    private Config setConfig(String key) {

        // 令牌桶的容积,希望允许用户每秒处理多少个请求,而不会丢失任何请求

        int replenishRate = 100;

        // 用户允许在一秒钟内完成的最大请求数,这是令牌桶可以容纳的令牌的数量,将此值设置为零将阻止所有请求

        int burstCapacity = 10;



        defaultConfig = new Config()

                .setReplenishRate(replenishRate)

                .setBurstCapacity(burstCapacity);

        return defaultConfig;

    }

    

    Config getDefaultConfig() {

        return defaultConfig;

    }

    

    @Override

    @SuppressWarnings("unchecked")

    public void setApplicationContext(ApplicationContext context) throws BeansException {

        

        if (initialized.compareAndSet(false, true)) {

            this.redisTemplate = context.getBean("stringReactiveRedisTemplate", ReactiveRedisTemplate.class);

            this.script = context.getBean(REDIS_SCRIPT_NAME, RedisScript.class);

            if (context.getBeanNamesForType(Validator.class).length > 0) {

                this.setValidator(context.getBean(Validator.class));

            }

        }

    }



    /**

     * This uses a basic token bucket algorithm and relies on the fact that

     * Redis scripts execute atomically. No other operations can run between

     * fetching the count and writing the new count.

     * @param routeId 路由服务名,如配置中的spring.cloud

     */

    @Override

    public Mono isAllowed(String routeId, String id) {

        

        // 会判断RedisRateLimiter是否初始化了

        if (!this.initialized.get()) {

            throw new IllegalStateException("RedisRateLimiter is not initialized");

        }

        

        // 根据key(接口)找到对应的限流配置

        //Config routeConfig = setConfig(id);

        

        // 获取routeId对应的限流配置

        Config routeConfig = getConfig().getOrDefault(routeId, defaultConfig);

        if (routeConfig == null) {

            throw new IllegalArgumentException("No Configuration found for route " + routeId);

        }

        

        // 允许用户每秒做多少次请求

        int replenishRate = routeConfig.getReplenishRate();

        int burstCapacity = routeConfig.getBurstCapacity();



        try {

            // 限流key的名称根据id获取对应键值,id为自定义的值如ip、user、path

            List keys = getKeys(id);

            // 传给LUA脚本的4个参数[令牌桶的容积,一秒钟内完成的最大请求数,当前时间]

            List scriptArgs = Arrays.asList(replenishRate + "", burstCapacity + "", Instant.now().getEpochSecond() + "", "1");

            // 执行LUA脚本

            Flux> flux = this.redisTemplate.execute(this.script, keys, scriptArgs);

            // .log("redisratelimiter", Level.FINER);

            return flux.onErrorResume(throwable -> Flux.just(Arrays.asList(1L, -1L)))

                      .reduce(new ArrayList(), (longs, l) -> {

                            longs.addAll(l);

                            return longs;

                        }).map(results -> {

                            boolean allowed = results.get(0) == 1L;

                            Long tokensLeft = results.get(1);

    

                            Response response = new Response(allowed, getHeaders(replenishRate, burstCapacity, tokensLeft));

                            if (log.isDebugEnabled()) {

                                log.debug("response: " + response);

                            }

                            return response;

                        });

        } catch (Exception e) {

            /*

             * We don't want a hard dependency on Redis to allow traffic. Make

             * sure to set an alert so you know if this is happening too much.

             * Stripe's observed failure rate is 0.01%.

             */

            e.printStackTrace();

            log.error("Error determining if user allowed from redis", e);

        }

        return Mono.just(new Response(true, getHeaders(replenishRate, burstCapacity, -1L)));

    }



    public HashMap getHeaders(Integer replenishRate, Integer burstCapacity, Long tokensLeft) {

        HashMap headers = new HashMap<>();

        headers.put(this.remainingHeader, tokensLeft.toString());

        headers.put(this.replenishRateHeader, String.valueOf(replenishRate));

        headers.put(this.burstCapacityHeader, String.valueOf(burstCapacity));

        return headers;

    }



    static List getKeys(String id) {

        // use `{}` around keys to use Redis Key hash tags

        // this allows for using redis cluster



        // Make a unique key per user.

        String prefix = "request_rate_limiter.{" + id;

        // You need two Redis keys for Token Bucket.

        String tokenKey = prefix + "}.tokens";

        

        String timestampKey = prefix + "}.timestamp";

        return Arrays.asList(tokenKey, timestampKey);

    }



    /**

     * 系统配置,要与 key-resolver名称匹配上,spring自动注入(spring可以处理处理‘-’并使用驼峰命名)

     * redis-rate-limiter.replenishRate: 10 # 令牌桶的容积,希望允许用户每秒处理多少个请求,而不会丢失任何请求

     * redis-rate-limiter.burstCapacity: 20 # 用户允许在一秒钟内完成的最大请求数,这是令牌桶可以容纳的令牌的数量,将此值设置为零将阻止所有请求

     * @author Administrator

     */

    @Validated

    public static class Config {

        @Min(1)

        private int replenishRate;



        @Min(1)

        private int burstCapacity = 1;



        public int getReplenishRate() {

            return replenishRate;

        }



        public Config setReplenishRate(int replenishRate) {

            this.replenishRate = replenishRate;

            return this;

        }



        public int getBurstCapacity() {

            return burstCapacity;

        }



        public Config setBurstCapacity(int burstCapacity) {

            this.burstCapacity = burstCapacity;

            return this;

        }



        @Override

        public String toString() {

            return "Config{" + "replenishRate=" + replenishRate + ", burstCapacity=" + burstCapacity + '}';

        }

    }

}

第三步、限流器工厂

package com.easystudy.limiter;



import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.beans.BeansException;

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.route.Route;

import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;

import org.springframework.context.ApplicationContext;

import org.springframework.context.ApplicationContextAware;

import org.springframework.core.io.buffer.DataBuffer;

import org.springframework.http.HttpHeaders;

import org.springframework.http.server.reactive.ServerHttpResponse;

import org.springframework.stereotype.Component;

import org.springframework.web.server.ServerWebExchange;



import com.easystudy.error.ErrorCode;

import com.easystudy.error.ReturnValue;



import reactor.core.publisher.Mono;



@Component

public class RateCheckLimiterGatewayFilterFactory extends AbstractGatewayFilterFactory implements ApplicationContextAware {

    private static Logger log = LoggerFactory.getLogger(RateCheckLimiterGatewayFilterFactory.class);

    private static ApplicationContext applicationContext;

    private RateCheckRedisRateLimiter rateLimiter;

    private KeyResolver keyResolver;



    public RateCheckLimiterGatewayFilterFactory() {

        super(Config.class);

    }



    @Override

    public void setApplicationContext(ApplicationContext context) throws BeansException {

        log.info("RateCheckLimiterGatewayFilterFactory.setApplicationContext,applicationContext=" + context);

        applicationContext = context;

    }



    @Override

    public GatewayFilter apply(Config config) {

        this.rateLimiter = applicationContext.getBean(RateCheckRedisRateLimiter.class);

        this.keyResolver = applicationContext.getBean(config.keyResolver, KeyResolver.class);



        return (exchange, chain) -> {

            Route route = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);



            return keyResolver.resolve(exchange).flatMap(key ->

                    // TODO: if key is empty?

                    rateLimiter.isAllowed(route.getId(), key).flatMap(response -> {

                        log.info("回复: " + response);

                        System.out.println("回复: " + response);

                        // TODO: set some headers for rate, tokens left

                        if (response.isAllowed()) {

                            return chain.filter(exchange);

                        }

                        // 超过了限流的response返回值

                        return setRateCheckResponse(exchange);

                    }));

        };

    }



    private Mono setRateCheckResponse(ServerWebExchange exchange) {

        // 超过了限流

        ServerHttpResponse response = exchange.getResponse();

        // 设置headers

        HttpHeaders httpHeaders = response.getHeaders();

        httpHeaders.add("Content-Type", "application/json; charset=UTF-8");

        httpHeaders.add("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0");

        // 设置body HttpStatus.TOO_MANY_REQUESTS.value()

        ReturnValue body = new ReturnValue(ErrorCode.ERROR_BUSY);

        DataBuffer bodyDataBuffer = response.bufferFactory().wrap(body.toJsonString().getBytes());

        return response.writeWith(Mono.just(bodyDataBuffer));

    }



    /**

     * 系统配置,要与 key-resolver名称匹配上,spring自动注入(spring可以处理处理‘-’并使用驼峰命名)

     * @author Administrator

     */

    public static class Config {

        private String keyResolver;//限流id



        public String getKeyResolver() {

            return keyResolver;

        }

        public void setKeyResolver(String keyResolver) {

            this.keyResolver = keyResolver;

        }

    }

}

第四步、限流配置:

server:

  #监听端口

  port: 7010

  #servlet上下文根路径

  servlet:

    #此时的gatewy是不起作用的,匹配路径为根路径

    #context-path: /gateway

     

spring:

  application:

    name: gateway

  #安全认证的配置--切记,此节点在spring节点之下,否则无法使用用户名密码登录

  security:

    # eureka服务端启用安全验证后,服务端必须【添加一个安全认证类】,开启httpbasic方式登录【因为高版本已经启用这种方式】

    user:

      # 用户名

      name: admin

      # 密码

      password: dw123456

  #redis配置-限流存储

  redis:

    #单台主机

    host: 127.0.0.1

    port: 6379

    pool:

      maxIdle: 300

      minIdle: 100

      max-wait: -1

      max-active: 600

  cloud:

    #gateway反向代理配置

    gateway:

      routes:

      #认证的http走这里

      - id: service-a

        #lb代表从注册中心获取服务,后面接的就是你需要转发到的服务名称,这个服务名称必须跟eureka中的对应

        #uri: lb://service-a

        uri:https://www.baidu.com

        #路由条件

        predicates:

        - Path= /service-a/**

        #过滤规则

        filters:

        - StripPrefix= 1

        #以及配置同- RequestRateLimiter=10, 20, #{@userKeyResolver}

        #使用内置的请求过滤器限流,常用的有Hystrix断路由,RequestRateLimiter限流,StripPrefix截取请求url

        #- name: RequestRateLimiter #使用默认的RequestRateLimiterGatewayFilterFactory(会查找过滤器名+GatewayFilterFactory的Bean去过滤)

        - name: RateCheckLimiter           #查找RateCheckLimiterGatewayFilterFactory进行过滤

          args:

            #redis限流

            redis-rate-limiter.replenishRate: 10 # 令牌桶的容积,希望允许用户每秒处理多少个请求,而不会丢失任何请求

            redis-rate-limiter.burstCapacity: 20 # 用户允许在一秒钟内完成的最大请求数,这是令牌桶可以容纳的令牌的数量,将此值设置为零将阻止所有请求

            #采用自定义配置限流,限流键解析器 Bean对象名字,是引用名为ipKeyResolver的bean的SpEL表达式

            #使用SpEL按名称引用bean见GatewayConfiguration或者不用SPEL直接指定bean名称,@Bean(name="apiKeyResolver")

            #而 keyResolver是选填 ,为空时使用默认值 defaultKeyResolver

            key-resolver: apiKeyResolver

      #websocket走这里

      - id: websocket

        uri: lb:ws://websocket

        #${server.servlet.context-path}/websocket/**

        predicates:

        - Path= /websocket/**

        filters:

        - StripPrefix= 1

       

# eureka配置

eureka:

  client:

    #从eureka服务器获取注册表信息的频率(以秒为单位)-用于客户端

    registry-fetch-interval-seconds: 10

    # 是否将eureka自身作为应用注册到eureka注册中心,默认为true

    registerWithEureka: true

    service-url:

      # 这里可以填写所有的eureka服务器地址并以','分离,当前面不能注册时候,自动选择后面的进行注册,排除单点故障问题

      defaultZone: http://${spring.security.user.name}:${spring.security.user.password}@127.0.0.1:7002/eureka/,http://${spring.security.user.name}:${spring.security.user.password}@127.0.0.1:7004/eureka/

  instance:

    #显示ip

    prefer-ip-address: true

    instance-id: ${spring.application.name}:${server.port}

    #配置与服务器心跳间隔-用于客户端

    lease-renewal-interval-in-seconds: 30

    #配置服务超时时间(此值将设置为至少高于leaseRenewalIntervalInSeconds中指定的值)-用于客户端

    #当服务关闭超过,这个时间时,eureka服务器会清除掉这个服务,配置此项要关闭服务器的自我保护模式-用于客户端

    lease-expiration-duration-in-seconds: 60

    #客户端自定义数据元数据-给其他eureka客户端调用--用户客户端

    metadata-map:

      #自定义元数据serverid

      serverid: gateway

     

logging:

  level:

    org.springframework.cloud.gateway: DEBUG #TRACE

    org.springframework.http.server.reactive: DEBUG

    org.springframework.web.reactive: DEBUG

    reactor.ipc.netty: DEBUG

    #让hibernate打印出执行的SQL

    #root: INFO

    #org.hibernate: INFO

    #org.hibernate.type.descriptor.sql.BasicBinder: TRACE

    #org.hibernate.type.descriptor.sql.BasicExtractor: TRACE 

 

第五步、自定义Bean配置

package com.easystudy.config;



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;



@Configuration

public class GatewayConfiguration {

    

    /**

     * 根据ip限流

     * @return

     */

    @Bean

    public KeyResolver ipKeyResolver() {

        return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getHostName());

    }

    

    /**

     * 根据用户限流:使用这种方式限流,请求路径中必须携带userId参数

     * @return

     */

    @Bean

    KeyResolver userKeyResolver() {

        return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("userId"));

    }

    

    /**

     * 接口限流:获取请求地址的uri作为限流key。

     */

    @Bean(name="apiKeyResolver")

    KeyResolver apiKeyResolver() {

        return exchange -> Mono.just(exchange.getRequest().getPath().value());

    }

}

全局过滤器:

package com.easystudy.config;



import org.springframework.cloud.gateway.filter.GatewayFilterChain;

import org.springframework.cloud.gateway.filter.GlobalFilter;

import org.springframework.context.annotation.Configuration;

import org.springframework.http.server.reactive.ServerHttpRequest;

import org.springframework.web.server.ServerWebExchange;



import reactor.core.publisher.Mono;



/**

* 自定义GlobalFilter只要使用了@Configuration注解或者配置为一个spring bean就会生效,不需要在RouteLocator中配置

* @author Administrator

*/

@Configuration

public class GlobalRouteFilter implements GlobalFilter {

    @Override

    public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {

        ServerHttpRequest.Builder builder = exchange.getRequest().mutate();

        

        builder.header("GlobalFilter","GlobalFilter success");

        //chain.filter(exchange.mutate().request(builder.build()).build());

        

        return chain.filter(exchange.mutate().request(builder.build()).build());

    }

}

跨域配置:

package com.easystudy.config;



import org.springframework.cloud.client.discovery.DiscoveryClient;

import org.springframework.cloud.gateway.discovery.DiscoveryClientRouteDefinitionLocator;

import org.springframework.cloud.gateway.discovery.DiscoveryLocatorProperties;

import org.springframework.cloud.gateway.route.RouteDefinitionLocator;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.http.HttpHeaders;

import org.springframework.http.HttpMethod;

import org.springframework.http.HttpStatus;

import org.springframework.http.server.reactive.ServerHttpRequest;

import org.springframework.http.server.reactive.ServerHttpResponse;

import org.springframework.web.cors.reactive.CorsUtils;

import org.springframework.web.server.ServerWebExchange;

import org.springframework.web.server.WebFilter;

import org.springframework.web.server.WebFilterChain;



import reactor.core.publisher.Mono;





/**

* 如果是前后端分离的项目,需要增加配置以解决跨域问题

*/

@Configuration

public class RouteConfiguration {

    //这里为支持的请求头,如果有自定义的header字段请自己添加(不知道为什么不能使用*)

    private static final String ALLOWED_HEADERS = "x-requested-with, authorization, Content-Type, Authorization, credential, X-XSRF-TOKEN,token,username,client";

    private static final String ALLOWED_METHODS = "*";

    private static final String ALLOWED_ORIGIN = "*";

    private static final String ALLOWED_Expose = "*";

    private static final String MAX_AGE = "18000L";



    @Bean

    public WebFilter corsFilter() {

        return (ServerWebExchange ctx, WebFilterChain chain) -> {

            ServerHttpRequest request = ctx.getRequest();

            if (CorsUtils.isCorsRequest(request)) {

                ServerHttpResponse response = ctx.getResponse();

                HttpHeaders headers = response.getHeaders();

                headers.add("Access-Control-Allow-Origin", ALLOWED_ORIGIN);

                headers.add("Access-Control-Allow-Methods", ALLOWED_METHODS);

                headers.add("Access-Control-Max-Age", MAX_AGE);

                headers.add("Access-Control-Allow-Headers", ALLOWED_HEADERS);

                headers.add("Access-Control-Expose-Headers", ALLOWED_Expose);

                headers.add("Access-Control-Allow-Credentials", "true");

                if (request.getMethod() == HttpMethod.OPTIONS) {

                    response.setStatusCode(HttpStatus.OK);

                    return Mono.empty();

                }

            }

            return chain.filter(ctx);

        };

    }

    /**

    *

    *如果使用了注册中心(如:Eureka),进行控制则需要增加如下配置

    */

    @Bean

    public RouteDefinitionLocator discoveryClientRouteDefinitionLocator(DiscoveryClient discoveryClient, DiscoveryLocatorProperties properties) {

        return new DiscoveryClientRouteDefinitionLocator(discoveryClient, properties);

    }

}

第六步、入口函数


 

package com.easystudy;



import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.cloud.client.discovery.EnableDiscoveryClient;



@SpringBootApplication

@EnableDiscoveryClient     // 该注解会根据配置文件中的地址,将服务自身注册到服务注册中

public class GatewayApplication {

    

    public static void main(String[] args) {

        SpringApplication.run(GatewayApplication.class, args);

    }



}

启动项目即可!

 

其他配置

 

 

你可能感兴趣的:(spring,cloud)