ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

Spring Cloud - 05 (Gateway)

2021-09-28 02:01:08  阅读:159  来源: 互联网

标签:断言 Spring springframework gateway import 过滤器 org Gateway Cloud


目录

服务网关-Gateway

服务网关是微服务的第一道关卡,目前Nginx是应用最广泛的反向代理技术,在各个大厂的核心业务系统中都有大量应用,不过Nginx可不是使用Java来配置的,使用和配置Nginx需要掌握它的语法树。Spring Cloud则为广大的Java技术人员提供了更加“编程友好”的方式来构建网关层,那就是Gateway网关层组件。我们可以通过Java代码或者是yml配置文件的方式编写自己的路由规则,并通过内置过滤器或自定义过滤器来实现复杂的业务需求(比如在网关层做令牌验证)。Gateway本身也集成了强大的限流功能,结合使用Redis+SpEL表达式,可以对业务系统进行精准限流。

Gateway可以做什么?
image
在Spring Cloud中,Gateway是借助Eureka的服务发现机制来实现服务寻址的,负载均衡则依靠Ribbon。

路由功能

Gateway中可以定义很多个Route,一个Route就是一套包含完整转发规则的路由,主要由三部分组成:
image

  • 断言(Predicate)集合:断言是路由处理的第一个环节,它是路由的匹配规则,它决定了一个网络请求是否可以匹配给当前路由来处理。之所以它是一个集合的原因是我们可以给一个路由添加多个断言,当每个断言都匹配成功以后才算过了路由的第一关。

  • 过滤器(filter)集合:如果请求通过了前面的断言匹配,那就表示它被当前路由正式接手了,接下来这个请求就要经过一系列的过滤器集合。过滤器的功能就是八仙过海各显神通了,可以对当前请求做一系列的操作,比如说权限验证,或者将其他非业务性校验的规则提到网关过滤器这一层。在过滤器这一层依然可以通过修改Response里的Status Code达到中断效果,比如对鉴权失败的访问请求设置Status Code为403之后中断操作。

  • URI:如果请求顺利通过过滤器的处理,接下来就到了最后一步,那就是转发请求。URI是统一资源标识符,它可以是一个具体的网址,也可以是IP+端口的组合,或者是Eureka中注册的服务名称。

关于负载均衡: 对最后一步寻址来说,如果采用基于Eureka的服务发现机制,那么在Gateway的转发过程中可以采用服务注册名的方式来调用,后台会借助Ribbon实现负载均衡(可以为某个服务指定具体的负载均衡策略),其配置方式如:lb://FEIGN-SERVICE-PROVIDER/,前面的lb就是指代Ribbon作为LoadBalancer。

常用断言(Predicate)介绍

一个请求在抵达网关层后,首先就要进行断言匹配,在满足所有断言之后才会进入Filter阶段。

1、路径匹配:
Path断言是最常用的一个断言请求,几乎所有路由都要使用到它,我们来看一下它的例子

.route(r -> r.path("/gateway/**")
             .uri("lb://FEIGN-SERVICE-PROVIDER/")
)
.route(r -> r.path("/baidu")
             .uri("http://baidu.com:80/")
)

Path断言的使用非常简单,就像我们在Controller中配置@RequestPath的方式一样,在Path断言中填上一段URL匹配规则,当实际请求的URL和断言中的规则相匹配的时候,就下发到该路由中URI指定的地址,这个地址可以是一个具体的HTTP地址,也可以是Eureka中注册的服务名称。在上面的例子中,如果我们访问“/gateway/test”,这个路径将匹配到第一个路由。

2、M:ethod断言:
这个断言是专门验证HTTP Method的,在下面的例子中,我们把Method断言和Path断言通过一个and连接符合并起来,共同作用于路由判断,当我们访问“/gateway/sample”并且HTTP Method是GET的时候,将适配下面的路由

.route(r -> r.path("/gateway/**")
             .and().method(HttpMethod.GET)
             .uri("lb://FEIGN-SERVICE-PROVIDER/")
)

