网关Gateway


【该项目提供了一个构建在Spring生态系统之上的API网关,包括:Spring 5、Spring Boot 2和 Project Reactor 。Spring Cloud Gateway旨在提供一种简单而有效的方法来路由到api,并为它们提供交叉关注点,例如:安全性、监视/度量和弹性。

使用Spring Cloud Gateway

要在项目中引入Spring Cloud Gateway,需要引用 group id为 org.springframework.cloud 和 artifact id为spring-cloud-starter-gateway starter。最新的Spring Cloud Release 构建信息,请参阅Spring Cloud Project page

如果应用了该starter,但由于某种原因不希望启用网关,请进行设置spring.cloud.gateway.enabled=false

Spring Cloud Gateway依赖Spring Boot和Spring Webflux提供的Netty runtime。它不能在传统的Servlet容器中工作或构建为WAR

词汇表

  • Route 路由:gateway的基本构建模块。它由ID、目标URI、断言集合和过滤器集合组成。如果聚合断言结果为真,则匹配到该路由。
  • Predicate 断言:这是一个Java 8 Function Predicate。输入类型是 Spring Framework ServerWebExchange。这允许开发人员可以匹配来自HTTP请求的任何内容,例如Header或参数。
  • Filter 过滤器:这些是使用特定工厂构建的 Spring Framework GatewayFilter实例。所以可以在返回请求之前或之后修改请求和响应的内容。

如何工作

客户端向Spring Cloud Gateway发出请求。如果Gateway Handler Mapping确定请求与路由匹配,则将其发送到Gateway Web Handler。此handler通过特定于该请求的过滤器链处理请求。图中filters被虚线划分的原因是filters可以在发送代理请求之前或之后执行逻辑。先执行所有“pre filter”逻辑,然后进行请求代理。在请求代理执行完后,执行“post filter”逻辑。

编写代码

假设有一个服务的地址如下所示,现在我们需要通过网关转发到这个地址来

http://localhost:8010/app1/echo/2020

依赖

<properties>
    <java.version>1.8</java.version>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <spring-boot.version>2.2.6.RELEASE</spring-boot.version>
    <spring-cloud-alibaba.version>2.2.1.RELEASE</spring-cloud-alibaba.version>
    <spring-cloud.version>Hoxton.SR4</spring-cloud.version>
</properties>

