网关

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

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

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

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

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


创建 Gateway 服务

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

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

  2. 导入Gateway所需依赖:

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

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

    server:
      port: 10010 # 网关端口
    spring:
      application:
        name: gateway # 服务名称
    cloud:
      nacos:  # Nacos配置
        server-addr: localhost:8848
      gateway:
        routes:   # 网关路由配置(是数组类型,可以配置多个)
          - id: user-service  # 路由ID,自定义,只要唯一即可
            # uri支持以下两种方式,推荐使用lb方式
            # uri: http://localhost:8081  # 路由的目标地址,使用http表示固定地址(不推荐使用)
            uri: lb://user-service  # 路由的目标地址
                                    # lb是Load Balance的缩写,表示负载均衡
                                    # 后面是服务地址
            predicates: # 路由断言,判断请求是否符合路由规则的条件
              - Path=/user/** # 路径断言,匹配"/user/"开头的请求
          - id: order-service
            uri: lb://${spring.cloud.gateway.routes[1].id}  # 通过yaml变量设置uri
            predicates:
              - Path=/order/**
              - 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的路由配置如上所示:

cloud:
  gateway:
    routes:   # 网关路由配置(是数组类型,可以配置多个)
      - id: user-service  # 路由ID,自定义,只要唯一即可
        # uri支持以下两种方式,推荐使用lb方式
        # uri: http://localhost:8081  # 路由的目标地址,使用http表示固定地址(不推荐使用)
        uri: lb://user-service  # 路由的目标地址
                                # lb是Load Balance的缩写,表示负载均衡
                                # 后面是服务地址
        predicates: # 路由断言,判断请求是否符合路由规则的条件
          - Path=/user/** # 路径断言,匹配"/user/"开头的请求
      - id: order-service
        uri: lb://${spring.cloud.gateway.routes[1].id}  # 通过yaml变量设置uri
        predicates:
          - Path=/order/**
          - 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中配置:

spring:
  cloud:
    gateway:
      routes:
      - id: user-service 
        uri: lb://user-service 
        predicates: 
        - Path=/user/** 
        filters: # 过滤器
        - AddRequestHeader=Hello, Hello Spring Cloud Gateway! # 添加请求头

DefaultFilter

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

spring:
  cloud:
    gateway:
      routes:
      - id: user-service 
        uri: lb://userservice 
        predicates: 
        - Path=/user/**
      - id: order-service 
        uri: lb://order-service 
        predicates: 
        - Path=/order/**
      default-filters: # 默认过滤项
      - AddRequestHeader=Truth, Itcast is freaking awesome! 

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

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

Spring Cloud Gateway 官方文档

GlobalFilter

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

GlobalFilter接口:

public interface GlobalFilter {

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

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

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

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

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

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

/**
 * 识别用户权限
 */
@Order(-1)   // 顺序注解(定义过滤器的执行顺序),值越小优先级越高
@Component
public class AuthorizeFilter implements GlobalFilter {

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

        // 1. 获取请求参数
        ServerHttpRequest request = exchange.getRequest();
        MultiValueMap<String, String> params = request.getQueryParams();
        // 2. 获取参数中的 authorization
        String auth = params.getFirst("authorization");

        // 3. 判断参数值是否等于 admin
        if ("admin".equals(auth)) {
            // 4. 是则放行
            // chain.filter()的返回值是Mono<Void>
            return chain.filter(exchange);
        }

        // 5. 否则拦截
        ServerHttpResponse response = exchange.getResponse();
        // 5.1. 设置状态码
        // HttpStatus.UNAUTHORIZED 表示用户未认证,状态码为401
        response.setStatusCode(HttpStatus.UNAUTHORIZED);
        // 5.2. 拦截请求
        // setComplete()的返回也是Mono<Void>
        return response.setComplete();
    }
}

定义GlobalFilter的过程可总结为:

  1. 继承GlobalFilter接口。

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

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

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

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

    return chain.filter(exchange);
    

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

    return 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()方法:

    @Component
    public class AuthorizeFilter implements GlobalFilter, Ordered {
    
        @Override
        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
            /* 拦截/放行逻辑... */
        }
    
        /**
         * 定义过滤器执行顺序,效果与{@code @Order}相同
         * @return 过滤器的执行顺序,值越小优先级越高
         */
        @Override
        public int getOrder() {
            return -1;
        }
    }
    

过滤器链

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

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

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

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

Gateway执行流程图

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

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

public class AddRequestHeaderGatewayFilterFactory
		extends AbstractNameValueGatewayFilterFactory {

	@Override
	public GatewayFilter apply(NameValueConfig config) {
        return new GatewayFilter() {    // 生成过滤器对象
			@Override
			public Mono<Void> filter(ServerWebExchange exchange,
					GatewayFilterChain chain) {
				/* ... */
			}

			@Override
			public String toString() {
				/* ... */
			}
		};
    }
}

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

public class FilteringWebHandler implements WebHandler {

    private final List<GatewayFilter> globalFilters;

    public FilteringWebHandler(List<GlobalFilter> globalFilters) {
		this.globalFilters = loadFilters(globalFilters);
	}

    /**
     * 将所有GlobalFilter链转为GatewayFilter
     */
	private static List<GatewayFilter> loadFilters(List<GlobalFilter> filters) {
		return filters.stream().map(filter -> {
			GatewayFilterAdapter gatewayFilter = new GatewayFilterAdapter(filter);
			if (filter instanceof Ordered) {
				int order = ((Ordered) filter).getOrder();
				return new OrderedGatewayFilter(gatewayFilter, order);
			}
			return gatewayFilter;
		}).collect(Collectors.toList());
	}

    /**
     * 加载全局过滤器,与所有的路由过滤器和默认过滤器合并后更具Order排序、组织过滤器链
     */
    @Override
	public Mono<Void> handle(ServerWebExchange exchange) {
		Route route = exchange.getRequiredAttribute(GATEWAY_ROUTE_ATTR);
        // 加载所有的默认过滤器和路由过滤器
        // 加载方式是,先加载默认过滤器链,然后根据Route(规则)加载路由过滤器链,最后合并为一个过滤器链
		List<GatewayFilter> gatewayFilters = route.getFilters();

		List<GatewayFilter> combined = new ArrayList<>(this.globalFilters);
		combined.addAll(gatewayFilters);
		// TODO: needed or cached?
		AnnotationAwareOrderComparator.sort(combined);

		if (logger.isDebugEnabled()) {
			logger.debug("Sorted gatewayFilterFactories: " + combined);
		}

		return new DefaultGatewayFilterChain(combined).filter(exchange);
	}

    private static class GatewayFilterAdapter implements GatewayFilter {

        private final GlobalFilter delegate;

        /**
         * 私有类构造方法
         */
        GatewayFilterAdapter(GlobalFilter delegate) {
            this.delegate = delegate;
        }

        /**
         * 实现GatewayFilter的filter()方法
         */
        @Override
        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
            return this.delegate.filter(exchange, chain);
        }

        @Override
        public String toString() { /* ... */ }
    }

    /* ... */
}

综上所述,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即可:

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

模拟一个跨域问题:

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

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

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

    Live Server 插件

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

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

    跨域报错信息

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