网关

网关是所有微服务的统一入口。网关的核心功能特性:

  • 请求路由:一切请求都必须先经过网关,但网关不处理业务,而是根据某种规则,把请求转发到某个微服务,这个过程叫做路由。当路由的目标服务有多个时,还需要做负载均衡
  • 权限控制:网关作为微服务入口,需要校验用户是是否有请求资格,如果没有则进行拦截。
  • 限流:当请求流量过高时,在网关中按照下流的微服务能够接受的速度来放行请求,避免服务压力过大。

在SpringCloud中网关的实现包括两种:

  • Gateway:基于Spring5中提供的WebFlux,属于响应式编程的实现,具备更好的性能。
  • Zuul:基于Servlet的实现,属于阻塞式编程。

Spring Cloud Gateway旨在为微服务架构提供一种简单有效的统一的API路由管理方式。


创建 Gateway 服务

创建一个Gateway服务的基本步骤如下:

  1. 创建一个新的gateway模块。

  2. 导入Gateway所需依赖:

     1<!-- Nacos服务注册发现依赖 -->
     2<dependency>
     3    <groupId>com.alibaba.cloud</groupId>
     4    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
     5</dependency>
     6<!-- 网关Gateway依赖 -->
     7<dependency>
     8    <groupId>org.springframework.cloud</groupId>
     9    <artifactId>spring-cloud-starter-gateway</artifactId>
    10</dependency>
    
  3. 因为Gateway属于一个服务,所以需要创建并编写GatewayApplication启动类:

    1@SpringBootApplication
    2public class GatewayApplication {
    3    public static void main(String[] args) {
    4        SpringApplication.run(GatewayApplication.class, args);
    5    }
    6}
    
  4. 在配置文件application.yml中编写Gateway相关配置:

     1server:
     2  port: 10010 # 网关端口
     3spring:
     4  application:
     5    name: gateway # 服务名称
     6cloud:
     7  nacos:  # Nacos配置
     8    server-addr: localhost:8848
     9  gateway:
    10    routes:   # 网关路由配置(是数组类型,可以配置多个)
    11      - id: user-service  # 路由ID,自定义,只要唯一即可
    12        # uri支持以下两种方式,推荐使用lb方式
    13        # uri: http://localhost:8081  # 路由的目标地址,使用http表示固定地址(不推荐使用)
    14        uri: lb://user-service  # 路由的目标地址
    15                                # lb是Load Balance的缩写,表示负载均衡
    16                                # 后面是服务地址
    17        predicates: # 路由断言,判断请求是否符合路由规则的条件
    18          - Path=/user/** # 路径断言,匹配"/user/"开头的请求
    19      - id: order-service
    20        uri: lb://${spring.cloud.gateway.routes[1].id}  # 通过yaml变量设置uri
    21        predicates:
    22          - Path=/order/**
    23          - Before=2037-01-20T17:42:47.789-07:00[America/Denver]
    
  5. 启动GatewayApplication,使用localhost:10010来访问系统上的服务。

    例如访问user-service上的/user/{id}请求,就可以使用http://localhost:10010/user/{id}访问。获取id1user就访问http://localhost:10010/user/1

    如上所述,访问order-service上的/order/{id}就使用http://localhost:10010/order/{id}访问。

使用网关就可以通过网关来访问服务中的资源,并且还能做到负载均衡和权限控制等。


路由配置

Gateway的路由配置如上所示:

 1cloud:
 2  gateway:
 3    routes:   # 网关路由配置(是数组类型,可以配置多个)
 4      - id: user-service  # 路由ID,自定义,只要唯一即可
 5        # uri支持以下两种方式,推荐使用lb方式
 6        # uri: http://localhost:8081  # 路由的目标地址,使用http表示固定地址(不推荐使用)
 7        uri: lb://user-service  # 路由的目标地址
 8                                # lb是Load Balance的缩写,表示负载均衡
 9                                # 后面是服务地址
10        predicates: # 路由断言,判断请求是否符合路由规则的条件
11          - Path=/user/** # 路径断言,匹配"/user/"开头的请求
12      - id: order-service
13        uri: lb://${spring.cloud.gateway.routes[1].id}  # 通过yaml变量设置uri
14        predicates:
15          - Path=/order/**
16          - Before=2037-01-20T17:42:47.789-07:00[America/Denver]
  • cloud.gateway.routes:Gateway的网关路由配置,数组类型。其元素可以有iduripredicates等属性。

  • id属性:标识一个服务的路由配置的唯一ID。可由用户自定义,但在当前Gateway网关服务中不可重复存在。

  • uri属性:标识当前服务路由配置的目标地址。有两种配置方式:

    • http:使用http://前缀,表示当前的地址是固定地址。例如http://localhost:8081

    • lb:使用lb://前缀,表示当前的地址是非固定的,需要做负载均衡。例如:lb://user-service

      lb是Load Balance的缩写,表示负载均衡。

  • predicates属性:路由断言,根据Gateway提供的断言工厂,对经过网关的请求进行权限的断言(也就是判断有没有权限可以访问该服务)。predicates是数组属性,可以配置多个规则。

断言工厂

在配置文件中写的断言规则,会被Predicate Factory读取并处理,转变为路由判断的条件。

例如上方Path=/user/**就是按照路径匹配,断言只有/user/**这个请求方式才能通过网关访问到user-serviceorder-service中的Path=/order/**同理。这两条规则是由org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory类来处理的。

在Gateway中还有以下断言工厂:

名称 说明 示例
After 是某个时间点后的请求。 After=2037-01-20T17:42:47.789-07:00[America/Denver]
Before 是某个时间点之前的请求。 Before=2031-04-13T15:14:47.433+08:00[Asia/Shanghai]
Between 是某两个时间点之前的请求。 Between=2037-01-20T17:42:47.789-07:00[America/Denver], 2037-01-21T17:42:47.789-07:00[America/Denver]
Cookie 请求必须包含某些cookie。 Cookie=chocolate, ch.p
Header 请求必须包含某些header。 Header=X-Request-Id, \d+
Host 请求必须是访问某个host(域名)。 Host=**.somehost.org,**.anotherhost.org
Method 请求方式必须是指定方式。 Method=GET,POST
Path 请求路径必须符合指定规则。 Path=/red/{segment},/blue/**
Query 请求参数必须包含指定参数。 Query=name, Jack
或者Query=name
RemoteAddr 请求者的ip必须是指定范围。 RemoteAddr=192.168.1.1/24
Weight 权重处理。

路由过滤器

GatewayFilter是网关中提供的一种过滤器,可以对进入网关的请求和微服务返回的响应做处理。

Spring提供了31种不同的路由过滤器工厂。常用的有:

名称 说明
AddRequestHeader 给当前请求添加一个请求头。
RemoveRequestHeader 移除请求中的一个请求头。
AddResponseHeader 给响应结果中添加一个响应头。
RemoveResponseHeader 从响应结果中移除有一个响应头。
RequestRateLimiter 限制请求的流量。

GatewayFilter的使用也是在application.yml中配置:

 1spring:
 2  cloud:
 3    gateway:
 4      routes:
 5      - id: user-service 
 6        uri: lb://user-service 
 7        predicates: 
 8        - Path=/user/** 
 9        filters: # 过滤器
10        - AddRequestHeader=Hello, Hello Spring Cloud Gateway! # 添加请求头

DefaultFilter

上方所示的GatewayFilter只有在访问user-service时才能生效。Spring Cloud Gateway还提供了全局默认的GatewayFilter配置方式:

 1spring:
 2  cloud:
 3    gateway:
 4      routes:
 5      - id: user-service 
 6        uri: lb://userservice 
 7        predicates: 
 8        - Path=/user/**
 9      - id: order-service 
10        uri: lb://order-service 
11        predicates: 
12        - Path=/order/**
13      default-filters: # 默认过滤项
14      - AddRequestHeader=Truth, Itcast is freaking awesome! 

这种方式无论访问的是user-service还是order-service都会生效。

在Spring Cloud Gateway的官方文档中可以查找更多路由工厂及其使用方式:

Spring Cloud Gateway 官方文档

GlobalFilter

GlobalFilter的作用与GatewayFilter的作用一样,也是处理一切进入网关的请求和微服务响应。区别在于GatewayFilter通过配置定义,处理逻辑是固定的;而GlobalFilter的逻辑需要自己写代码实现,可以自定义实现。

GlobalFilter接口:

 1public interface GlobalFilter {
 2
 3	/**
 4	 * 处理当前请求,有必要的话通过{@link GatewayFilterChain}将请求交给下一个过滤器处理
 5	 * @param exchange 请求上下文,里面可以获取Request、Responses等信息
 6	 * @param chain 用来把请求委托给下一个过滤器(放行请求)
 7	 * @return {@code Mono<Void>} 返回一个当前过滤器业务结束的标示
 8	 */
 9	Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);