<dependencies>
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>
</dependencies>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>${spring-cloud.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-dependencies</artifactId>
            <version>${spring-cloud-alibaba.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>${spring-boot.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

启动类

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

配置文件

server:
  port: 9000
spring:
  application:
    name: cloud-gateway
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
    gateway:
      routes:
      - id: app1-service # 路由的id, 没有固定规则但要求唯一, 建议配合服务名
        uri: http://localhost:8010 # 匹配后提供服务的地址
        predicates:
        - Path=/app1/** # 断言,路径匹配的进行路由       

启动工程,在浏览器输入

http://localhost:9000/app1/echo/2020

这样就可以通过网关将请求转发到

http://localhost:8010/app1/echo/2020

配置路由方式

通过配置文件

spring:
  cloud:
    gateway:
      routes:
      - id: app1-service # 路由的id, 没有固定规则但要求唯一, 建议配合服务名
        uri: http://localhost:8010 # 匹配后提供服务的地址
        predicates:
        - Path=/app1/** # 断言,路径匹配的进行路由

JavaBean配置

@Configuration
public class GatewayConfig {
    @Bean
    public RouteLocator routeLocator(RouteLocatorBuilder routeLocatorBuilder) {
        RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes();
        routes.route(
                "app1-service",
                    r -> r.path("/app1/**").uri("http://localhost:8010")
                );
        return routes.build();
    }
}

动态路由

Gateway会根据注册中心注册的服务列表,根据注册中心上微服务名路径创建动态路由转发,从而实现动态路由的功能。Gateway可以根据服务名进行负载均衡。

spring:
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true  # 开启从注册中心动态创建路由的功能,利用微服务名进行路由
      routes:
      - id: app1-service # 路由的id, 没有固定规则但要求唯一, 建议配合服务名
        # uri: http://localhost:8101 # 匹配后提供服务的地址
        uri: lb://app1-service # 匹配后提供服务的地址,app1-service就是我们要访问的服务名
        predicates:
        - Path=/app1/** # 断言,路径匹配的进行路由

常用predicates

官方文档:https://cloud.spring.io/spring-cloud-static/spring-cloud-gateway/2.2.2.RELEASE/reference/html/#gateway-request-predicates-factories

After

spring:
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true  # 开启从注册中心动态创建路由的功能,利用微服务名进行路由
      routes:
      - id: app1-service # 路由的id, 没有固定规则但要求唯一, 建议配合服务名
        uri: lb://app1-service # 匹配后提供服务的地址
        predicates:
        - Path=/app1/** # 断言,路径匹配的进行路由
        - After=2020-05-02T15:41:31.892+08:00[Asia/Shanghai] # 在这个时间之后才可以正常访问

这个时间可通过下面代码生成

ZonedDateTime zonedDateTime = ZonedDateTime.now();
System.out.println(zonedDateTime);

Before

spring:
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true  # 开启从注册中心动态创建路由的功能,利用微服务名进行路由
      routes:
      - id: app1-service # 路由的id, 没有固定规则但要求唯一, 建议配合服务名
        uri: lb://app1-service # 匹配后提供服务的地址
        predicates:
        - Path=/app1/** # 断言,路径匹配的进行路由
        - After=2020-05-02T15:41:31.892+08:00[Asia/Shanghai] # 在这个时间之前才可以正常访问

Between

spring:
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true  # 开启从注册中心动态创建路由的功能,利用微服务名进行路由
      routes:
      - id: app1-service # 路由的id, 没有固定规则但要求唯一, 建议配合服务名
        uri: lb://app1-service # 匹配后提供服务的地址
        predicates:
        - Path=/app1/** # 断言,路径匹配的进行路由
        - Between=2020-05-02T15:47:31.892+08:00[Asia/Shanghai],2020-05-02T15:50:31.892+08:00[Asia/Shanghai] # 在这个时间段才可以正常访问

还有Cookie 、 Header 、 Host 、 Method 、 Path 、 Query 、 RemoteAddr ,用法都是类似的

GatewayFilter

官方文档:https://cloud.spring.io/spring-cloud-static/spring-cloud-gateway/2.2.2.RELEASE/reference/html/#gatewayfilter-factories

路由过滤器可用于修改进入的HTTP请求和返回的HTTP响应。SpringCloud Gateway内置了很多路由过滤器,他们都由GatewayFilter工厂类来产生。

AddRequestHeader为例

spring:
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true  # 开启从注册中心动态创建路由的功能,利用微服务名进行路由
      routes:
      - id: app1-service # 路由的id, 没有固定规则但要求唯一, 建议配合服务名
        # uri: http://localhost:8101 # 匹配后提供服务的地址
        uri: lb://app1-service # 匹配后提供服务的地址
        predicates:
        - Path=/app1/** # 断言,路径匹配的进行路由
        filters:
        - AddRequestHeader=X-Request-red, blue 

为这个路由的所有请求带上请求头信息,key=X-Request-red,value = blue

自定义过滤器

自定义过滤器需要实现GlobalFilter接口和Ordered接口

@Component
public class CustomGlobalFilter implements GlobalFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        String username = request.getQueryParams().getFirst("username");
        if (username == null) {
            System.out.println("用户名为空,非法操作");
            exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
            return exchange.getResponse().setComplete();
        }
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        // 值越小,优先级越高
        return 0;
    }
}

返回错误信息

@Component
public class CustomGlobalFilter implements GlobalFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        String username = request.getQueryParams().getFirst("username");
        if (username == null) {
            System.out.println("用户名为空,非法操作");
            ObjectMapper objectMapper = new ObjectMapper();
            Map<String, Object> map = new HashMap<>();
            map.put("code","000001");
            map.put("msg","用户名为空,非法操作");
            try {
                String string = objectMapper.writeValueAsString(map);
                ServerHttpResponse response = exchange.getResponse();
                DataBuffer buffer = response.bufferFactory().wrap(string.getBytes());
                response.setStatusCode(HttpStatus.UNAUTHORIZED);
                // 指定编码,否则在浏览器中会中文乱码
                response.getHeaders().add("Content-Type", "text/plain;charset=UTF-8");
                return response.writeWith(Mono.just(buffer));
            } catch (JsonProcessingException e) {
                e.printStackTrace();
            }
        }
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        // 值越小,优先级越高
        return 0;
    }
}

重定向

对于浏览器,通常是发现没有权限后跳转到登录页面。响应状态码需要为HttpStatus.SEE_OTHER(303)。

重定向(redirect)会丢失之前请求的参数,对于需要转发到目标URL的参数,需手工添加。

@Component
public class CustomGlobalFilter implements GlobalFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        String username = request.getQueryParams().getFirst("username");

        // 重定向(redirect)到登录页面
        if (username == null) {
            System.out.println("用户名为空,非法操作");
            String url = "http://localhost:9000/login.html";
            ServerHttpResponse response = exchange.getResponse();
            // 303状态码表示由于请求对应的资源存在着另一个URI,应使用GET方法定向获取请求的资源
            response.setStatusCode(HttpStatus.SEE_OTHER);
            response.getHeaders().set(HttpHeaders.LOCATION, url);
            return response.setComplete();
        }
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        // 值越小,优先级越高
        return 0;
    }
}

负载均衡实现原理

Gateway可以作为服务端的负载均衡,那么负载均衡的处理关键就是与Ribbon集成,Gateway是利用GlobalFilter进行实现的,它的实现类是LoadBalancerClientFilter,代码逻辑比较简单。

public class LoadBalancerClientFilter implements GlobalFilter, Ordered {
    protected final LoadBalancerClient loadBalancer;
	private LoadBalancerProperties properties;
	public LoadBalancerClientFilter(LoadBalancerClient loadBalancer,
			LoadBalancerProperties properties) {
		this.loadBalancer = loadBalancer;
		this.properties = properties;
	}
    
    @Override
	@SuppressWarnings("Duplicates")
	public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
		URI url = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR);
		String schemePrefix = exchange.getAttribute(GATEWAY_SCHEME_PREFIX_ATTR);
		if (url == null
				|| (!"lb".equals(url.getScheme()) && !"lb".equals(schemePrefix))) {
			return chain.filter(exchange);
		}
		// preserve the original url
		addOriginalRequestUrl(exchange, url);

		if (log.isTraceEnabled()) {
			log.trace("LoadBalancerClientFilter url before: " + url);
		}
        
        // 获取服务实例
		final ServiceInstance instance = choose(exchange);

		if (instance == null) {
			throw NotFoundException.create(properties.isUse404(),
					"Unable to find instance for " + url.getHost());
		}

		URI uri = exchange.getRequest().getURI();

		// if the `lb:<scheme>` mechanism was used, use `<scheme>` as the default,
		// if the loadbalancer doesn't provide one.
		String overrideScheme = instance.isSecure() ? "https" : "http";
		if (schemePrefix != null) {
			overrideScheme = url.getScheme();
		}
        // 构造请求URL地址
		URI requestUrl = loadBalancer.reconstructURI(
				new DelegatingServiceInstance(instance, overrideScheme), uri);

		if (log.isTraceEnabled()) {
			log.trace("LoadBalancerClientFilter url chosen: " + requestUrl);
		}

		exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, requestUrl);
		return chain.filter(exchange);
	}
    
    // 获取服务实例
    protected ServiceInstance choose(ServerWebExchange exchange) {
		return loadBalancer.choose(
				((URI) exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR)).getHost());
	}
}

自动配置类

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ LoadBalancerClient.class, RibbonAutoConfiguration.class,
		DispatcherHandler.class })
@AutoConfigureAfter(RibbonAutoConfiguration.class)
@EnableConfigurationProperties(LoadBalancerProperties.class)
public class GatewayLoadBalancerClientAutoConfiguration {

	@Bean
	@ConditionalOnBean(LoadBalancerClient.class)
	@ConditionalOnMissingBean({ LoadBalancerClientFilter.class,
			ReactiveLoadBalancerClientFilter.class })
	public LoadBalancerClientFilter loadBalancerClientFilter(LoadBalancerClient client,
			LoadBalancerProperties properties) {
		return new LoadBalancerClientFilter(client, properties);
	}

}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值