网关
网关是所有微服务的统一入口。网关的核心功能特性:
- 请求路由:一切请求都必须先经过网关,但网关不处理业务,而是根据某种规则,把请求转发到某个微服务,这个过程叫做路由。当路由的目标服务有多个时,还需要做负载均衡。
- 权限控制:网关作为微服务入口,需要校验用户是是否有请求资格,如果没有则进行拦截。
- 限流:当请求流量过高时,在网关中按照下流的微服务能够接受的速度来放行请求,避免服务压力过大。
在SpringCloud中网关的实现包括两种:
- Gateway:基于Spring5中提供的WebFlux,属于响应式编程的实现,具备更好的性能。
- Zuul:基于Servlet的实现,属于阻塞式编程。
Spring Cloud Gateway旨在为微服务架构提供一种简单有效的统一的API路由管理方式。
创建 Gateway 服务
创建一个Gateway服务的基本步骤如下:
-
创建一个新的gateway模块。
-
导入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> -
因为Gateway属于一个服务,所以需要创建并编写
GatewayApplication启动类:1@SpringBootApplication 2public class GatewayApplication { 3 public static void main(String[] args) { 4 SpringApplication.run(GatewayApplication.class, args); 5 } 6} -
在配置文件
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] -
启动
GatewayApplication,使用localhost:10010来访问系统上的服务。例如访问
user-service上的/user/{id}请求,就可以使用http://localhost:10010/user/{id}访问。获取id为1的user就访问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的网关路由配置,数组类型。其元素可以有id、uri和predicates等属性。 -
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-service。order-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的官方文档中可以查找更多路由工厂及其使用方式:
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的过程可总结为:
-
继承
GlobalFilter接口。 -
实现
GlobalFilter.filter()方法。在实现
GlobalFilter.filter()时,可以使用exchange对象获取请求的Request、Response、Attribute、Session、FormData等信息。其中获取到的Request和Response分别是ServerHttpRequest和ServerHttpResponse对象。使用
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。 -
为实现的
GlobalFilter使用@Component注解,让Spring可以将该过滤器加载为Bean。 -
为实现的
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执行流程图,确实是在请求路由之后才开始组装过滤器链:
![]()
路由过滤器和默认过滤器的实现十分接近,它们的本质都是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加载过滤器链的过程大致如下:
-
加载默认过滤器链。
-
加载路由过滤器链。
-
合并默认过滤器链和路由过滤器链为一条过滤器链。
路由过滤器和默认过滤器的
Order由Spring指定,默认是按照声明顺序从1递增。 -
加载全局过滤器链。
-
将全局过滤器链和 默认过滤器与路由过滤器合并的链 合并。
所有的过滤器都有一个
int类型的Order值,Order值越小,优先级越高,执行顺序越靠前。当过滤器的
Order值一样时,会按照 默认过滤器 > 路由过滤器 > 全局过滤器 的顺序执行。
跨域问题处理
跨域是指发送与当前服务的域名(或端口、协议)不一致的请求。
跨域问题的产生原因是浏览器不允许Ajax请求对域名不同或端口不同的服务发起请求。例如:
- 域名不同:
www.taobao.com和www.taobao.org,www.linner.asia和blog.linner.asia。 - 域名相同,端口不同:
localhost:8080和localhost8081。
解决方案之一就是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 # 每次跨域检测的有效期(在有效期内浏览器不会重复询问跨域请求)
模拟一个跨域问题:
-
编写一个简单的页面,其中用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> -
使用Tomcat或Nginx之类的Web服务器放置这个Html页面。
我在模拟时使用的是VS Code的
Live Server插件。它会想Web服务器一样在你电脑上开一个端口加载页面,让你能实时预览你的页面效果。用在这里做个简单的静态页面Web服务器也很方便。
Live Server使用的端口是
5500,当然也有可能不同。 -
在给Gateway网关配置跨域请求处理之前,通过Web服务器访问页面,可以在浏览器控制台发现类似以下的报错:
-
配置成功后重启Gateway网关,再次访问页面,可以发现浏览器控制台打印出了跨域请求获取到的结果。
评论