spring-cloud-gateway及其应用

网关功能

  • 流量转发
  • 用户认证
  • 服务限流
  • 服务降级
  • 灰度发布

1 概述

基于springboot和spring webflux,基于netty运行,的http网关服务。

它的目标是替换奈飞的zuul。其实它和zuul2的性能差不多。选择其一即可。

考虑到spring的生态,使用spring-cloud-gateway更加有优势

2 核心概念

Route 网关的基础(断言为真时匹配到路由)

  • id
  • 目标uri
  • 断言
  • 过滤器

Predicate java8的函数,输入类型是webflux的ServerWebExchange,允许开发人员处理http请求

Filter是gateway上的过滤器,可以在请求发出前后做一些业务上的处理

整体流程

maven配置

    <dependencies>
        <!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-gateway -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
            <version>3.0.4</version>
        </dependency>


        <!--
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
        </dependency>
    </dependencies>

服务配置

spring:
  application:
    name: zuul-gateway-static
  cloud:
    gateway:
      routes:
        - id: test1
          uri: "http://www.example.com/"
          predicates:
            - Path=/abc/**
server:
  port: 10011
  address: 127.0.0.1
debug: true
management:
    endpoint:
      gateway:
        enabled: true
    endpoints:
      web:
        exposure:
          include: "*"
logging:
  config: classpath:logback-spring.xml



#eureka.instance.ip-address=127.0.0.1
#eureka.client.serviceUrl.defaultZone=http://tom:123456@localhost:9010/eureka/
#eureka.instance.preferIpAddress=true
#eureka.instance.instance-id=${spring.application.name}:${server.address}:${server.port}
#eureka.client.healthcheck.enabled=true
#eureka.instance.lease-expiration-duration-in-seconds=20
#eureka.instance.lease-renewal-interval-in-seconds=15

访问页面就可以看到转发的数据 http://127.0.0.1:10011/abc

spring-cloud-gateway 接入consul转发服务信息

注册consul

consul:
      discovery:
        service-name: zuul-gateway-static
        health-check-path: /health/status
        prefer-ip-address: true
        ip-address: 127.0.0.1
      host: localhost
      port: 8500

配置consul的转发信息

- id: lb-info
          uri: lb://consul-server-producer
          predicates:
            - Path=/info/**

访问转发后的地址: http://127.0.0.1:10011/info/1231211

各类断言工厂(路由判断)

path路由断言工厂

  • 配置
        - id: pathInfo
          uri: http://www.example.com/
          predicates:
            - Path=/abcd/{segment}

query路由断言工厂

  • 配置
        - id: queryInfo
          uri: http://www.example.com/
          predicates:
            - Query= foo,bb

method路由断言工厂`

  • 配置
- id: methodnfo
          uri: http://www.example.com/
          predicates:
            - Method= DELETE
  • 访问地址:
curl --location --request DELETE 'http://127.0.0.1:10011/adfasfsdfdsd'

head 路由断言工厂

  • 配置
        - id: headinfo
          uri: http://www.example.com/
          predicates:
            - Header=x-ragnar-traceid,[\w\d]+
  • 访问地址
curl --location --request GET 'http://127.0.0.1:10011/adfasfsdfdsd12312' \
--header 'x-ragnar-traceid: 123213123'

自定义路由断言工厂

自定义断言工厂代码

@Slf4j
@Component
public class GrayRoutePredicateFactory extends AbstractRoutePredicateFactory<GrayCfg> {

    public GrayRoutePredicateFactory() {
        super(GrayCfg.class);
    }

    @Override
    public Predicate<ServerWebExchange> apply(GrayCfg cfg) {

        return serverWebExchange -> {
            log.info("enter GrayRoutePredicateFactory"+cfg.isGrayStatus());
            if (cfg.isGrayStatus()) {
                log.info(" GrayRoutePredicateFactory hit   start gray");
                return true;
            }

            return false;
        };
    }
}

自定义断言工厂配置

        - id: grayinfo
          uri: http://www.baidu.com/
          predicates:
            - Path=/eee/**
            - name: Gray
              args:
                grayStatus: true

过滤器工厂(修改请求)

AddRequestHeader过滤器工厂

- AddRequestHeader=from,abc

RemoveRequestHeader过滤器工厂

- RemoveRequestHeader=from2

SetStatus过滤器工厂

- id: statusFilter
    uri: http://www.example.com/
    predicates:
    - Query=foo,b1
    filters:
    - SetStatus=401

RedirectTo过滤器工厂

- id: redictFilter
    uri: http://www.example.com/
    predicates:
    - Query=foo,b2
    filters:
    - RedirectTo=302,http://weibo.com

自定义过滤器工厂

  • 自定义过滤器配置
 - id: lb-info
    uri: lb://consul-server-producer
    predicates:
    - Path=/info/**
    filters:
    - RemoveRequestHeader=from2
    - AddRequestHeader=from,abc
    - name: Login
        args:
        checkLogin: true
  • 自定义过滤器代码@Slf4j
@Component
public class LoginGatewayFilterFactory extends AbstractGatewayFilterFactory<LoginCfg> {


public LoginGatewayFilterFactory() {

    super(LoginCfg.class);

}

@Override
public GatewayFilter apply(LoginCfg loginCfg) {

    return (exchange, chain) -> {
        log.info("if us check login:"+ loginCfg.isCheckLogin());
        ServerHttpRequest request = exchange.getRequest().mutate()
                .build();
        if (loginCfg.isCheckLogin()) {
            log.info("do login checking.......");
        }
        return chain.filter(exchange.mutate().request(request).build());
    };
}
}

全局过滤器

[图片上传失败...(image-f49665-1664766893624)]

自定义全局过滤器

package cn.beckbi.filter;

import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import reactor.core.publisher.Mono;

/**
 * @program: spring-cloud
 * @description:
 * @author: bikang
 * @create: 2022-08-21 20:21
 */