3、RequestParam匹配:
请求断言也是在业务中经常使用的,它会从ServerHttpRequest中的Parameters列表中查询指定的属性,有如下两种不同的使用方式

.route(r -> r.path("/gateway/**")
             .and().method(HttpMethod.GET)
             .and().query("name", "test")
             .and().query("age")
             .uri("lb://FEIGN-SERVICE-PROVIDER/")
)
  • 属性名验证:query("age"),此时断言只会验证QueryPrameters列表中是否包含了一个叫age的属性,并不会验证它的值;
  • 属性值验证:query("name", "test"),它不仅会验证name属性是否存在,还会验证它的值是不是和断言相匹配,比如当前的断言会验证请求参数中的name属性值是不是test,第二个参数实际上是一个用作模式匹配的正则表达式。

4、Header断言:
这个断言会检查Header中是否包含了响应的属性,通常可以用来验证请求是否携带了访问令牌,比如如下设置:

.route(r -> r.path("/gateway/**")
             .and().header("Authorization")
             .uri("lb://FEIGN-SERVICE-PROVIDER/")
)

上面的断言指定了Header中必须包含一个Authorization属性,Header断言和Query断言一样,也可以通过传入两个参数的形式对属性值进行检查。

5、Cookie断言:
顾名思义,Cookie验证的是Cookie中保存的信息,Cookie断言和上面介绍的两种断言使用方式大同小异,唯一的不同是它必须连同属性值一同验证,不能单独只验证属性是否存在,示例如下:

.route(r -> r.path("/gateway/**")
             .and().cookie("name", "test")
             .uri("lb://FEIGN-SERVICE-PROVIDER/")
)

6、时间片匹配:
时间匹配有三种模式,分别是Before、After和Between,这些断言指定了在什么时间范围内路由才会生效

.route(r -> r.path("/gateway/**")
             .and().before(ZonedDateTime.now().plusMinutes(1))
             .uri("lb://FEIGN-SERVICE-PROVIDER/")
)

以Before断言为例,它接受的是一个ZonedDateTime参数,用来表示生效的时间。比如上面的例子中我们使用了ZonedDateTime.now().plusMinutes(1)表示当前时间的后一分钟,由于路由的规则是在项目启动时加载的,那么这里的当前时间也就是项目加载完成的时间,因此该路由的有效时间就是服务启动后的一分钟以内。

过滤器(filter)的实现方式

在Gateway中实现一个过滤器非常简单,只要实现GatewayFilter接口的默认方法就好了

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    // 随意发挥,例如此处可以写鉴权的逻辑
    return chain.filter(exchange);
}
  • ServerWebExchange:这是Spring封装的HTTP request-response交互协议,从中我们可以获取request和response中的各种请求参数,也可以向其中添加内容。
  • GatewayFilterChain:它是过滤器的调用链,在方法结束的时候我们需要将exchange对象传入调用链中的下一个对象。

过滤器排序:
在Gateway中我们可以通过实现org.springframework.core.Ordered接口,来给过滤器指定执行顺序,比如下面的代码实现了Ordered接口方法,将过滤器执行顺序设置为0:

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

过滤器示例

Gateway的组件库相当丰富,有二十多个过滤器,下面随便展示几个:

1、Header过滤器:
这个系列有很多组过滤器,AddRequestHeader和AddResponseHeader,分别向Request和Response里加入指定Header。相应的RemoveRequestHeader和RemoveResponseHeader分别做移除操作,用法也很简单:

.filters(f -> f.addResponseHeader("who", "gateway-header"))

上面的例子会向header中添加一个who的属性,对应的值是gateway-header。

2、StripPrefix过滤器:
这是个比较常用的过滤器,它的作用是去掉部分URL路径。比如我们的过滤器配置如下:

.route(r -> r.path("/gateway-test/**")
             .filters(f -> f.stripPrefix(1))
             .uri("lb://FEIGN-SERVICE-PROVIDER/")
)

