GateWay配置使用

是什么

  • Cloud全家桶中有个很重要的组建就是网关,在1.x版本中都是采用的Zuul网关
  • 但在2.X版本中,zuul的升级一直跳票,SpringCloud最后自己研发了一个网关替代Zuul
  • SpringCloudGateway:gateway是原zuul1.X版的替代.

image.png

  • Gateway是在Spring生态系统之上构建的APL网关服务,基于Spring5、Spring Boot2和Project Reactor等技术
  • Gateway指在提供一种简单的有效的方式来对API进行路由,以及提供一些强大的过滤器功能,例如:熔断、限流、重试等

image.png

  • SpringCloud Gateway是SpringCloud的一个全新项目,基于Spring 5.0+SpringBoot 2.0 和 ProjectReactor等技术开发的网关,它指在为微服务架构提供一种简单有效的统一的API路由管理方式

  • SpringCloud Gateway作为SpringCloud生态系统中的网关,目标是替代Zuul,在SpringCloud 2.0以上版本中,没有对新版本的Zuul 2.0以上最新高性能版本进行集成,仍然还是使用的Zuul 1.X非Reactor模式的老版本。而为了提升网关的性能,SpringCloud Gateway是基于WebFlux框架实现的,而WebFlux框架底层则使用了高性能的Reactor模式通信框架Netty

  • SpringCloud Gateway的目标提供统一的路由方式且基于Filter链的方式提供了网关基本的功能,例如:安全、监控/指标、和限流

  • SpringCloud Gateway使用的是WebFlux中的reactor-netty响应式编程组建,底层使用了Netty通讯框架

作用

  • 反向代理
  • 鉴权
  • 流量控制
  • 熔断
  • 日志监控

位置

image.png

Gateway与Zuul的区别

在SpringCloud Finchley正式版之前,SpringCloud 推荐的网关是Netflix提供的Zuul

  1. Zuul 1.X,是一个基于阻塞I/O的API
  2. Zuul 1.X基于Servlet2.5使用阻塞架构它不支持任何长链接(如:WebSocket)Zuul的设计模式和Nginx较像,每次I/O操作都是从工作线程中选择一个执行,请求线程被阻塞到工作线程完成,但是差别是Nginx用C++实现,Zuul用Java实现,而JVM本身会有第一次加载较慢的情况,使的Zuul的性能相对较差
  3. Zuul 2.X理念更先进,想基于Netty非阻塞和支持长连接,但SpringCloud目前还没有整合。Zuul 2.X的性能较Zuul 1.X有较大的提升,在性能方面,根据官方提供的基准测试,SpringCloud Gateway的RPS(每秒请求数)是Zuul的1.6倍
  4. SpringCloud Gateway 建立在Spring Framework 5 、ProjectReactor和SpringBoot 2之上,使用非阻塞API
  5. SpringCloud Gateway还支持WebSocket,而且与Spring紧密集成拥有更好的开发体验

三大核心概念

Route(路由)

  • 路由是构建网关的基本模块,它由ID,目标URI,一系列的断言和过滤器组成,如断言为true则匹配该路由

Predicate(断言)

  • 参考的是Java8的java.util.function.Predicate
  • 开发人员可以匹配HTTP请求中的所有内容(例如请求头或请求参数),如果请求与断言相匹配则进行路由

Filter(过滤)

  • 指的是Spring框架中GatewayFilter的实例,使用过滤器,可以在请求被路由前或者之后对请求进行修改

理论总结

image.png

  • web请求,通过一些匹配条件,定位到真正的服务节点,并在这个转发过程的前后,进行一些精细化控制
  • predicate就是我们的匹配条件,而Filter,就可以理解为一个无所不能的拦截器,有了这两个元素,再加上目标url,就可以实现一个具体的路由了

Gateway工作流程

2.jpg

  • 客户端向 Spring Cloud Gateway发出请求。然后在 Gateway Handler Mapping中找到与请求相匹配的路由,将其发送到 Gateway Web Handler.
  • Handler再通过指定的过滤器链来将请求发送到我们实际的服务执行业务逻辑,然后返回
  • 过滤器之间用虚线分开是因为过滤器可能会在发送代理请求之前或之后执行业务逻辑
  • Filter在"pre"类型的过滤器可以做参数校验、权限校验、流量监控、日志输出、协议转换等
  • 在"post"类型的过滤器中可以做响应内容、响应头的修改,日志的输出,流量监控等有着非常重要的作用