10}

注:GlobalFilter是在org.springframework.cloud.gateway.filter包下的,是属于Spring Cloud Gateway中的一部分。

在Filter中编写自定义逻辑,可以实现登录状态判断、权限校验、请求限流等等功能。

假设实现一个简单的用户权限判断,其判断逻辑如下:

  • 请求参数中是否有authorization
  • authorization参数值是否为admin

如果同时满足则放行,否则拦截。

 1/**
 2 * 识别用户权限
 3 */
 4@Order(-1)   // 顺序注解(定义过滤器的执行顺序),值越小优先级越高
 5@Component
 6public class AuthorizeFilter implements GlobalFilter {
 7
 8    @Override
 9    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
10
11        // 1. 获取请求参数
12        ServerHttpRequest request = exchange.getRequest();
13        MultiValueMap<String, String> params = request.getQueryParams();
14        // 2. 获取参数中的 authorization
15        String auth = params.getFirst("authorization");
16
17        // 3. 判断参数值是否等于 admin
18        if ("admin".equals(auth)) {
19            // 4. 是则放行
20            // chain.filter()的返回值是Mono<Void>
21            return chain.filter(exchange);
22        }
23
24        // 5. 否则拦截
25        ServerHttpResponse response = exchange.getResponse();
26        // 5.1. 设置状态码
27        // HttpStatus.UNAUTHORIZED 表示用户未认证,状态码为401
28        response.setStatusCode(HttpStatus.UNAUTHORIZED);
29        // 5.2. 拦截请求
30        // setComplete()的返回也是Mono<Void>
31        return response.setComplete();
32    }
33}