假如HTTP请求访问的是/gateway-test/sample/update,如果没有StripPrefix过滤器,那么转发到FEIGN-SERVICE-PROVIDER服务的访问路径也是一样的。当我们添加了这个过滤器之后,Gateway就会根据“stripPrefix(1)”中的值截取URL中的路径,比如这里我们设置的是1,那么就去掉一个前缀,最终发送给后台服务的路径变成了“/sample/update”。

3、PrefixPath过滤器:
它和StripPrefix的作用是完全相反的,会在请求路径的前面加入前缀

.route(r -> r.path("/gateway-test/**")
             .filters(f -> f.prefixPath("go"))
             .uri("lb://FEIGN-SERVICE-PROVIDER/")
)

比如说我们访问“/gateway-test/sample”的时候,上面例子中配置的过滤器就会把请求发送到“/go/gateway-test/sample”。

4、RedirectTo过滤器:
它可以把收到特定状态码的请求重定向到一个指定网址:

.filters(f -> f.redirect(302, "https://www.imooc.com/"))

上面的例子接收HTTP status code和URL两个参数,如果请求结果是404,则重定向到第二个参数指定的页面,这个功能也可以做统一异常处理,将Unauthorized或Forbidden请求重定向到登录页面。

5、SaveSession过滤器:
我们知道微服务是无状态的会话,所以大多都不依赖session机制,但是如果你有分布式session的需求,比如说某些功能是基于spring-session和spring-security来实现的,那么这个过滤器或许对你有用,它在调用服务之前都会强制保存session

.filters(f -> f.saveSession())

代码示例

pom

<?xml version="1.0" encoding="UTF-8"?>
<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.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>spring-cloud-demo</artifactId>
        <groupId>com.jinsh</groupId>
        <version>0.0.1-SNAPSHOT</version>
        <relativePath>../../pom.xml</relativePath>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>gateway-sample</artifactId>

    <dependencies>
        <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.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
    </dependencies>
</project>

启动类

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:
  application:
    name: gateway-service
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true # 这个设置为true,它就会去注册中心拉取所有服务的路由规则
          lower-case-service-id: true # 路径改为小写
      # yml配置路由规则
      routes:
        - id: feigncClient
          uri: lb://FEIGN-CLIENT  # lb负载均衡,FEIGN-CLIENT服务
          predicates:
            - Path=/yml/** # 断言规则,/yml/**的请求将匹配到FEIGN-CLIENT
          filters:
            - StripPrefix=1 # 去除了第一个路径 /yml/sayHi -> /sayHi

eureka:
  client:
    service-url:
      defaultZone: http://localhost:20000/eureka/

server:
  port: 63001

management:
  endpoints:
    web:
      exposure:
        include: '*'
  endpoint:
    health:
      show-details: always

自定义过滤器实现接口计时功能

import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import org.springframework.util.StopWatch;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

/**
 * 自定义过滤器实现接口计时功能
 */
@Slf4j
@Component
// 实现GlobalFilter接口,就可以成为全局过滤器
public class TimerFilter implements GatewayFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        StopWatch timer = new StopWatch();
        timer.start(exchange.getRequest().getURI().getRawPath());
        return chain.filter(exchange).then(
                Mono.fromRunnable(() -> {
                    timer.stop();
                    log.info(timer.prettyPrint());
                })
        );
    }

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

java代码方式配置路由规则

=import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpMethod;

@Configuration
public class GatewayConfiguration {

    @Autowired
    private TimerFilter timerFilter;

    /**
     * java代码配置路由规则
     * @param builder
     * @return
     */
    @Bean
    @Order
    public RouteLocator customizedRoutes(RouteLocatorBuilder builder) {
        return builder.routes()
                .route(r -> r.path("/java/**")
                        .and().method(HttpMethod.GET)
                        .and().header("name") // header里面需要包含name属性
                        .filters(f -> f.stripPrefix(1)
                                .addRequestParameter("java-param", "gateway-config") // 在Request中添加参数
                                .filter(timerFilter)
                        )
                        .uri("lb://FEIGN-CLIENT")
                ).build();
    }
}

GateWay 限流

