微服务网关,为什么需要API网关?网关的工作流程,断言、动态路由、过滤器、token认证。
1、为什么需要API网关?
在 SpringCloud 微服务架构中,往往有多个微服务,这些微服务可能部署在不同的机器上,而且一个微服务可能会扩容成多个相同的微服务,组成微服务集群。
这种情况下,会存在如下问题:
- 如果需要添加鉴权功能,则需要对每个微服务进行改造。
- 如果需要对流量进行控制,则需要对每个微服务进行改造。
- 跨域问题,需要对每个微服务进行改造。
- 存在安全问题,每个微服务需要暴露自己的 Endpoint 给客户端。
Endpoint 就是服务的访问地址 + 端口,比如下面的地址:
- 灰度发布、动态路由需要对每个微服务进行改造。
这个问题的痛点是各个微服务都是一个入口,有没有办法统一入口呢?
解决这个问题的方式就是在客户端和服务器之间加个中间商就好了呀,只有中间商一个入口,这个中间商就是网关。
2、工作流程
Gateway 的工作流程如下图所示:
① 路由判断;客户端的请求到达网关后,先经过 Gateway Handler Mapping 处理,这里面会做断言(Predicate)判断,看下符合哪个路由规则,这个路由映射后端的某个服务。
② 请求过滤:然后请求到达 Gateway Web Handler,这里面有很多过滤器,组成过滤器链(Filter Chain),这些过滤器可以对请求进行拦截和修改,比如添加请求头、参数校验等等,有点像净化污水。然后将请求转发到实际的后端服务。这些过滤器逻辑上可以称作 Pre-Filters,Pre 可以理解为“在…之前”。
③ 服务处理:后端服务会对请求进行处理。
④ 响应过滤: 后端处理完结果后,返回给 Gateway 的过滤器再次做处理,逻辑上可以称作 Post-Filters,Post 可以理解为“在…之后”。
⑤ 响应返回:响应经过过滤处理后,返回给客户端。
小结:客户端的请求先通过匹配规则找到合适的路由,就能映射到具体的服务。然后请求经过过滤器处理后转发给具体的服务,服务处理后,再次经过过滤器处理,最后返回给客户端。
3、断言
断言(Predicate)这个词听起来极其深奥,它是一种编程术语,我们生活中根本就不会用它。说白了它就是对一个表达式进行 if 判断,结果为真或假,如果为真则做这件事,否则做那件事。
在 Gateway 中,如果客户端发送的请求满足了断言的条件,则映射到指定的路由器,就能转发到指定的服务上进行处理。
断言配置的示例如下,配置了两个路由规则,有一个 predicates 断言配置,当请求 url 中包含 api/thirdparty,就匹配到了第一个路由 route_thirdparty。
接下来我们看下 Route 路由和 Predicate 断言的对应关系:
- 一对多:一个路由规则可以包含多个断言。如上图中路由 Route1 配置了三个断言 Predicate。
- 同时满足:如果一个路由规则中有多个断言,则需要同时满足才能匹配。如上图中路由 Route2 配置了两个断言,客户端发送的请求必须同时满足这两个断言,才能匹配路由 Route2。
- 第一个匹配成功:如果一个请求可以匹配多个路由,则映射第一个匹配成功的路由。如上图所示,客户端发送的请求满足 Route3 和 Route4 的断言,但是 Route3 的配置在配置文件中靠前,所以只会匹配 Route3。
常见的 Predicate 断言配置如下所示,假设匹配路由成功后,转发到 http://localhost:9001。
代码演示:
下面演示 Gateway 中通过断言来匹配路由的例子。
- 新建一个 Maven 工程,引入 Gateway 依赖。
1 | <dependency> |
- 新建 application.yml 文件,添加 Gateway 的路由规则。
1 | spring: |
第一条路由规则:断言为 Query=url,qq,表示当请求路径中包含 url=qq,则跳转到http://www.qq.com
第二条路由规则:当请求路径中包含 url=baidu,则跳转到http://www.baidu.com
4、动态路由
在微服务架构中,我们不会直接通过 IP + 端口的方式访问微服务,而是通过服务名的方式来访问。如下图所示,微服务中加入了注册中心,多个微服务将自己注册到了注册中心,这样注册中心就保存了服务名和 IP+端口的映射关系。
网关经过断言匹配到一个路由后,将请求转发给指定 uri,这个 uri 可以配置成 微服务的名字,比如 passjava-member。
那么这个服务名具体要转发到哪个 IP 地址和端口上呢?这个就依赖注册中心的注册表了,Gateway 从注册中心拉取注册表,就能知道服务名对应具体的 IP + 端口,如果一个服务部署了多台机器,则还可以通过负载均衡进行请求的转发。
对应的配置为 uri 字段如下所示
uri: lb://passjava-question,表示将请求转发给 passjava-question 微服务,且支持负载均衡。lb 是 loadbalance(负载均衡) 单词的缩写。
那什么叫动态路由呢?
当 passjava-question 服务添加一个微服务,或者 IP 地址更换了,Gateway 都是可以感知到的,但是配置是不需要更新的。这里的动态指的是微服务的集群个数、IP 和端口是动态可变的。
5、过滤器
过滤器 Filter 按照请求和响应可以分为两种:Pre 类型和 Post 类型。
Pre 类型:在请求被转发到微服务之前,对请求进行拦截和修改,例如参数校验、权限校验、流量监控、日志输出以及协议转换等操作。
Post 类型:微服务处理完请求后,返回响应给网关,网关可以再次进行处理,例如修改响应内容或响应头、日志输出、流量监控等。
另外一种分类是按照过滤器 Filter 作用的范围进行划分:
GlobalFilter:全局过滤器,应用在所有路由上的过滤器。
GatewayFilter:局部过滤器,应用在单个路由或一组路由上的过滤器。
6、token认证
在用 Gateway 做登录认证的时候,通常需要我们自定义一个全局过滤器做登录认证。
比如客户端登录时,将用户名和密码发送给网关,网关转发给认证服务器后,如果账号密码正确,则拿到一个 JWT token,然后客户端再访问应用服务时,先将请求发送给网关,网关统一做 JWT 认证,如果 JWT 符合条件,再将请求转发给应用服务。
原理如下图所示:
7、跨域处理
在Spring Cloud Gateway中实现跨域处理的步骤如下:
首先,在你的Spring Cloud Gateway项目中添加CORS过滤器。你可以创建一个类来实现
GlobalFilter
接口,然后在过滤器中添加跨域处理的逻辑。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class CorsFilter implements GlobalFilter, Ordered {
private static final String ALLOWED_HEADERS = "x-requested-with, authorization, Content-Type, Authorization, credential, X-XSRF-TOKEN";
private static final String ALLOWED_METHODS = "GET, PUT, POST, DELETE, OPTIONS";
private static final String ALLOWED_ORIGIN = "*";
private static final String MAX_AGE = "7200"; // 2 hours
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
if (!request.getHeaders().containsKey(HttpHeaders.ORIGIN)) {
return chain.filter(exchange);
}
ServerHttpResponse response = exchange.getResponse();
HttpHeaders headers = response.getHeaders();
headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, ALLOWED_ORIGIN);
headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, ALLOWED_HEADERS);
headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, ALLOWED_METHODS);
headers.add(HttpHeaders.ACCESS_CONTROL_MAX_AGE, MAX_AGE);
if (request.getMethod() == HttpMethod.OPTIONS) {
response.setStatusCode(HttpStatus.OK);
return Mono.empty();
}
return chain.filter(exchange);
}
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}然后,确保你的Spring Cloud Gateway应用程序已经启用了跨域请求。在你的配置文件(例如
application.yml
)中添加以下配置:
1 | spring: |
这样就允许了所有路径的跨域请求。
- 最后,重新启动你的Spring Cloud Gateway应用程序。现在,它应该能够处理跨域请求了。