定义GlobalFilter的过程可总结为:

  1. 继承GlobalFilter接口。

  2. 实现GlobalFilter.filter()方法。

    在实现GlobalFilter.filter()时,可以使用exchange对象获取请求的RequestResponseAttributeSessionFormData等信息。其中获取到的RequestResponse分别是ServerHttpRequestServerHttpResponse对象。

    使用exchange获取到的对象与使用标准的ServletAPI获取到的不一样。

    放行资源使用的是chain对象。该对象仅有一个方法filter()。该方法接受一个ServerWebExchange对象(也就是exchange,相当于将exchange传给下一级Filter),并返回给上层Filter一个Mono<Void>对象。放行资源时标准的用法是:

    1return chain.filter(exchange);
    

    拦截资源使用的是从exchange中获取的response对象。调用response对象的setComplete()方法,返回给上层Filter一个Mono<Void>。标准用法如下:

    1return response.setComplete();
    

    这样相当于直接将业务结束标示Mono<Void>返回给上层Filter。而没有调用chain.filter(exchange)的话,请求也就不会进入到下层Filter。

  3. 为实现的GlobalFilter使用@Component注解,让Spring可以将该过滤器加载为Bean。

  4. 为实现的GlobalFilter定义顺序(有两种定义方式)。

    定义GlobalFilter执行的优先级顺序的一个方法就是使用如上所示的@Order注解。在@Order注解中,其value属性是一个int类型的值,默认为Integer.MAX_VALUE也就是int类型的最大值2147483647(即$2^{31}-1$,按32位补码计算),value越小优先级越高。

    另一种方式就是继承一个Ordered接口,并实现其getOrder()方法:

     1@Component
     2public class AuthorizeFilter implements GlobalFilter, Ordered {
     3
     4    @Override
     5    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
     6        /* 拦截/放行逻辑... */
     7    }
     8
     9    /**
    10     * 定义过滤器执行顺序,效果与{@code @Order}相同
    11     * @return 过滤器的执行顺序,值越小优先级越高
    12     */
    13    @Override
    14    public int getOrder() {
    15        return -1;
    16    }
    17}
    

