服务网关 Gateway
Spring Cloud Gateway 是 Spring Cloud 生态系统中的网关,它基于 Spring5.0 + SpringBoot2.0 + WebFlux(基于高性能的 Reactor 模式响应式通信框架 Netty,异步非阻塞模型)等技术开发。旨在为微服务架构提供一种简单有效的、统一的 API 路由管理方式。其目标是代替 Zuul。
Spring Cloud Gateway 不仅提供统一的路由方式(反向代理)并且基于 Filter 链的方式提供了网关基本的功能,如:鉴权、流量控制、熔断、路径重写、日志监控等。
1. 核心概念
当用户发起一个请求之后,网关会根据一定的条件匹配,匹配成功之后可以将请求转发到指定的服务地址。而在这个过程中,我们可以进行一些具体的控制(限流、日志等)。
Gateway 有三个核心概念,分别是:路由、断言、过滤器。
- 路由(Route)
网关的基本构建块,也是网关的基础工作单元。路由由一个 ID(唯一标识)、一个目标 RUL(最终路由到的地址)、一组断言(匹配条件判断)和一组过滤器(精细化控制)构成。如果断言为 true,则匹配该路由。
- 断言(Predicate)
用于匹配 HTTP 请求的条件,它可以基于请求的路径、方法、参数、头部信息等多个因素进行判断(类似于 Nginx 中的 location 匹配一样),如果断言与请求相匹配则路由。
- 过滤器(Filter)
用于对请求和响应进行处理的组件,它可以用于修改请求或响应的头部信息、添加请求或响应的字段、限流、重试等多种场景。总而言之,使用过滤器能够在请求之前或之后执行业务逻辑。
首先任何请求进来,网关都会把它们拦住。根据请求的URL把它们分配到不同的路由上,路由上面会有断言,来判断请求能不能进来。进来之后会有一系列的过滤器对请求被转发前或转发后进行改动。 具体怎么个改动法,那就根据业务不同而自定义了。一般就是监控,限流,日志输出等等。
2. 执行流程
Gateway 的执行流程如下:
- 网关控制器映射(Gateway Handler Mapping)
找到与请求相匹配的路由,将其发送到网关 Web 控制器。
- 网关 Web 控制器(Gateway Web Handler)
通过指定的过滤器将请求发送到我们实际的服务执行业务逻辑,然后返回。
- 过滤器(Filter)
主要在请求之前(鉴权、参数校验、日志记录等)和之后(修改响应头、流量监控等)做一些控制处理
Spring Cloud Gateway 的执行流程简述:
客户端向 GateWay 发出请求,然后在 GateWay Handler Mapping 中找到与请求相匹配的路由,将其发送到 GateWay Web Handler;Handler 再通过指定的过滤器链来将请求发送到我们实际的服务执行业务逻辑,然后返回。
过滤器之间用虚线分开是因为过滤器可能会在发送代理请求之前(pre)或者之后(post)执行业务逻辑。
3. 入门案例
新建 gateway-9999 模块,然后在 pom.xml 文件中加入以下依赖:
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!-- 加入上面 Eureka 客户端的相关依赖,这里就不在展示~ -->
需注意:spring-boot-starter-web 依赖需要去除,否则会报错。
写 application.yaml 配置文件,具体如下:
server:port: 9999
eureka:client:service-url:defaultZone: http://localhost:7001/eureka/instance:instance-id: gateway-9999
spring:application:name: cloud-gateway-9999cloud:gateway:routes:# 路由ID,没有固定规则但要求唯一,建议配合服务名- id: provider-8001# 匹配后提供服务的路由地址uri: http://127.0.0.1:8001# 断言predicates:# 路径相匹配的进行路由- Path=/provider/**
在启动类上面加入 @EnableEurekaClient 注解,注册到 Eureka 注册中心,从注册中心获取对应服务。
配置好之后,我们启动注册中心 Eureka7001 和提供者 8001 服务,然后启动 Gateway 服务。
通过 Postman 访问 localhost:9999/provider/get/port 或者 localhost:9999/provider/index,经测试都可以访问。
还可以使用 Java API 的方式配置路由,创建 GatewayConfig 配置类,具体如下:
@Configuration
public class GatewayConfig {@Beanpublic RouteLocator getRouteLocator(RouteLocatorBuilder routeLocatorBuilder) {RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes();/*** 设置路由* id: 路由ID,要求唯一* path(): gateway访问的路径* uri(): 实际的服务地址*/routes.route("my_route", item -> item.path("/guonei").uri("https://news.baidu.com")).build();routes.route("provider", item -> item.path("/provider/index").uri("http://127.0.0.1:8001")).build();return routes.build();}
}
4. 路由规则
一个请求在抵达网关层后,首先就要进行断言匹配,在满足所有断言之后才会进入 Filter 阶段。说白了 Predicate 就是一种路由规则,通过 Gateway 中丰富的内置断言的组合,我们就能让一个请求找到对应的 Route 来处理。
Spring Cloud GateWay 帮我们内置了很多 Predicates 功能,实现了各种路由匹配规则(通过Header、请求参数等作为条件)匹配到对应的路由。如下图所示:
-
DateTime 时间类断言
利用时间断言可以实现一些类似于准点秒杀、商品限时特惠等场景。
- After 时间点后匹配
在该日期时间之后发生的请求都将被匹配。示例配置如下:
spring:application:name: cloud-gateway-9999cloud:gateway:routes:- id: provider-8001uri: http://127.0.0.1:8001# 断言predicates:# 路径相匹配的进行路由# - Path=/provider/**- After=2024-01-01T12:00:32.077075700+08:00[Asia/Shanghai]
配置文件中的时间参数为 UTC 时间格式,可以使用
System.out.println(ZonedDateTime.now());
获取当前的 UTC 格式的时间。- Before 时间点前匹配
在该日期时间之前发生的请求都将被匹配。示例配置如下:(下面配置代码只展示 predicates 部分~)
predicates:- Before=2024-01-01T12:00:32.077075700+08:00[Asia/Shanghai]
- Between 时间区间匹配
有两个参数,datetime1 和 datetime2。在 datetime1 和 datetime2 之间的请求将被匹配。datetime2 参数的实际时间必须在 datetime1 之后。示例配置如下:
predicates:- Between=2024-01-01T12:00:32.077075700+08:00[Asia/Shanghai],2024-01-02T12:00:32.077075700+08:00[Asia/Shanghai]
-
Cookie 类断言
验证 Cookie 中保存的信息,必须连同属性值一同验证,不能单独只验证属性是否存在。示例配置如下:
predicates:- Cookie=username,lyl
使用 Postman 进行接口测试,在 Headers 栏中加入 Key 为 Cookie,Value 为 username=lyl 的参数,然后访问对应接口。
- Header 请求头类断言
Header 中包含了响应的属性才会被匹配,通常可以用来验证请求是否携带了访问令牌。示例配置如下:
predicates:# 请求头要有X-Request-Id属性并且值为整数的正则表达式- Header=X-Request-Id, \d+
- Host 请求主机类断言
有一个参数:host name 列表。使用 Ant 路径匹配规则,点 .
作为分隔符。多个用逗号 ,
隔开。示例配置如下:
predicates:- Host=lylhost
需要在本地 Host 文件中新增:
127.0.0.1 lylhost
- Method 请求方法类断言
HTTP 方法符合要求则匹配,例如 GET、POST 等,多个用逗号隔开,示例配置如下:
predicates:- Method=GET
- Path 请求路径类断言
请求路径符合规则则匹配,支持正则表达式,示例配置如下:
predicates:- Path=/provider/**
- QueryParam 请求参数类断言
验证参数,可单独验证是否包含参数,也可验证参数是否满足指定规则,符合则匹配,示例配置如下:
predicates:# 参数username符合要求则匹配- Query=username, \d+# 参数password存在则匹配- Query=password
- RemoteAddr 远程地址类断言
远程地址符合则匹配,示例配置如下:
predicates:- RemoteAddr=192.168.1.1/24
动态路由
GateWay 支持自动从注册中心中获取服务列表并访问,即所谓的动态路由。具体配置如下:
spring:cloud:gateway:routes:- id: provider-8001# 匹配后提供服务的路由地址,lb后跟提供服务的微服务的名uri: lb://CLOUD-PROVIDER-8001predicates:- Path=/provider/**
动态路由设置时,uri 以 lb: // 开头(lb 代表从注册中心获取服务),后面是需要转发到的服务名称
分别启动 Eureka、provider-8001 和 provider-8002 服务,使用 Postman 调用 localhost:9999/provider/get/port 接口进行测试。
5. 网关过滤器
根据生命周期可以将 Spring Cloud Gateway 中的 Filter 分为 PRE 和 POST 两种:
- PRE:代表在请求被路由之前执行该过滤器,可用来实现参数校验、权限校验、流量监控、日志输出、协议转换等功能。
- POST:代表在请求被路由到微服务之后执行该过滤器。可用来实现响应头的修改(如添加标准的 HTTP Header )、收集统计信息和指标、将响应发送给客户端、输出日志、流量监控等功能。
根据作用范围,Filter 可以分为以下两种:
- GatewayFilter:网关过滤器,只应用在单个路由或者一个分组的路由上。
- GlobalFilter:全局过滤器,应用在所有的路由上。
GlobalFilter 全局过滤器是我们使用比较多的过滤器。
(1)GatewayFilter
网关过滤器(GatewayFilter)允许以某种方式修改传入的 HTTP 请求,或输出的 HTTP 响应。网关过滤器作用于特定路由。
Spring Cloud Gateway 内置了许多网关过滤器工厂来编写网关过滤器。例如:
- AddRequestHeader:为原始请求添加 Header,参数为 Header 的名称及值
- AddRequestParameter:为原始请求添加请求参数,参数为参数名称和值
- AddResponseHeader:为原始响应添加 Header,参数为 Header 的名称及值
- PrefixPath:为原始请求路径添加前缀 ,参数为前缀路径
- RedirectTo:将原始请求重定向到指定的URL,参数为 HTTP 状态码及重定向的 URL
- RemoveRequestHeader:为原始请求删除某个 Header,参数为 Header 名称
- RemoveResponseHeader:为原始响应删除某个Header,参数为 Header 名称
- SetResponseHeader:修改原始响应中某个 Header 的值,参数为 Header 名称和修改后的值
- SetStatus:修改原始响应的状态码,参数为 HTTP 状态码,可以是数字,也可以是字符串
示例配置如下:
spring:cloud:gateway:routes:- id: provider-8001uri: lb://CLOUD-PROVIDER-8001predicates:- Path=/provider/**# 过滤器filters:# 修改原始响应的状态码- SetStatus=2024
我们也可以通过继承 AbstractGatewayFilterFactory 类,重写 shortcutFieldOrder() 方法的方式自定义网关过滤器,这里就不再展示~
(2)GlobalFilter
全局过滤器作用于所有路由,无需配置。通过全局过滤器可以实现对权限的统一校验,安全性验证等功能。
这里我们模拟一个黑名单拒绝访问的场景,具体示例代码如下:
/*** Description: 模拟实现黑名单功能* 场景:请求过来时,判断发送请求的客户端的ip,如果在黑名单中,拒绝访问*/
@Slf4j
@Component
public class BlackListFilter implements GlobalFilter, Ordered {/*** 模拟黑名单(实际场景可从数据库或Redis中查询)*/private static List<String> blackList = new ArrayList<>();static {// 模拟本机地址blackList.add("0:0:0:0:0:0:0:1");blackList.add("127.0.0.1");}/*** 过滤器核心方法* @param exchange 封装了request和response对象的上下文* @param chain 网关过滤器链(包含全局过滤器和单路由过滤器)*/@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {// 获取request和response对象ServerHttpRequest request = exchange.getRequest();ServerHttpResponse response = exchange.getResponse();// 获取客户端IPString clientIp = request.getRemoteAddress().getHostString();// 在黑名单中则拒绝访问,直接返回if (blackList.contains(clientIp)) {response.setStatusCode(HttpStatus.UNAUTHORIZED);log.info("=====>IP:" + clientIp + " 在黑名单中,将被拒绝访问!");String data = "Request be denied!";DataBuffer wrap = response.bufferFactory().wrap(data.getBytes());return response.writeWith(Mono.just(wrap));}// 不在黑名单的请求放行,执行后续的过滤器链return chain.filter(exchange);}/*** 返回值表示当前过滤器的顺序(优先级),数值越小,优先级越高*/@Overridepublic int getOrder() {return 0;}
}
通过 Postman 进行测试,返回 Request be denied!
6. 跨域配置
当一个请求 URL 的协议、域名、端口三者之间任意一个与当前页面 URL 不同即为跨域。
为什么会出现跨域:
出于浏览器的同源策略限制。同源策略是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,则浏览器的正常功能可能都会受到影响。可以说 Web 是构建在同源策略基础之上的,浏览器只是针对同源策略的一种实现。
当前页面URL | 被请求页面URL | 是否跨域 | 原因 |
---|---|---|---|
http://www.test.com/ | http://www.test.com/index.html | 否 | 同源(协议、域名、端口号相同) |
http://www.test.com/ | https://www.test.com/index.html | 是 | 协议不同(http/https) |
http://www.test.com/ | http://www.baidu.com/ | 是 | 主域名不同(test/baidu) |
http://www.test.com/ | http://blog.test.com/ | 是 | 子域名不同(www/blog) |
http://www.test.com:8080/ | http://www.test.com:7001/ | 是 | 端口号不同(8080/7001) |
打开浏览器的 F12,当出现 ‘Access-Control-Allow-Origin’ 时,就是出现了跨域情况。
CORS
如何允许跨域,一种解决方法就是目的域告诉请求者允许什么来源域来请求,那么浏览器就会知道 B 域是否允许 A 域发起请求。
CORS(Cross-origin resource sharing),跨域资源共享就是这样一种解决手段。
CORS 使得浏览器在向目的域发起请求之前先发起一个 OPTIONS 方式的请求到目的域获取目的域的信息,比如获取目的域允许什么域来请求的信息。具体配置如下:
spring:cloud:gateway:globalcors:cors-configurations:'[/**]':allowCredentials: trueallowedOriginPatterns: "*"allowedMethods: "*"allowedHeaders: "*"add-to-simple-url-handler-mapping: true
7. JWT
JWT(JSON Web Token)是一种用于双方之间传递安全信息的简洁的、URL 安全的声明规范。定义了一种简洁的,自包含的方法用于通信双方之间以 JSON 对象的形式安全的传递信息。特别适用于分布式站点的单点登录(SSO)场景。
JWT 的原理就是,服务器在用户登录认证之后,生成一个 JSON 对象,然后生成一个长字符串返回给用户。用户在后面发起请求时,都会在请求头中携带 JTW,服务器对其进行验证,解析成功即让其成功访问,反之则拒绝访问。
JWT 是一个很长的字符串,中间用点.
分割成 3 部分,示例如下:
eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSm9obiIsImFkbWluIjp0cnVlfQ.1YiYlxYX9I1Yjdk0lyJp3vluWfbOSsGToWzMDJcPcOpnbEVZZGzR4NhpgYFKqmAzarsfnNpOKrvS48KeXuO0IA
- Header(头部)
JSON对象,描述 JWT 的元数据。
Header 通常包含两个部分,即 token 的类型(typ)和算法(alg)。
其中 alg 属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256);typ 属性表示这个令牌(token)的类型(type),统一写为 JWT。示例如下:
{"alg": "HS512","typ": "JWT"
}
Header 由 Base64 编码后构成了 JWT 的第一部分。
Base64 是一种用 64 个字符来表示任意二进制数据的方法,Base64 是一种任意二进制到文本字符串的编码方法,常用于在 URL、Cookie、网页中传输少量二进制数据。
-
Payload(载荷)
Payload 是 JWT 的第二部分,它包含了要传递的信息,也就是所谓的 claim,有三种类型:
-
预定义的声明(Registered Claims)
这些声明是可选的,但建议尽可能使用。
- iss:签发者
- sub: 主题
- aud:接收者
- exp:过期时间,必须要大于签发时间
- nbf:生效时间
- iat:签发时间
- jti:编号,唯一身份标识
-
自定义的声明(Public Claims)
自定义公共的声明,用于传递用户的信息或其他数据,建议使用命名空间来避免冲突。
- 私有的声明(Private Claims)
在双方约定的情况下私下使用的声明,避免和其他声明冲突。
示例如下:
{"sub":"GE_ecd48edee025487897926cdd1e71234","name":"用户名","created":1704092559000,"exp":1704178959 }
Payload 也是由 Base64 编码后与 Header 通过一个点号连接,形成了 JWT 的第二部分。
-
-
Signature(签证)
Signature 是 JWT 的第三部分,由 Header 和 Payload 经过 base64UrlEncode 之后,指定一个密钥(secret)通过算法计算得出,算法通常是 HS256(HMACSHA256)或RS256(RSASHA256)等。
HMACSHA512(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
JWT 的签名过程如下:
1)取得 Header 和 Payload 的 Base64Url 编码后的字符串,中间用点号连接起来。
2)使用 Header 中指定的算法和密钥对上一步骤得到的字符串进行签名,生成 Signature。
3)将 Header、Payload 和 Signature 以点号连接起来,形成最终的 JWT。
在验证 JWT 时,需要对 Header 和 Payload 进行 Base64Url解码,再按照相同的算法和密钥计算Signature,与JWT中的Signature进行比对,如果一致说明JWT是真实的,否则说明JWT可能被篡改过。