Spring Cloud Zuul API服务网关一、Zuul 介绍二、构建Spring Cloud Zuul网关构建网关请求路由传统路由方式面向服务的路由请求过滤
一、Zuul 介绍
通过前几篇文章的介绍,我们了解了Spring Cloud Eureka 如何搭建注册中心,Spring Cloud Ribbon 如何做负载均衡,Spring Cloud Hystrix 断路器如何保护我们的服务,以防止雪崩效应的出现,Spring Cloud Feign进行声明式服务调用都有哪些应用,相比Ribbon和Hystrix都有哪些改善。可以说,以上几个组件都是搭建一套微服务架构所必须的。通过以上思路,能够梳理出下面这种基础架构:
无服务网关的架构图
在此架构中,我们的服务集群是内部ServiceA
和 ServiceB
,他们都会向Eureka Server集群进行注册与订阅服务。而OpenService
是一个对外的Restful API 服务,它通过F5,Nginx等网络设备或工具软件实现对各个微服务的路由与负载,公开给外部客户端调用
那么上述的架构存在什么问题呢?从运维
的角度来看,当客户端单机某个功能的时候往往会发出一些请求到后端,这些请求通过F5,Nginx等设施的路由和负载均衡分配后,被转发到各个不同的实例上,而为了让这些设施能够正确的路由与分发请求,运维人员需要手动维护这些实例列表
,当系统规模增大的时候,这些看似简单的维护回变得越来越不可取。从开发
的角度来看,为了保证服务的安全性,我们需要在调用内部接口的时候,加一层过滤
的功能,比如权限
的校验,用户登陆状态
的校验等;同时为了防止客户端在请求时被篡改等安全方面的考虑,还会有一些签名机制的存在。
正是由于上述架构存在的问题,API网关
被提出,API网关更像是一个智能的应用服务器,它的定义类似于设计模式中的外观模式,它就像是一个门面的角色,结婚时候女方亲属堵门时候的角色,我去参加婚礼当伴郎的时候去村子里面见新娘,女方亲属会把鞋子藏起来,有可能藏在屋子里有可能藏在身上,这得需要你自己去寻找,找到了鞋子之后,你才能够给新娘穿上才能正式的会见家长。API网关真正实现的功能有请求路由
,负载均衡
,校验过滤
,请求转发的熔断机制
,服务的聚合
等一系列功能。
Spring Cloud Zuul
通过与Spring Cloud Euerka
进行整合,将自身注册为Eureka服务治理下的应用,同时从Eureka中获得了所有的微服务的实例信息。者可以通过使用Zuul来创建各种校验过滤器,然后指定哪些规则的请求需要执行校验逻辑,只有通过校验的才会被路由到具体的微服务接口。下面我们就来搭建一下Spring Cloud Zuul服务网关
二、构建Spring Cloud Zuul网关
下面我们就来实际搭建一下Zuul网关,来体会一下网关实际的用处
构建网关
在实现各种API网关服务的高级功能之前,我们先来启动一下前几章搭建好的服务server-provider
,feign-consumer
,eureka-server
,虽然之前我们一直将feign-consumer视为消费者,但是在实际情况下,每个服务既时服务消费者,也是服务提供者,之前我们访问的http://localhost:9001/feign-consumer等一系列接口就是它提供的服务。这里就来介绍一下详细的构建过程
- 创建一个Spring Boot功能,命名为api-gateway,并在Pom.xml文件中引入如下内容
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.3.7.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.api.gateway</groupId> <artifactId>api-gateway</artifactId> <version>0.0.1-SNAPSHOT</version> <name>api-gateway</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-zuul</artifactId> <version>1.3.5.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka</artifactId> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Brixton.SR5</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
对于spring-cloud-starter-zuul 依赖,可以通过查看依赖配置了解到,它不仅包含了Netflix Zuul的核心依赖zuul-core,还包括了下面这些网关的重要依赖
- spring-cloud-starter-hystrix: 该依赖用在网关服务中实现对微服务转发时候的保护机制,通过线程隔离和断路器,防止因为微服务故障引发的雪崩效应
- spring-cloud-starter-ribbon: 该依赖用在实现网关服务进行负载均衡和请求重试
- spring-cloud-starter-actuactor: 该依赖用来提供常规的微服务管理端点。另外,Spring Cloud Zuul 中还特别提供了/routes端点来返回当前的路由规则
- 在ApiGatewayApplication 主入口中添加
@EnableZuulProxy
注解开启服务网关功能
@EnableZuulProxy @SpringBootApplication public class ApiGatewayApplication { public static void main(String[] args) { SpringApplication.run(ApiGatewayApplication.class, args); } }
- 在application.properties 中配置Zuul应用的基础信息,包括应用名,端口号,具体如下
spring.application.name=api-gateway server.port=5555
请求路由
下面,我们通过一个简单的示例来为上面构建的网关增加请求路由的功能,为了演示请求路由的功能,我们先将之前的Eureka服务注册中心和微服务应用都启动起来。观察下面的服务列表,可以看到两个微服务应用已经注册成功了
传统路由方式
使用Spring Cloud Zuul
实现路由功能非常简单,只需要对api-gateway服务增加一些关于路由的配置规则,就能实现传统路由方式
zuul.routes.api-a-url.path=/api-a-url/** # 映射具体的url路径 zuul.routes.api-a-url.url=http://localhost:8080/
该配置定义了发往API网关服务的请求中,所有符合/api-a-url/** 规则的访问都将被路由转发到 http://localhost:8080 的地址上,也就是说,当我们访问http://localhost:5555/api-a-url/hello 的时候,API网关服务会将该请求路由到http://localhost:8080/hello 提供的微服务接口中。其中,配置属性zuul.routes.api-a-url.path 中的api-a-url部分为路由的名字,可以任意定义,但是一组path和url映射关系的路由名要相同
面向服务的路由
很显然,传统的配置方式对我们来说并不友好,他同样需要运维人员花费大量的时间维护各个路由path 和url的关系。为了解决这个问题,Spring Cloud Zuul实现了与Spring Cloud Eureka的无缝衔接,我们可以让路由的path不是映射具体的url,而是让它映射到具体的服务
,而具体的url则交给Eureka的服务发现机制去自动维护
- 为了实现与Eureka的整合,我们需要在api-gateway的pom.xml中引入
spring-cloud-starter-eureka
依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka</artifactId> </dependency>
- 在api-gateway服务中对应的application.properties文件中加入如下代码
zuul.routes.api-a.path=/api-a/** zuul.routes.api-a.serviceId=server-provider zuul.routes.api-b.path=/api-b/** zuul.routes.api-b.serviceId=feign-consumer eureka.client.service-url.defaultZone=http://localhost:1111/eureka/
针对我们之前准备的两个微服务应用
server-provider
和feign-consumer
,在上面的配置中分别定义了api-a 和 api-b 的路由来映射它们。然后这个api-gateway的默认注册中心是默认注册中心地址
- 完成上述配置后,我们可以将四个服务启动起来,分别是
eureka-server
,server-provider
,feign-consumer
,api-gateway
服务,启动完毕,会在eureka-server信息面板中看到多了一个api-gateway
网关服务。
- http://localhost:5555/api-a/hello: 这个接口符合
/api-a/**
的规则,由api-a 路由负责转发,该路由映射的serviceId 为server-provider
,所以最终/hello
请求会被发送到server-provider
服务的某个实例上去 - http://localhost:9001/api-b/feign-consumer: 这个接口符合
/api-b/**
的规则,由api-b 进行路由转发,实际的地址由Eureka负责映射,该路由的serviceId是feign-consumer
, 所以最终/feign-consumer
请求会被路由到feign-consumer
服务上。
请求过滤
在实现了请求路由功能之后,我们的微服务应用提供的接口就可以通过统一的API网关入口被客户端访问到了,但是,每个客户端用户请求微服务应用提供的接口时,它们的访问权限往往都有一定限制。为了实现客户端请求的安全校验和权限控制,最简单和粗暴的方法就是为每个微服务应用都实现一套用于校验签名和鉴别权限的过滤器或拦截器。但是,这样的方法并不可取,因为同一个系统中会有很多校验逻辑相同的情况,最好的方法是将这些校验逻辑剥离出去,构成一个独立的服务。
对于上面这种问题,更好的做法是通过前置的网关服务来完成非业务性质的校验。为了在API网关中实现对客户端请求的校验,我们将继续介绍Spring Cloud Zuul的另外一个核心功能:请求过滤
,实现方法比较简单,我们只需要继承ZuulFilter
抽象类并实现它定义的4个抽象函数即可
下面的代码定义了一个简单的Zuul过滤器,它实现了在请求被路由之前检查HttpServletRequest
中是否带有accessToken
参数
public class AccessFilter extends ZuulFilter { private static Logger log = LoggerFactory.getLogger(AccessFilter.class); /** * 过滤器的执行时序 * @return */ @Override public String filterType() { return "pre"; } /** * 过滤器的执行顺序 * @return */ @Override public int filterOrder() { return 0; } /** * 判断过滤器是否应该执行 * @return */ @Override public boolean shouldFilter() { return true; } /** * 过滤器的具体执行逻辑 * @return */ @Override public Object run() { RequestContext rc = RequestContext.getCurrentContext(); HttpServletRequest request = rc.getRequest(); log.info("send {} request to {}", request.getMethod(),request.getRequestURL().toString()); String accessToken = request.getParameter("accessToken"); if(null == accessToken){ log.warn("access token is null"); rc.setResponseStatusCode(401); rc.setSendZuulResponse(false); } log.info("access token ok"); return null; } }
在上面实现的过滤器代码中,我们通过继承ZuulFilter 抽象类并重写了四个方法
- filterType : 过滤器类型,它决定过滤器的请求在哪个生命周期中执行,这里定义为pre,意思是在请求前执行
- filterOrder : 过滤器的执行顺序,当请求在一个阶段存在多个过滤器时,需要根据方法的返回值来判断过滤器的执行顺序
- shouldFilter: 过滤器是否需要执行,这里直接返回true,因为该过滤器对所有的请求都生效
- run: 过滤器的具体逻辑,这里我们通过rc.setResponseStatusCode(401)设置失效的标志,rc.setSendZuulResponse(false)令Zuul过滤该请求
在实现了自定义过滤器之后,它并不会直接生效,我们还需要为其创建具体的Bean才能启动该过滤器。
@EnableZuulProxy @SpringBootApplication public class ApiGatewayApplication { public static void main(String[] args) { SpringApplication.run(ApiGatewayApplication.class, args); } @Bean public AccessFilter filter(){ return new AccessFilter(); } }
在对api-gateway
服务完成了上面的改造之后,我们可以重新启动它,并发起下面的请求,对上面的过滤器做一个验证
- 输入 http://localhost:5555/api-a/hello : 返回 401错误
- 输入 http://localhost:5555/api-a/hello?accessToken=token,正确路由到
server-provider
的/hello 接口,并返回Hello World。
到这里,对于API网关的快速入门示例就搭建完成了,通过对Spring Cloud Zuul 网关的搭建,我们能认知到网关的重要性,可以总结如下:
- 它作为系统的统一入口, 屏蔽了系统内部各个微服务的细节。
- 它可以与服务治理框架结合,实现自动化的服务实例维护以及负载均衡的路由转发。
- 它可以实现接口权限校验与微服务业务逻辑的解耦。
- 通过服务网关中的过滤器, 在各生命周期中去校验请求的内容, 将原本在对外服务层做的校验前移, 保证了微服务的无状态性, 同时降低了微服务的测试难度, 让服务本身更集中关注业务逻辑的处理。