这里采用令牌桶计数的方式做限流,下面直接上操作

准备工作:
我们的Best Practice是基于Redis来实现限流,因此要保证本地启动了Redis服务。同时将下列配置加入到Gateway的配置文件中:

spring:
  application:
    name: gateway-service
  redis:
      host: localhost
      port: 6379
      database: 0

这里是配置Redis连接信息的,假如你不配置的话,Gateway也会尝试用默认配置项来连接Redis。但如果你在Redis配置信息中提供了错误的IP或者Port的话,调用方法时依然会成功,不过限流功能就失效了,因为底层的Netty服务无法连接到Redis,也就无法提供限流支持。但Gateway为了保证服务可用性,限流功能的异常并不会阻碍正常的方法调用。

Key Resolver:
Gateway的限流组件要求定义一个Key Resolver用来对每次路由请求生成一个Key,这个Key就是一个限流分组的标识,每个Key相当于一个令牌桶。假如我们限定了一个服务每秒只能被调用3次,这个限制会对不同的Key单独计数,我们把调用方机器的Host Name作为限流Key,那么来自同一台机器的调用将落到同一个Key下面,也就是说在这个场景下,每台机器都独立计算单位时间调用量。
创建Key Resolver的方式很简单:

@Bean
public KeyResolver remoteAddrKeyResolver() {
    return exchange -> Mono.just(
        exchange.getRequest().getRemoteAddress().getHostName());
}

上面的例子创建了基于Host Name的令牌生成器,我们可以根据自己的业务来选择合适的Key,比如说可以在接口层面做限流(使用接口的Path作为Key),还可以从Request中提取业务字段作为Key(比如用户ID等)。

配置文件方式配置过滤器:

spring:
  cloud:
    gateway:
      routes:
      - id:  feignapi
        uri: lb://FEIGN-SERVICE-PROVIDER
        predicates:
        - Path=/feign-api/**
        filters:
        - StripPrefix=1
        - name: RequestRateLimiter
          args:
            key-resolver: '#{@remoteAddrKeyResolver}'  # 这里注入的就是在上一步中我们定义的Key Resolver,它使用SpEL表达式从Spring上下文中获取指定Bean
            redis-rate-limiter.replenishRate: 10  # 令牌桶每秒的平均填充速度
            redis-rate-limiter.burstCapacity: 20  # 令牌桶总量

java代码方式配置过滤器:

package com.jinsh;

import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.cloud.gateway.filter.ratelimit.RedisRateLimiter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import reactor.core.publisher.Mono;

/**
 * 限流配置
 */
@Configuration
public class RedisLimitConfiguration {

    // HostAddressKey
    @Bean
    @Primary
    public KeyResolver remoteAddressKeyResolver() {
        return exchange -> Mono.just(
            exchange.getRequest()
                .getRemoteAddress()
                .getAddress()
                .getHostAddress()
        );
    }

    @Bean("redisLimiter")
    @Primary
    public RedisRateLimiter redisLimiter() {
        // 10:令牌桶每秒的平均填充速度,20:令牌桶总量
        return new RedisRateLimiter(10, 20);
    }
}

路由规则

@Configuration
public class GatewayConfiguration {

    @Autowired
    private KeyResolver hostNameResolver;

    @Autowired
    @Qualifier("redisLimiter")
    private RateLimiter rateLimiter;

    /**
     * java代码配置路由规则
     * @param builder
     * @return
     */
    @Bean
    @Order
    public RouteLocator customizedRoutes(RouteLocatorBuilder builder) {
        return builder.routes()
                .route(r -> r.path("/java/**")
                        .filters(f -> f.requestRateLimiter(c -> {
                            c.setKeyResolver(hostNameResolver);
                            c.setRateLimiter(rateLimiter);
                        }))
                        .uri("lb://FEIGN-CLIENT")
                ).build();
    }
}

标签:断言,Spring,springframework,gateway,import,过滤器,org,Gateway,Cloud
来源: https://www.cnblogs.com/jinshengnianhua/p/15346030.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有