gateway 网关
背景:
在微服务架构中,一个微服务会拆分为多个微服务,客户端如何调用微服务呢?只能在客户端记录微服务的地址进行调用。
存在的问题:
客户端多次请求不同的微服务,增加客户端代码或配置编写的复杂性
认证复杂,每个服务都需要独立认证。
存在跨域请求,在一定场景下处理相对复杂。
解决方案:api网关
所谓的API网关,就是指系统的统一入口,它封装了应用程序的内部结构,为客户端提供统一服
务,一些与业务本身功能无关的公共逻辑可以在这里实现,诸如认证、鉴权、监控、路由转发等等。
一、gateway简介
1、简介
Spring Cloud Gateway是Spring公司基于Spring 5.0,Spring Boot 2.0 和 Project Reactor 等技术
开发的网关,它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式。它的目标是替代
Netflix Zuul,其不仅提供统一的路由方式,并且基于 Filter 链的方式提供了网关基本的功能,例如:安
全,监控和限流。
2、优点
性能强劲:是第一代网关Zuul的1.6倍
功能强大:内置了很多实用的功能,例如转发、监控、限流等
设计优雅,容易扩展
3、缺点
其实现依赖Netty
与WebFlux
,不是传统的Servlet
编程模型,学习成本高
不能将其部署在Tomcat、Jetty等Servlet
容器里,只能打成jar包执行
需要Spring Boot 2.0及以上的版本,才支持
4、核心概念
4.1 路由(route)
路由是网关最基础的部分,路由信息由一个ID、一个目的URL、一组断言工厂和一组Filter组成。如果断言为真,则说明请求URL和配置的路由匹配。
4.2 断言(predicates)
Java8
中的断言函数,Spring Cloud Gateway中的断言函数输入类型是Spring5.0
框架中的ServerWebExchange
。Spring Cloud Gateway中的断言函数允许开发者去定义匹配来自Http Request
中的任何信息,比如请求头和参数等。
4.3 过滤器(filter)
一个标准的Spring webFilter
,Spring Cloud Gateway
中的Filter分为两种类型,分别是Gateway Filter和Global Filter。过滤器Filter可以对请求和响应进行处理。
二、 gateway网关
1、快速入门
1.1 搭建gateway-server
添加依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
注意SpringCloud Gateway
使用的web框架为webflux
,和SpringMVC
不兼容。所以不需要spring-boot-starter-web。
启动类
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
添加配置文件
application.yml
进行配置
server:
port: 7001 #服务端口
spring:
application:
name: gateway-server #指定服务名
cloud:
gateway:
routes:
- id: product-service
uri: http://localhost:9444
predicates:
- Path=/product-service/**
# http://localhost:7001/product-service/service/demo1 →
# http://localhost:9444/product-service/service/demo1
filters:
# StripPrefix=1 表示去掉第一层路径 product-service
# http://localhost:7001/product-service/service/demo1 →
# http://localhost:9444/service/demo1
- StripPrefix=1
1.2 搭建客户端服务
引用eureka-client-demo1
1.3 测试
测试客户端:localhost:9444/service/demo1
{
"code": "ok",
"port": 9444,
"url": "/service/demo1"
}
测试经过网关:localhost:7001/product-service/service/demo1
{
"code": "ok",
"port": 9444,
"url": "/service/demo1"
}
2、路由规则及断言
2.1 内置断言
Spring Cloud Gateway 的功能很强大,前面我们只是使用了 predicates 进行了简单的条件匹配,其实
Spring Cloud Gataway 帮我们内置了很多 Predicates 功能。在 Spring Cloud Gateway 中 Spring 利用
Predicate 的特性实现了各种路由匹配规则,有通过 Header、请求参数等不同的条件来进行作为条件
匹配到对应的路由。
路由规则配置
id,路由标识符,区别于其他 Route。
uri,路由指向的目的地 uri,即客户端请求最终被转发到的微服务。
order,用于多个 Route 之间的排序,数值越小排序越靠前,匹配优先级越高。
predicate,断言的作用是进行条件判断,只有断言都返回真,才会真正的执行路由。
filter,过滤器用于修改请求和响应信息。
示例:
以org.springframework.cloud.gateway.handler.predicate.AfterRoutePredicateFactory
public class AfterRoutePredicateFactory extends AbstractRoutePredicateFactory<AfterRoutePredicateFactory.Config> {
public static final String DATETIME_KEY = "datetime";
public AfterRoutePredicateFactory() {
super(Config.class);
}
@Override
public List<String> shortcutFieldOrder() {
return Collections.singletonList(DATETIME_KEY);
}
@Override
public Predicate<ServerWebExchange> apply(Config config) {
ZonedDateTime datetime = config.getDatetime();
return exchange -> {
final ZonedDateTime now = ZonedDateTime.now();
return now.isAfter(datetime);
};
}
public static class Config {
@NotNull
private ZonedDateTime datetime;
public ZonedDateTime getDatetime() {
return datetime;
}
public void setDatetime(ZonedDateTime datetime) {
this.datetime = datetime;
}
}
}
配置:
spring:
application:
name: gateway-server #指定服务名
cloud:
gateway:
routes:
- id: after-route
uri: https://www.baidu.com
predicates:
- After=2020-10-05T19:39:37.198+08:00[Asia/Shanghai]
时间格式为:datetime
@org.junit.Test
public void test1(){
ZonedDateTime datetime =ZonedDateTime.now();
System.out.println(datetime);
}
访问测试:http://localhost:7001?access_token=zengqingfa
测试时间变化为某个未来的时间:
- id: after-route
uri: https://www.baidu.com
predicates:
- After=2020-10-05T20:39:37.198+08:00[Asia/Shanghai]
重新启动测试:
无法访问。
2.2 自定义断言工厂
假设我们的应用仅仅让age在(min,max)之间的人来访问。
1、自定义断言工厂
@Component
public class AgeRoutePredicateFactory extends AbstractRoutePredicateFactory<AgeRoutePredicateFactory.Config> {
public AgeRoutePredicateFactory() {
super(AgeRoutePredicateFactory.Config.class);
}
//用于从配置文件中获取参数值赋值到配置类中的属性上
@Override
public List<String> shortcutFieldOrder() {
//这里的顺序要跟配置文件中的参数顺序一致
return Arrays.asList("minAge", "maxAge");
}
@Override
public Predicate<ServerWebExchange> apply(AgeRoutePredicateFactory.Config config) {
return new Predicate<ServerWebExchange>() {
@Override
public boolean test(ServerWebExchange serverWebExchange) {
//从serverWebExchange获取传入的参数
String ageStr = serverWebExchange.getRequest().getQueryParams().getFirst("age");
if (StringUtils.isNotEmpty(ageStr)) {
int age = Integer.parseInt(ageStr);
return age > config.getMinAge() && age < config.getMaxAge();
}
return true;
}
};
}
/**
* 自定义一个配置类, 用于接收配置文件中的参数
*/
@Data
@NoArgsConstructor
public static class Config {
private int minAge;
private int maxAge;
}
}
2、配置
spring:
application:
name: gateway-server #指定服务名
cloud:
gateway:
routes:
- id: product-service
uri: lb://eureka-client1
predicates:
- Path=/product-service/**
- Age=18,60 # 限制年龄只有在18到60岁之间的人能访问
filters:
- StripPrefix=1
3、启动测试:
http://localhost:7001/product-service/service/demo1?access_token=zengqingfa&age=1
http://localhost:7001/product-service/service/demo1?access_token=zengqingfa&age=20
3、动态路由
和zuul网关类似,在SpringCloud GateWay中也支持动态路由:即自动的从注册中心中获取服务列表并
访问。
3.1 gateway-server改造
添加eureka依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
添加注解
@EnableEurekaClient
@SpringBootApplication
@EnableEurekaClient
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
application.yml配置文件修改
server:
port: 7001 #服务端口
spring:
application:
name: gateway-server #指定服务名
cloud:
gateway:
routes:
- id: product-service
#uri : uri以 lb: //开头(lb代表从注册中心获取服务)+微服务名称
uri: lb://eureka-client1
predicates:
- Path=/product-service/**
filters:
- StripPrefix=1
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:9001/eureka
##是否需要将自己注册注册中心
register-with-eureka: true
##是否需要检索服务信息
fetch-registry: true
3.2 客户端不修改
3.3 测试
http://localhost:7001/product-service/service/demo1?access_token=zengqingfa
{
"code": "ok",
"port": 9444,
"url": "/service/demo1",
"userCode": "zengqingfa2323232"
}
4、重写转发路径
在SpringCloud Gateway中,路由转发是直接将匹配的路由path直接拼接到映射路径(URI)之后,那
么在微服务开发中往往没有那么便利。这里就可以通过RewritePath机制来进行路径重写。
4.1 改造
server:
port: 7001 #服务端口
spring:
application:
name: gateway-server #指定服务名
cloud:
gateway:
routes:
- id: product-service
#uri : uri以 lb: //开头(lb代表从注册中心获取服务)+微服务名称
uri: lb://eureka-client1
predicates:
- Path=/product-service/**
直接访问:
localhost:7001/product-service/service/demo1
报错404
于路由转发规则默认转发到微服务(http://127.0.0.1:9444/product- service/product/1
)路径上,而微服务又没有 product-service 对应的映射配置。
4.2 添加RewritePath重写转发路径
server:
port: 7001 #服务端口
spring:
application:
name: gateway-server #指定服务名
cloud:
gateway:
routes:
- id: product-service
#uri : uri以 lb: //开头(lb代表从注册中心获取服务)+微服务名称
uri: lb://eureka-client1
predicates:
- Path=/product-service/**
filters:
- RewritePath=/product-service/(?<segment>.*), /$\{segment} #路径重写
访问:
http://localhost:7001/product-service/service/demo1?access_token=zengqingfa
成功
通过RewritePath配置重写转发的url,将/product-service/(?.*),重写为{segment},然后转发。比如在网页上请求http://localhost:8080/product-service/product,此时会将请求转发到
http://127.0.0.1:9002/product/1( 值得注意的是在yml文档中 $ 要写成 $\ )
5、过滤器
Spring Cloud Gateway除了具备请求路由功能之外,也支持对请求的过滤。通过Zuul网关类似,也是通
过过滤器的形式来实现的。
5.1 过滤器的生命周期
Spring Cloud Gateway 的 Filter 的生命周期不像Zuul
的那么丰富,它只有两个:“pre
” 和 “post
”。
PRE
: 这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。
POST:这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的HTTPHeader
、收集统计信息和指标、将响应从微服务发送给客户端等。
5.2 过滤器类型
Spring Cloud Gateway 的 Filter 从作用范围可分为另外两种GatewayFilter
与GlobalFilter
。
GatewayFilter:应用到单个路由或者一个分组的路由上。
GlobalFilter:应用到所有的路由上。
5.3 局部过滤器
局部过滤器(GatewayFilter
),是针对单个路由的过滤器。可以对访问的URL过滤,进行切面处理。在
Spring Cloud Gateway中通过GatewayFilter
的形式内置了很多不同类型的局部过滤器。
1、内置局部过滤器
每个过滤器工厂都对应一个实现类,并且这些类的名称必须以 GatewayFilterFactory
结尾,这是
Spring Cloud Gateway
的一个约定,例如 AddRequestHeader
对应的实现类为
AddRequestHeaderGatewayFilterFactory
。
过滤器工厂 | 作用 | 参数 |
---|---|---|
AddRequestHeader | 为原始请求添加Header | Header的名称及值 |
AddRequestParameter | 为原始请求添加参数 | 参数名称及值 |
AddResponseHeader | 为原始响应添加Header | Header的名称及值 |
SetStatus | 修改原始响应的状态码 | HTTP 状态码,可以是数字,也可以是字符串 |
以SetStatus
为例:
配置
spring:
application:
name: gateway-server #指定服务名
cloud:
gateway:
routes:
- id: product-service
uri: lb://eureka-client1
predicates:
- Path=/product-service/**
filters:
- StripPrefix=1
- SetStatus=2500
测试
http://localhost:7001/product-service/service/demo1?access_token=zengqingfa
2、自定义局部过滤器
自定义过滤器工厂
@Component
@Slf4j
public class LogGatewayFilterFactory extends AbstractGatewayFilterFactory<LogGatewayFilterFactory.Config> {
//构造函数
public LogGatewayFilterFactory() {
super(LogGatewayFilterFactory.Config.class);
}
//读取配置文件中的参数 赋值到 配置类中
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("consoleLog", "cacheLog");
}
//过滤器逻辑
@Override
public GatewayFilter apply(LogGatewayFilterFactory.Config config) {
return new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
if (config.isCacheLog()) {
log.info("cacheLog已经开启了....");
}
if (config.isConsoleLog()) {
log.info("consoleLog已经开启了....");
}
return chain.filter(exchange);
}
};
}
@Data
@NoArgsConstructor
public static class Config {
private boolean consoleLog;
private boolean cacheLog;
}
}
配置
spring:
application:
name: gateway-server #指定服务名
cloud:
gateway:
routes:
- id: product-service
uri: lb://eureka-client1
predicates:
- Path=/product-service/**
filters:
- StripPrefix=1
- Log=true,false #控制日志是否开启
测试
http://localhost:7001/product-service/service/demo1?access_token=zengqingfa
2020-10-06 10:28:12.238 INFO 16052 --- [ctor-http-nio-3] c.z.g.factory.LogGatewayFilterFactory : consoleLog已经开启了....
5.4 全局过滤器
全局过滤器(GlobalFilter
)作用于所有路由,Spring Cloud Gateway 定义了Global Filter接口,用户
可以自定义实现自己的Global Filter。通过全局过滤器可以实现对权限的统一校验,安全性验证等功能。
Spring Cloud Gateway内部也是通过一系列的内置全局过滤器对整个路由转发进行处理
1、内置全局过滤器
过滤器 | 作用 |
---|---|
loadBalancerClientFilter | 负载均衡 |
ForwardPathFilter | 路径转发 |
public interface GlobalFilter {
/**
* Process the Web request and (optionally) delegate to the next
* {@code WebFilter} through the given {@link GatewayFilterChain}.
* @param exchange the current server exchange
* @param chain provides a way to delegate to the next filter
* @return {@code Mono<Void>} to indicate when request processing is complete
*/
Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);
}
负载均衡过滤器配置:lb://微服务名称
- id: product-service
uri: lb://eureka-client1
order: 1
2、自定义全局过滤器: 统一鉴权
开发中的鉴权逻辑:
当客户端第一次请求服务时,服务端对用户进行信息认证(登录)认证通过,将用户信息进行加密形成token,返回给客户端,作为登录凭证以后每次请求,客户端都携带认证的token服务端对token进行解密,判断是否有效。
对于验证用户是否已经登录鉴权的过程可以在网关层统一检验。检验的标准就是请求中是否携带token凭证以及token的正确性。
自定义全局过滤器
@Slf4j
@Component
public class LoginFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String token = exchange.getRequest().getQueryParams().getFirst("access_token");
List<String> cookie = exchange.getRequest().getHeaders().get("Cookie");
if (StringUtils.isBlank(token)) {
log.error("token is empty...");
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
} else {
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 0;
}
}
认证流程
自定义全局过滤器需要实现GlobalFilter
和Ordered
接口。
在filter方法中完成过滤器的逻辑判断处理
在getOrder
方法指定此过滤器的优先级,返回值越大级别越低
ServerWebExchange
就相当于当前请求和响应的上下文,存放着重要的请求-响应属性、请求实例和响应实例等等。一个请求中的request,response都可以通过 ServerWebExchange
获取调用 chain.filter 继续向下游执行
测试
http://localhost:7001/product-service/service/demo1
添加token参数测试:
http://localhost:7001/product-service/service/demo1?access_token=zengqingfa
{
code: "ok",
port: 9444,
url: "/service/demo1",
userCode: "zengqingfa2323232"
}
6、网关限流
网关是所有请求的公共入口,所以可以在网关进行限流,而且限流的方式也很多,我们本次采用前
面学过的Sentinel组件来实现网关的限流。Sentinel支持对SpringCloud Gatway、Zuul
等主流网关进
行限流·
从1.6.0版本开始,Sentinel提供了SpringCloud Gateway
的适配模块,可以提供两种资源维度的限流:
route维度:即在Spring配置文件中配置的路由条目,资源名为对应的routeId
自定义API
维度:用户可以利用Sentinel提供的API
来自定义一些API
分组
1、导入依赖
<!--限流-->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
</dependency>
2、编写配置类
基于Sentinel 的Gateway限流是通过其提供的Filter来完成的,使用时只需注入对应的
SentinelGatewayFilter
实例以及 SentinelGatewayBlockExceptionHandler
实例即可。
@Configuration
public class GatewayConfiguration {
private final List<ViewResolver> viewResolvers;
private final ServerCodecConfigurer serverCodecConfigurer;
public GatewayConfiguration(ObjectProvider<List<ViewResolver>> viewResolversProvider,
ServerCodecConfigurer serverCodecConfigurer) {
this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
this.serverCodecConfigurer = serverCodecConfigurer;
}
// 初始化一个限流的过滤器
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public GlobalFilter sentinelGatewayFilter() {
return new SentinelGatewayFilter();
}
// 配置初始化的限流参数
@PostConstruct
public void initGatewayRules() {
Set<GatewayFlowRule> rules = new HashSet<>();
rules.add(
new GatewayFlowRule("eureka-client1") //资源名称,对应路由id
.setCount(1) // 限流阈值
.setIntervalSec(1) // 统计时间窗口,单位是秒,默认是 1 秒
);
GatewayRuleManager.loadRules(rules);
}
// 配置限流的异常处理器
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
}
// 自定义限流异常页面
@PostConstruct
public void initBlockHandlers() {
BlockRequestHandler blockRequestHandler = new BlockRequestHandler() {
@Override
public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) {
Map map = new HashMap<>();
map.put("code", 0);
map.put("message", "接口被限流了");
return ServerResponse.status(HttpStatus.OK).
contentType(MediaType.APPLICATION_JSON_UTF8).
body(BodyInserters.fromObject(map));
}
};
GatewayCallbackManager.setBlockHandler(blockRequestHandler);
}
}
3、配置
server:
port: 7001 #服务端口
spring:
application:
name: gateway-server #指定服务名
cloud:
gateway:
routes:
- id: eureka-client
uri: lb://eureka-client
order: 1
predicates:
- Path=/eureka-client/**
filters:
- StripPrefix=1
- id: eureka-client1
uri: lb://eureka-client1
order: 1
predicates:
- Path=/eureka-client1/**
filters:
- StripPrefix=1
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:9001/eureka
##是否需要将自己注册注册中心
register-with-eureka: true
##是否需要检索服务信息
fetch-registry: true
4、路由维度测试
http://localhost:7001/eureka-client1/service/demo1?access_token=zengqingfa
http://localhost:7001/eureka-client/hello?access_token=zengqingfa
5、自定义api
维度
修改配置类
@Configuration
public class GatewayConfiguration {
private final List<ViewResolver> viewResolvers;
private final ServerCodecConfigurer serverCodecConfigurer;
public GatewayConfiguration(ObjectProvider<List<ViewResolver>> viewResolversProvider,
ServerCodecConfigurer serverCodecConfigurer) {
this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
this.serverCodecConfigurer = serverCodecConfigurer;
}
// 初始化一个限流的过滤器
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public GlobalFilter sentinelGatewayFilter() {
return new SentinelGatewayFilter();
}
// 配置初始化的限流参数
@PostConstruct
public void initGatewayRules() {
Set<GatewayFlowRule> rules = new HashSet<>();
rules.add(new GatewayFlowRule("client_api1").setCount(1).setIntervalSec(1));
rules.add(new GatewayFlowRule("client_api2").setCount(1).setIntervalSec(1));
GatewayRuleManager.loadRules(rules);
}
// 配置限流的异常处理器
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
}
// 自定义限流异常页面
@PostConstruct
public void initBlockHandlers() {
BlockRequestHandler blockRequestHandler = new BlockRequestHandler() {
@Override
public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) {
Map map = new HashMap<>();
map.put("code", 0);
map.put("message", "接口被限流了111");
return ServerResponse.status(HttpStatus.OK).
contentType(MediaType.APPLICATION_JSON_UTF8).
body(BodyInserters.fromObject(map));
}
};
GatewayCallbackManager.setBlockHandler(blockRequestHandler);
}
//自定义API分组
@PostConstruct
private void initCustomizedApis() {
Set<ApiDefinition> definitions = new HashSet<>();
ApiDefinition api1 = new ApiDefinition("client_api1")
.setPredicateItems(new HashSet<ApiPredicateItem>() {{
// 以/eureka-client/api1 开头的请求
add(new ApiPathPredicateItem().setPattern("/eureka-client1/eureka-client/api1/**").
setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));
}});
ApiDefinition api2 = new ApiDefinition("client_api2")
.setPredicateItems(new HashSet<ApiPredicateItem>() {{
// 以/eureka-client/api2/demo1 完成的url路径匹配
add(new ApiPathPredicateItem().setPattern("/eureka-client1/eureka-client/api2/demo1"));
}});
definitions.add(api1);
definitions.add(api2);
GatewayApiDefinitionManager.loadApiDefinitions(definitions);
}
}
增加路由controller
@RestController
public class GatewayDemoRest {
//限流
@RequestMapping("/eureka-client/api1/demo1")
public String demo1() {
return "/eureka-client/api1/demo1";
}
//限流
@RequestMapping("/eureka-client/api1/demo2")
public String demo2() {
return "/eureka-client/api1/demo2";
}
//限流
@RequestMapping("/eureka-client/api2/demo1")
public String demo3() {
return "/eureka-client/api2/demo1";
}
//未限流
@RequestMapping("/eureka-client/api2/demo2")
public String demo4() {
return "/eureka-client/api2/demo2";
}
}
测试
http://localhost:7001/eureka-client1/eureka-client/api2/demo2
未限流,其他三个限流
附录(码云地址):https://gitee.com/zengqingfa/springcloud-demo.git