@Slf4j
@Configuration
public class GlobalFilterConfig {


    @Bean
    @Order(-2)
    public GlobalFilter filter1() {
        return (exchange, chain) -> {
            log.info("filter1 pre");
            return chain.filter(
                    exchange
            ).then(
                Mono.fromRunnable(()->{
                    log.info("filter1 post");
                })
            );
        };
    }

    @Bean
    @Order(0)
    public GlobalFilter filter2() {
        return (exchange, chain) -> {
            log.info("filter2 pre");
            return chain.filter(
                    exchange
            ).then(
                    Mono.fromRunnable(()->{
                        log.info("filter2 post");
                    })
            );
        };
    }

    @Bean
    @Order(2)
    public GlobalFilter filter3() {
        return (exchange, chain) -> {
            log.info("filter3 pre");
            return chain.filter(
                    exchange
            ).then(
                    Mono.fromRunnable(()->{
                        log.info("filter3 post");
                    })
            );
        };
    }
}

自定义拦截器

package cn.beckbi.filter;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Builder;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.client.loadbalancer.ResponseData;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
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 org.springframework.util.MultiValueMap;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.Map;
import java.util.Objects;

/**
 * @program: spring-cloud
 * @description:
 * @author: bikang
 * @create: 2022-08-21 20:29
 */
@Slf4j
@Component
public class UserFilter implements GlobalFilter, Ordered {

    @Builder
    @Data
    static class Resp {
        private int code;
        private String msg;
    }

    private static final String BAD_CID = "123";

    private ObjectMapper mapper = new ObjectMapper();