过滤器链

请求进入网关后会遇到三类过滤器:

  • 当前路由的过滤器;
  • 默认过滤器(DefaultFilter);
  • 全局过滤器(GlobalFilter)。

在请求路由后,Spring Cloud Gateway会将每个路由的路由过滤器、默认过滤器和全局过滤器合并到一个过滤器链(集合)中,并进行排序。然后Spring Cloud Gateway会根据请求,按顺序执行路由对应的过滤器链。

在CSDN上看到一个Gateway执行流程图,确实是在请求路由之后才开始组装过滤器链:

Gateway执行流程图

原文链接:Spring Cloud Gateway 源码剖析之Filter Chain过滤器链

路由过滤器和默认过滤器的实现十分接近,它们的本质都是AddRequestHeaderGatewayFilterFactory,并且最后通过apply()方法读取配置后生成统一的过滤器对象GatewayFilter

 1public class AddRequestHeaderGatewayFilterFactory
 2		extends AbstractNameValueGatewayFilterFactory {
 3
 4	@Override
 5	public GatewayFilter apply(NameValueConfig config) {
 6        return new GatewayFilter() {    // 生成过滤器对象
 7			@Override
 8			public Mono<Void> filter(ServerWebExchange exchange,
 9					GatewayFilterChain chain) {
10				/* ... */
11			}
12
13			@Override
14			public String toString() {
15				/* ... */
16			}
17		};
18    }
19}

全局过滤器则是通过FilteringWebHandler中的私有类GatewayFilterAdapter(过滤器适配器)生成,使用loadFilters()将所有GlobalFilter转化为GatewayFilterAdapter

 1public class FilteringWebHandler implements WebHandler {
 2
 3    private final List<GatewayFilter> globalFilters;
 4
 5    public FilteringWebHandler(List<GlobalFilter> globalFilters) {
 6		this.globalFilters = loadFilters(globalFilters);
 7	}
 8
 9    /**
10     * 将所有GlobalFilter链转为GatewayFilter
11     */
12	private static List<GatewayFilter> loadFilters(List<GlobalFilter> filters) {
13		return filters.stream().map(filter -> {
14			GatewayFilterAdapter gatewayFilter = new GatewayFilterAdapter(filter);
15			if (filter instanceof Ordered) {
16				int order = ((Ordered) filter).getOrder();
17				return new OrderedGatewayFilter(gatewayFilter, order);
18			}
19			return gatewayFilter;
20		}).collect(Collectors.toList());
21	}
22
23    /**
24     * 加载全局过滤器,与所有的路由过滤器和默认过滤器合并后更具Order排序、组织过滤器链
25     */
26    @Override
27	public Mono<Void> handle(ServerWebExchange exchange) {
28		Route route = exchange.getRequiredAttribute(GATEWAY_ROUTE_ATTR);
29        // 加载所有的默认过滤器和路由过滤器
30        // 加载方式是,先加载默认过滤器链,然后根据Route(规则)加载路由过滤器链,最后合并为一个过滤器链
31		List<GatewayFilter> gatewayFilters = route.getFilters();
32
33		List<GatewayFilter> combined = new ArrayList<>(this.globalFilters);
34		combined.addAll(gatewayFilters);
35		// TODO: needed or cached?
36		AnnotationAwareOrderComparator.sort(combined);
37
38		if (logger.isDebugEnabled()) {
39			logger.debug("Sorted gatewayFilterFactories: " + combined);
40		}
41
42		return new DefaultGatewayFilterChain(combined).filter(exchange);
43	}
44
45    private static class GatewayFilterAdapter implements GatewayFilter {
46
47        private final GlobalFilter delegate;
48
49        /**
50         * 私有类构造方法
51         */
52        GatewayFilterAdapter(GlobalFilter delegate) {
53            this.delegate = delegate;
54        }
55
56        /**
57         * 实现GatewayFilter的filter()方法
58         */
59        @Override
60        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
61            return this.delegate.filter(exchange, chain);
62        }
63
64        @Override
65        public String toString() { /* ... */ }
66    }
67
68    /* ... */
69}