项目搭建

导包

注:gateway无需导入web包

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
            <version>2.2.5.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
            <version>2.2.5.RELEASE</version>
        </dependency>

注册到Eureka

@SpringBootApplication
@EnableEurekaClient
public class GatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(GatewayApplication.class, args);
    }
}

指定某一台服务跳转

修改YML文件

server:
  port: 8888
spring:
  application:
    name: gateway
  cloud:
    gateway:
      routes:
        - id: route1 # 路由的id,保证唯一,推荐以业务、微服务起名
          uri: http://192.168.1.2 #匹配后提供服务的路由地址 访问192.168.1.2:8888实际执行的是192.168.1.2
          predicates:
            Path=/gateway/** # 路径匹配
        - id: route2
          uri: http://192.168.1.2
          predicates:
            Path=/route2/** 
eureka: #注册中心配置
  instance:
    instance-id: gateway
    prefer-ip-address: true
  client:
    fetch-registry: true
    register-with-eureka: true
    service-url:
      defaultZone: http://192.168.1.2:8761/eureka/

测试

  • 在client项目中创建一个方法feignGetInstance,路径为/gateway/feignGetInstance

  • 启动gateway及之前搭建好的eureka\service\client项目(每个项目启动一个还是多个无所谓,本次主要测试gateway转发)

  • 访问http://192.168.1.2:8888/gateway/feignGetInstance(gateway项目) 就相当于请求http://192.168.1.2/gateway/feignGetInstance(client项目)

指定服务名称跳转

上面配置的uri是某一台服务的地址,但如果是集群,需要配置Eureka中的服务名称

修改YML

新增如下配置

discovery:
locator:
enabled: true # 开启从注册中心动态创建路由的功能,利用微服务名称进行路由

修改如下配置

​ uri: lb://CLIENT-PROJECT

server:
  port: 8888
spring:
  application:
    name: gateway
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true # 开启从注册中心动态创建路由的功能,利用微服务名称进行路由
      routes:
        - id: route1 # 路由的id,保证唯一,推荐以业务、微服务起名
          uri: lb://CLIENT-PROJECT #匹配后提供服务的服务名称,从Eureka中Application一列就是
          predicates:
            Path=/gateway/** # 路径匹配
        - id: route2
          uri: http://192.168.1.2
          predicates:
            Path=/route2/** 
eureka: #注册中心配置
  instance:
    instance-id: gateway
    prefer-ip-address: true
  client:
    fetch-registry: true
    register-with-eureka: true
    service-url:
      defaultZone: http://192.168.1.2:8761/eureka/

测试

  • 启动多个client项目

  • 访问http://192.168.1.2:8888/gateway/feignGetInstance会自动通过Application服务名通过LB负载均衡找到对应的某服务

通过JAVA代码配置路由规则

添加如下Bean

    @Bean
    public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
        return builder.routes()
                .route(r -> r.path("/getInstanceIdByTimeOut/*")
                        .uri("lb://SERVICE-PROJECT") //也可使用http://192.168.1.2....
                        .id("java_route")
                )
                .build();
    }

测试

访问http://192.168.1.2:8888/getInstanceIdByTimeOut/1可发现也成功跳转

注意

path配置的路径要有对应的方法

过滤规则配置

routes:
        - id: route1 # 路由的id,保证唯一,推荐以业务、微服务起名
          uri: lb://CLIENT-PROJECT #匹配后提供服务的路由地址 访问192.168.1.2:8888实际执行的是192.168.1.2
          predicates: #各种过滤条件
            - Path=/gateway/** # 路径匹配
            #- After=2020-11-13T16:35:15.064+08:00[Asia/Shanghai] #在指定时间之后才可以访问
            #- Before=2021-11-13T16:38:15.064+08:00[Asia/Shanghai] #在指定时间之前才可以访问
            #- Cookie=mycookie,test #有指定的cookie并且与val一致才可以访问
            #- Header=X-Request-Id, 123 #请求头要有X-Request-Id属性,并且值为123才可以访问
            #- Host=*.xxx.com #必须是某些域名才可以访问
            #- Method=GET #必须是GET才能访问
            #- Query=param, 123 # 要有参数名param并且值是123才能访问
          filters:
            #- AddRequestHeader=myHeader, hval #接口方可通过request.getHeader()获得这个值
            #- AddRequestParameter=myParameter, pval #接口方可通过request.getParameter()获得这个值

自定义过滤器

可以拦截符合路由条件的所有请求,从而进行权限、拦截、限流等等操作,添加如下配置即可

package com.project.gateway.filter;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.reactivestreams.Publisher;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.filter.NettyWriteResponseFilter;
import org.springframework.cloud.gateway.filter.factory.rewrite.CachedBodyOutputMessage;
import org.springframework.cloud.gateway.support.BodyInserterContext;
import org.springframework.cloud.gateway.support.DefaultClientResponse;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.*;
import org.springframework.http.client.reactive.ClientHttpResponse;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.web.reactive.function.BodyInserter;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.ExchangeStrategies;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.nio.CharBuffer;
import java.nio.charset.StandardCharsets;
import java.util.List;

@Component
@Slf4j
public class GateWayFilter implements GlobalFilter, Ordered {
    @Override
    public int getOrder() {
        // 控制在NettyWriteResponseFilter后执行
        return NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER - 1;
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        return processResponse(exchange, chain);
    }

    private Mono<Void> processResponse(ServerWebExchange exchange, GatewayFilterChain chain) {

        ServerHttpRequest request = exchange.getRequest();//获得入参\cookies\headers....
        //List<String> headersval = request.getHeaders().get("headersval");
        String param = request.getQueryParams().getFirst("param");
        if (!StringUtils.isEmpty(param)) {
            //可在这进行校验是否登录、权限等等
            if(true) {
                //权限如果不符合等等逻辑,进入
                exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
                return exchange.getResponse().setComplete();
            }else{
                //权限、限流等条件正常进入
                //获得响应值start
                ServerHttpResponseDecorator responseDecorator = new ServerHttpResponseDecorator(exchange.getResponse()) {
                    @Override
                    public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
                        HttpHeaders httpHeaders = new HttpHeaders();
                        //httpHeaders.add(HttpHeaders.CONTENT_TYPE, 设置一些值);
                        ResponseAdapter responseAdapter = new ResponseAdapter(body, httpHeaders);
                        DefaultClientResponse clientResponse = new DefaultClientResponse(responseAdapter, ExchangeStrategies.withDefaults());
                        Mono<String> rawBody = clientResponse.bodyToMono(String.class).map(s -> s);
                        BodyInserter<Mono<String>, ReactiveHttpOutputMessage> bodyInserter = BodyInserters.fromPublisher(rawBody, String.class);
                        CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange, exchange.getResponse().getHeaders());
                        return bodyInserter.insert(outputMessage, new BodyInserterContext())
                                .then(Mono.defer(() -> {
                                    Flux<DataBuffer> messageBody = outputMessage.getBody();
                                    Flux<DataBuffer> flux = messageBody.map(buffer -> {
                                        CharBuffer charBuffer = StandardCharsets.UTF_8.decode(buffer.asByteBuffer());
                                        DataBufferUtils.release(buffer);
                                        // 将响应信息转化为字符串
                                        String responseStr = charBuffer.toString();
                                        responseStr="可编辑响应值";
                                        return getDelegate().bufferFactory().wrap(responseStr.getBytes(StandardCharsets.UTF_8));

                                    });
                                    HttpHeaders headers = getDelegate().getHeaders();
                                    // 修改响应包的大小,不修改会因为包大小不同被浏览器丢掉
                                    flux = flux.doOnNext(data -> headers.setContentLength(data.readableByteCount()));
                                    return getDelegate().writeWith(flux);
                                }));
                    }
                };
                //获得响应值end
                return chain.filter(exchange.mutate().response(responseDecorator).build());
            }
        }
        return chain.filter(exchange);
    }
    private class ResponseAdapter implements ClientHttpResponse {
        private final Flux<DataBuffer> flux;
        private final HttpHeaders headers;

        @SuppressWarnings("unchecked")
        private ResponseAdapter(Publisher<? extends DataBuffer> body, HttpHeaders headers) {
            this.headers = headers;
            if (body instanceof Flux) {
                flux = (Flux) body;
            } else {
                flux = ((Mono) body).flux();
            }
        }

        @Override
        public Flux<DataBuffer> getBody() {
            return flux;
        }

        @Override
        public HttpHeaders getHeaders() {
            return headers;
        }

        @Override
        public HttpStatus getStatusCode() {
            return null;
        }

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

        @Override
        public MultiValueMap<String, ResponseCookie> getCookies() {
            return null;
        }
    }

}

posted @ 2021-04-14 17:48  RollBack2010  阅读(896)  评论(0编辑  收藏  举报