    @Override
    public int getOrder(){
        return 0;
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        MultiValueMap<String, String> queryParams = exchange.getRequest().getQueryParams();

        boolean matchFilter = false;

        if (Objects.nonNull(queryParams) && Objects.nonNull(queryParams.get("cid"))) {
            String cid = queryParams.get("cid").get(0);
            if (Objects.nonNull(cid) && BAD_CID.equals(cid)) {
                matchFilter = true;
            }
        }






        if (matchFilter) {
            ServerHttpResponse serverHttpResponse = exchange.getResponse();
            Resp resp = Resp.builder()
                    .code(401)
                    .msg("非法请求")
                    .build();
            DataBuffer dataBuffer = serverHttpResponse.bufferFactory().wrap(
                    this.getJsonBytes(resp)
            );
            serverHttpResponse.setStatusCode(HttpStatus.UNAUTHORIZED);
            serverHttpResponse.getHeaders().add("Content-Type", "application/json; charset=utf-8");


            return serverHttpResponse.writeWith(Mono.just(dataBuffer));
        }

        return chain.filter(exchange);
    }

    private byte[] getJsonBytes(Object o) {
        try {
            return mapper.writeValueAsBytes(o);
        }catch (JsonProcessingException e) {
            log.error("json error", e);
        }
        return "".getBytes();
    }


}

http://127.0.0.1:10011/info/1231212312?cid=123

限流器代码:基于redis做限流

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
package cn.beckbi.limiter;

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.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import reactor.core.publisher.Mono;

import java.util.Objects;

/**
 * @program: spring-cloud
 * @description:
 * @author: bikang
 * @create: 2022-08-21 21:02
 */
@Configuration
public class LimiterConfig {

    @Bean("ipKeyResolver")
    public KeyResolver ipKeyResolver() {
        return exchange -> Mono.just(
                Objects.requireNonNull(exchange.getRequest().getRemoteAddress()).getHostName()
        );
    }

    @Bean("cidKeyResolver")
    public KeyResolver cidKeyResolver() {
        return exchange -> Mono.just(
                Objects.requireNonNull(exchange.getRequest().getQueryParams().getFirst("cid"))
        );
    }

    @Primary
    @Bean("apiKeyResolver")
    public KeyResolver apiKeyResolver() {
        return exchange -> {
            Route route = (Route) exchange.getAttributes().get(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
            return Mono.just(
                    route.getId()+"#"+exchange.getRequest().getPath().value()
            );
        };
    }
}

限流器配置

- name: RequestRateLimiter
    args:
    redis-rate-limiter.replenishRate: 1
    redis-rate-limiter.burstCapacity: 2
    key-resolver: "#{@apiKeyResolver}"

全局路由处理器

package cn.beckbi.errorhandler;



import cn.beckbi.util.JsonUtil;
import lombok.Builder;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.web.ErrorProperties;
import org.springframework.boot.autoconfigure.web.WebProperties;
import org.springframework.boot.autoconfigure.web.reactive.error.DefaultErrorWebExceptionHandler;
import org.springframework.boot.web.reactive.error.ErrorAttributes;
import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler;
import org.springframework.context.ApplicationContext;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.reactive.function.server.RequestPredicates;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import reactor.util.annotation.Nullable;


/**
 * @program: spring-cloud
 * @description:
 * @author: bikang
 * @create: 2022-08-21 21:36
 */
@Component
@Slf4j
@Order(-1)
public class JsonHandler implements ErrorWebExceptionHandler {