综上所述,Spring Cloud Gateway加载过滤器链的过程大致如下:

  1. 加载默认过滤器链。

  2. 加载路由过滤器链。

  3. 合并默认过滤器链和路由过滤器链为一条过滤器链。

    路由过滤器和默认过滤器的Order由Spring指定,默认是按照声明顺序从1递增。

  4. 加载全局过滤器链。

  5. 将全局过滤器链和 默认过滤器与路由过滤器合并的链 合并。

    所有的过滤器都有一个int类型的Order值,Order值越小,优先级越高,执行顺序越靠前。

    当过滤器的Order值一样时,会按照 默认过滤器 > 路由过滤器 > 全局过滤器 的顺序执行。

跨域问题处理

跨域是指发送与当前服务的域名(或端口、协议)不一致的请求。

跨域问题的产生原因是浏览器不允许Ajax请求对域名不同或端口不同的服务发起请求。例如:

  • 域名不同: www.taobao.comwww.taobao.orgwww.linner.asiablog.linner.asia
  • 域名相同,端口不同:localhost:8080localhost8081

解决方案之一就是CORS(JSONP只支持GET请求,不推荐)。Gateway为我们提供了使用CORS处理跨域问题的方法,只需修改application.yml即可:

 1spring:
 2  cloud:
 3    gateway:
 4      globalcors: # 全局的跨域处理
 5        add-to-simple-url-handler-mapping: true # 解决options请求被拦截问题
 6        cors-configurations:
 7          '[/**]':  # 对所有请求进行跨域处理
 8            allowedOrigins: # 允许跨域请求的网站
 9              - "http://localhost:5500"
10              - "http://http://127.0.0.1:5500"
11            allowedMethods: # 允许的跨域ajax的请求方式
12              - "GET"
13              - "POST"
14              - "DELETE"
15              - "PUT"
16              - "OPTIONS"
17            allowedHeaders: "*" # 允许在请求中携带的头信息(这里是允许所有)
18            allowCredentials: true  # 是否允许携带cookie
19            maxAge: 360000  # 每次跨域检测的有效期(在有效期内浏览器不会重复询问跨域请求)

模拟一个跨域问题:

  1. 编写一个简单的页面,其中用Ajax发起跨域请求:

     1<!DOCTYPE html>
     2<html lang="en">
     3<head>
     4    <meta charset="UTF-8">
     5    <meta name="viewport" content="width=device-width, initial-scale=1.0">
     6    <meta http-equiv="X-UA-Compatible" content="ie=edge">
     7    <title>Document</title>
     8</head>
     9<body>
    10    <h1>模拟跨域问题</h1>
    11</body>
    12<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    13<script>
    14    // 发送请求到Gateway网关上
    15    axios.get("http://localhost:10010/user/1?authorization=admin")
    16        .then(resp => console.log(resp.data))
    17        .catch(err => console.log(err))
    18</script>
    19</html>
    
  2. 使用Tomcat或Nginx之类的Web服务器放置这个Html页面。

    我在模拟时使用的是VS Code的Live Server插件。它会想Web服务器一样在你电脑上开一个端口加载页面,让你能实时预览你的页面效果。用在这里做个简单的静态页面Web服务器也很方便。

    Live Server 插件

    Live Server使用的端口是5500,当然也有可能不同。

  3. 在给Gateway网关配置跨域请求处理之前,通过Web服务器访问页面,可以在浏览器控制台发现类似以下的报错:

    跨域报错信息

  4. 配置成功后重启Gateway网关,再次访问页面,可以发现浏览器控制台打印出了跨域请求获取到的结果。