网关
网关是所有微服务的统一入口。网关的核心功能特性:
- 请求路由:一切请求都必须先经过网关,但网关不处理业务,而是根据某种规则,把请求转发到某个微服务,这个过程叫做路由。当路由的目标服务有多个时,还需要做负载均衡。
- 权限控制:网关作为微服务入口,需要校验用户是是否有请求资格,如果没有则进行拦截。
- 限流:当请求流量过高时,在网关中按照下流的微服务能够接受的速度来放行请求,避免服务压力过大。
在SpringCloud中网关的实现包括两种:
- Gateway:基于Spring5中提供的WebFlux,属于响应式编程的实现,具备更好的性能。
- Zuul:基于Servlet的实现,属于阻塞式编程。
Spring Cloud Gateway旨在为微服务架构提供一种简单有效的统一的API路由管理方式。
创建 Gateway 服务
创建一个Gateway服务的基本步骤如下:
-
创建一个新的gateway模块。
-
导入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>
-
因为Gateway属于一个服务,所以需要创建并编写
GatewayApplication
启动类:@SpringBootApplication public class GatewayApplication { public static void main(String[] args) { SpringApplication.run(GatewayApplication.class, args); } }
-
在配置文件
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]
-
启动
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的路由配置如上所示:
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的网关路由配置,数组类型。其元素可以有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
中配置:
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的官方文档中可以查找更多路由工厂及其使用方式:
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的过程可总结为:
-
继承
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>
对象。放行资源时标准的用法是:return chain.filter(exchange);
拦截资源使用的是从
exchange
中获取的response
对象。调用response
对象的setComplete()
方法,返回给上层Filter一个Mono<Void>
。标准用法如下:return 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()
方法:@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执行流程图,确实是在请求路由之后才开始组装过滤器链:
路由过滤器和默认过滤器的实现十分接近,它们的本质都是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加载过滤器链的过程大致如下:
-
加载默认过滤器链。
-
加载路由过滤器链。
-
合并默认过滤器链和路由过滤器链为一条过滤器链。
路由过滤器和默认过滤器的
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
即可:
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 # 每次跨域检测的有效期(在有效期内浏览器不会重复询问跨域请求)
模拟一个跨域问题:
-
编写一个简单的页面,其中用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>
-
使用Tomcat或Nginx之类的Web服务器放置这个Html页面。
我在模拟时使用的是VS Code的
Live Server
插件。它会想Web服务器一样在你电脑上开一个端口加载页面,让你能实时预览你的页面效果。用在这里做个简单的静态页面Web服务器也很方便。Live Server使用的端口是
5500
,当然也有可能不同。 -
在给Gateway网关配置跨域请求处理之前,通过Web服务器访问页面,可以在浏览器控制台发现类似以下的报错:
-
配置成功后重启Gateway网关,再次访问页面,可以发现浏览器控制台打印出了跨域请求获取到的结果。
评论