    @Builder
    @Data
    static class Msg {
        int code;
        String msg;
    }
    @Override
    public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {

        ServerHttpResponse response = exchange.getResponse();

        if (response.isCommitted()) {
            return Mono.error(ex);
        }

        response.getHeaders().setContentType(MediaType.APPLICATION_JSON);


        ServerHttpRequest request = exchange.getRequest();
        String rawQuery = request.getURI().getRawQuery();
        String query = StringUtils.hasText(rawQuery) ? "?" + rawQuery : "";
        String path = request.getPath() + query ;
        String message ;
        HttpStatus status = null;
        if (ex instanceof ResponseStatusException) {
            status = ((ResponseStatusException) ex).getStatus();
        }

        if (status == null){
            status = HttpStatus.INTERNAL_SERVER_ERROR;
        }

        // 通过状态码自定义异常信息
        if (status.value() >= 400 && status.value() < 500){
            message = "路由服务不可达或禁止访问!";
        }else {
            message = "路由服务异常!";
        }
        message += " path:" + path;

        Msg msg = Msg.builder().code(status.value())
                .msg(message)
                .build();


        return response
                .writeWith(Mono.fromSupplier(() -> {
                    DataBufferFactory bufferFactory = response.bufferFactory();
                    return bufferFactory.wrap(JsonUtil.getJsonBytes(msg));
                }));
    }

}

代码路径: https://github.com/beckbikang/spring-cloud/tree/main/kgateway

spring-cloud-gateway动态路由

实现了spring-cloud的动态路由,一个真正可用的网关就成型了,从应用的角度来看,这就是spring-cloud的最后的一课了

动态路由其实不难实现 RouteDefinitionRepository 接口即可。

配置

spring:
  application:
    name: zuul-gateway-dynamic
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true
          lower-case-service-id: true
    consul:
      discovery:
        service-name: zuul-gateway-dynamic
        health-check-path: /health/status
        prefer-ip-address: true
        ip-address: 127.0.0.1
      host: localhost
      port: 8500
  redis:
    database: 0
    host: 127.0.0.1
    port: 6379
    connect-timeout: 100ms
    timeout: 500ms
    jedis:
      pool:
        max-active: 200
        min-idle: 10
        max-idle: 20
        max-wait: 10000ms

server:
  port: 10013
  address: 127.0.0.1
debug: true
management:
    endpoint:
      gateway:
        enabled: true
    endpoints:
      web:
        exposure:
          include: "*"
logging:
  config: classpath:logback-spring.xml

代码

package cn.beckbi.route;

import com.alibaba.fastjson2.JSON;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionRepository;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.util.ArrayList;
import java.util.List;

/**
 * @program: spring-cloud
 * @description:
 * @author: bikang
 * @create: 2022-08-22 07:47
 */
@Slf4j
@Component
public class RedisRouteDefinitionRepository implements RouteDefinitionRepository {

    private static final String GATEWAY_ROUTE = "SPRING_CLOUD_GATEWAY_ROUTE_LIST";

    @Autowired
    private StringRedisTemplate stringRedisTemplate;


    @Override
    public Flux<RouteDefinition> getRouteDefinitions() {
        List<RouteDefinition> routeDefinitionList = new ArrayList<>();
        stringRedisTemplate.opsForHash().values(GATEWAY_ROUTE).forEach(route -> {
            routeDefinitionList.add(JSON.parseObject(route.toString(), RouteDefinition.class));
        });
        return Flux.fromIterable(routeDefinitionList);
    }

    @Override
    public Mono<Void> save(Mono<RouteDefinition> route) {
        return route.flatMap(routeDefinition -> {
            stringRedisTemplate.opsForHash().put(GATEWAY_ROUTE, routeDefinition.getId(), JSON.toJSONString(routeDefinition));
            return Mono.empty();
        });
    }

    @Override
    public Mono<Void> delete(Mono<String> routeId) {
        return routeId.flatMap(id->{
            if(stringRedisTemplate.opsForHash().hasKey(GATEWAY_ROUTE, id)){
                stringRedisTemplate.opsForHash().delete(GATEWAY_ROUTE, id);
                return Mono.empty();
            }else {
                return Mono.defer(() -> Mono.error(new Exception("routeDefinition not found:" + routeId)));
            }
        });
    }
}

动态增加路由

curl --location --request POST 'http://localhost:10013/actuator/gateway/routes/test1' \
--header 'Content-Type: application/json' \
--data-raw '{"uri":"http://www.example.com/","predicates":[{"name":"Path","args":{"pattern":"/abc/**"}}],"filters":[{"name":"StripPrefix","args":{"value":1}}],"order":0}'
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容