本文对服务网管zuul进行学习,参考文章 服务网关zuul 。
请先阅读之前的依赖文章:
Spring Cloud - 服务注册与发现
Spring Cloud - 服务消费
Spring Cloud - 分布式配置
Spring Cloud - 断路器
场景:
我们之前实现内部服务eureka-client,端口为2001,外部服务eureka-consumer,端口为3001。外部服务consumer提供Feign或Ribbon来负载均衡的访问内部服务(虽然我们只起了一个内部服务instance)。外部服务只能通过http://localhost:3001/consumer来访问其consumer服务。
问题:
若我们启动另一个外部服务,其端口号为3002,这样可以修改端口号为3002来访问它。但问题是,用户可能不知道这个端口号。
解决方法:
使用服务网关zuul来解决。
服务网关统一向外系统提供 REST API ,除了具备服务路由、均衡负载功能之外,它还具备了权限控制等功能。
Spring Cloud Netflix提供Zuul来实现服务网关的功能,为微服务架构提供了前门保护的作用,同时将权限控制这些较重的非业务逻辑内容迁移到服务路由层面,使得服务集群主体能够具备更高的可复用性和可测试性。
构建服务网关api-gateway
创建project api-gateway并引入zuul和eureka discovery作为依赖。
build.gradle
implementation('org.springframework.cloud:spring-cloud-starter-netflix-eureka-client')
implementation('org.springframework.cloud:spring-cloud-starter-netflix-zuul')
在主程序中使用@EnableZuulProxy来启动zuul的功能。
ApiGatewayApplication.java
@SpringBootApplication
@EnableZuulProxy
public class ApiGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(ApiGatewayApplication.class, args);
}
}
在application.properties中配置服务名,端口号以及服务注册中心地址。
spring.application.name=api-gateway
server.port=7001
eureka.client.serviceUrl.defaultZone=http://localhost:1234/eureka/
启动程序,该程序将自己注册到服务注册中心eureka-server并从服务注册中心获取所有服务以及它们对应的instance清单。
因此服务网关 Zuul 本身就已经维护了系统中所有serviceId
与实例地址的映射关系,例如,它知道eureka-consumer
这个serviceId
对应到两个地址(启动2个consumer instance):
- http://localhost:eureka-consumer:3001
- http://localhost:eureka-consumer:3002
当有外部请求到达服务网关 Zuul 的时候,根据请求的 URL 路径找到最佳匹配的path
规则,将该请求路由到哪个具体的serviceId
上去,并且通过 Ribbon 来实现负载均衡策略。
这样,基于zuul的服务路由就已经创建成功了。由于 Spring Cloud Zuul 在整合了 Eureka 之后,具备默认的服务路由功能,即:当我们这里构建的 api-gateway
应用启动并注册到 Eureka 之后,服务网关 Zuul 会发现上面我们启动的两个服务 eureka-client
和 eureka-consumer
,这时候 Zuul 就会创建路由规则。
每个路由规则都包含两部分,一部分是外部请求的匹配规则,另一部分是路由的服务 ID。针对当前示例的情况,Zuul 会创建下面的四个路由规则,其中:
- 转发到
eureka-client
服务的请求规则为:/eureka-client/**
- 转发到
eureka-consumer
服务的请求规则为:/eureka-consumer/**
- 转发到turbine服务的请求规则为:/turbine/**
- 转发到config-server服务的请求规则为:/config-server/**
之前,我们通过http://localhost:3001/consumer或http://localhost:3002/consumer来访问eureka-consumer提供的consumer服务。现在,我们只需要访问http://localhost:7001/eureka-consumer/consumer来实现同样的效果。
传统路由配置
所谓的传统路由配置方式就是在不依赖于服务发现机制的情况下,通过在配置文件中具体指定每个路由表达式与服务实例的映射关系来实现 API 网关对外部请求的路由。
没有 Eureka 服务治理框架帮助的时候,我们需要根据服务实例的数量采用不同方式的配置来实现路由规则。单实例配置:
zuul.routes.eureka-consumer.path=/eureka-consumer/**
zuul.routes.eureka-consumer.url=http://127.0.0.1:3001/
多实例配置:由于存在多个实例,API 网关在进行路由转发时需要实现负载均衡策略,于是这里还需要 Spring Cloud Ribbon 的配合。由于在 Spring Cloud Zuul 中自带了对 Ribbon 的依赖,所以我们只需要做一些配置即可。
zuul.routes.eureka-consumer.path=/eureka-consumer/**
zuul.routes.eureka-consumer.serviceId=eureka-consumer
ribbon.eureka.enabled=false
eureka-consumer.ribbon.listOfServers=http://127.0.0.1:3001/, http://127.0.0.1:3002/
不论是单实例还是多实例的配置方式,我们都需要为每一对映射关系指定一个名称,也就是上面配置中的 <route>
,每一个 <route>
就对应了一条路由规则。
每条路由规则都需要通过 path
属性来定义一个用来匹配客户端请求的路径表达式,并通过 url
或 serviceId
属性来指定请求表达式映射具体实例地址或服务名。
服务路由配置
Spring Cloud Zuul 通过与 Spring Cloud Eureka 的整合,实现了对服务实例的自动化维护。使用服务路由配置,我们不需要向传统路由配置方式那样为 serviceId
去指定具体的服务实例地址,只需要通过一组 zuul.routes.<route>.path
与 zuul.routes.<route>.serviceId
参数对的方式配置即可。
zuul.routes.eureka-consumer.path=/eureka-consumer/**
zuul.routes.eureka-consumer.serviceId=eureka-consumer
更简洁的方式是zuul.routes.<serviceId>=<path>
,其中<serviceId>
用来指定路由的具体服务名,<path>
用来配置匹配的请求表达式。
zuul.routes.eureka-consumer=/eureka-consumer/**
过滤器
为了实现对客户端请求的安全校验和权限控制,最简单和粗暴的方法就是为每个微服务应用都实现一套用于校验签名和鉴别权限的过滤器或拦截器。缺点是校验逻辑分布在每个微服务,代码冗余且难于维护。
可以将非业务性质的校验放在网关来同意处理,好处去除微服务中的拦截器,简化微服务实现,同时可以做到及早拦截。
Zuul 允许开发者在 API 网关上通过定义过滤器来实现对请求的拦截与过滤,实现的方法非常简单,我们只需要继承ZuulFilter
抽象类并实现它定义的四个抽象函数就可以完成对请求的拦截和过滤。
- 过滤类型 String filterType();
在 Zuul 中默认定义了四种不同生命周期的过滤器类型,具体如下:
-
pre
:可以在请求被路由之前调用。 -
routing
:在路由请求时候被调用。 -
post
:在routing和error过滤器之后被调用。 -
error
:处理请求时发生错误时被调用。
- 执行顺序 int filterOrder();
通过 int
值来定义过滤器的执行顺序,数值越小优先级越高。
- 执行条件 boolean shouldFilter();
返回一个 boolean
类型来判断该过滤器是否要执行。我们可以通过此方法来指定过滤器的有效范围。
- 具体操作 Object run();
过滤器的具体逻辑。在该函数中,我们可以实现自定义的过滤逻辑,来确定是否要拦截当前的请求,不对其进行后续的路由,或是在请求路由返回结果之后,对处理结果做一些加工等。
路由功能在真正运行时,它的路由映射和请求转发都是由几个不同的过滤器完成的:
- 路由映射主要通过
pre
类型的过滤器完成,它将请求路径与配置的路由规则进行匹配,以找到需要转发的目标地址; - 请求转发由
route
类型的过滤器来完成,对pre
类型过滤器获得的路由地址进行转发。
每一个进入 Zuul 的 HTTP 请求都会经过一系列的过滤器处理链得到请求响应并返回给客户端。
在服务网关api-gateway中添加过滤器
在project api-gateway中创建AccessFilter。
AccessFilter.java
public class AccessFilter extends ZuulFilter {
private static Logger log = LoggerFactory.getLogger(AccessFilter.class);
@Override
public String filterType(){
return "pre";
}
@Override
public boolean shouldFilter(){
return true;
}
@Override
public int filterOrder(){
return 0;
}
public Object run(){
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
log.info("Send {} request to {}", request.getMethod(), request.getRequestURL());
Object accessToken = request.getParameter("accessToken");
if(Objects.isNull(accessToken)){
log.warn("Access token is null");
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(401);
ctx.setResponseBody("unauthorized");
return null;
}
log.info("Access token is ok");
return null;
}
}
然后在主程序中声明AccessFilter bean。
@Bean
public AccessFilter accessFilter(){
return new AccessFilter();
}
启动api-gateway并访问http://localhost:7001/eureka-consumer/consumer?op1=2&op2=6 ,我们获得如下界面:
我们加上accessToken request参数访问http://localhost:7001/eureka-consumer/consumer?op1=2&op2=6&accessToken=123便可成功。
核心过滤器
Spring Cloud Zuul自带的核心过滤器: