請先閱讀之前的內(nèi)容:
- Spring Cloud 學(xué)習(xí)筆記 - No.1 服務(wù)注冊發(fā)現(xiàn)
- Spring Cloud 學(xué)習(xí)筆記 - No.2 服務(wù)消費 Ribbon & Feign
- Spring Cloud 學(xué)習(xí)筆記 - No.3 分布式配置 Config
- Spring Cloud 學(xué)習(xí)筆記 - No.4 斷路器 Hystrix
什么是服務(wù)網(wǎng)關(guān)
在之前的例子中,我們啟動了一個外部服務(wù) eureka-consumer,端口 3001。
同時我們也啟動了兩個內(nèi)部服務(wù) eureka-client,端口 2001 和 2002,該外部服務(wù)通過 Ribbon 或 Feign 來在客戶端負(fù)載均衡地調(diào)用內(nèi)部服務(wù)。
之前我們都是通過 http://127.0.0.1:3001/consumer 來調(diào)用外部服務(wù) eureka-consumer 提供的服務(wù) /consumer。
問題來了:
假設(shè)我們啟動了另外一個外部服務(wù) eureka-consumer,端口 3002。此時外部用戶只能通過 http://127.0.0.1:3002/consumer 來訪問,但是外部用戶可能并不知道 3002 這個端口。
服務(wù)網(wǎng)關(guān)是微服務(wù)架構(gòu)中一個不可或缺的部分。
通過服務(wù)網(wǎng)關(guān)統(tǒng)一向外系統(tǒng)提供 REST API 的過程中,除了具備服務(wù)路由、均衡負(fù)載功能之外,它還具備了權(quán)限控制等功能。
Spring Cloud Netflix 中的 Zuul 就擔(dān)任了這樣的一個角色,為微服務(wù)架構(gòu)提供了前門保護(hù)的作用,同時將權(quán)限控制這些較重的非業(yè)務(wù)邏輯內(nèi)容遷移到服務(wù)路由層面,使得服務(wù)集群主體能夠具備更高的可復(fù)用性和可測試性。
構(gòu)建服務(wù)網(wǎng)關(guān) api-gateway
可以通過如下的 Spring Assistant 插件來創(chuàng)建項目 api-gateway,添加 Zuul 等作為依賴。



在 pom.xml 中自動導(dǎo)入了如下的依賴:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
注意,如果是 Finchley 版本的 Spring Cloud,需要再添加如下依賴:
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.7.1</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.7.1</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
</dependency>
否則,啟動時會報如下的錯誤:
ERROR] Failed to execute goal org.springframework.boot:spring-boot-maven-plugin:2.0.3.RELEASE:run (default-cli) on project eureka-consumer: An exception occurred while running. null: InvocationTargetException: Error creating bean with name 'hystrixCommandAspect' defined in class path resource [org/springframework/cloud/netflix/hystrix/HystrixCircuitBreakerConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.netflix.hystrix.contrib.javanica.aop.aspectj.HystrixCommandAspect]: Factory method 'hystrixCommandAspect' threw exception; nested exception is java.lang.NoClassDefFoundError: org/aspectj/lang/JoinPoint: org.aspectj.lang.JoinPoint -> [Help 1]
在主程序中通過 @EnableZuulProxy 注解開啟 Zuul 的功能:
@SpringBootApplication
@EnableZuulProxy
public class ApiGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(ApiGatewayApplication.class, args);
}
}
在 application.properties,配置服務(wù)名,端口及 Eureka 服務(wù)注冊中心的地址:
spring.application.name=api-gateway
server.port=7001
eureka.client.serviceUrl.defaultZone=http://localhost:1234/eureka/
最后通過 mvn spring-boot:run 啟動該項目,它自己也作為一個服務(wù)注冊到 Eureka 服務(wù)注冊中心。它除了會將自己注冊到 Eureka 服務(wù)注冊中心上之外,也會從注冊中心獲取所有服務(wù)以及它們的實例清單。
因此服務(wù)網(wǎng)關(guān) Zuul 本身就已經(jīng)維護(hù)了系統(tǒng)中所有 serviceId 與實例地址的映射關(guān)系,例如,它知道 eureka-consumer 這個 serviceId 對應(yīng)到兩個地址:
當(dāng)有外部請求到達(dá)服務(wù)網(wǎng)關(guān) Zuul 的時候,根據(jù)請求的 URL 路徑找到最佳匹配的 path 規(guī)則,將該請求路由到哪個具體的serviceId 上去,并且通過 Ribbon 來實現(xiàn)負(fù)載均衡策略。

一個默認(rèn)的服務(wù)網(wǎng)關(guān)就構(gòu)建完畢了。由于 Spring Cloud Zuul 在整合了 Eureka 之后,具備默認(rèn)的服務(wù)路由功能,即:當(dāng)我們這里構(gòu)建的 api-gateway 應(yīng)用啟動并注冊到 Eureka 之后,服務(wù)網(wǎng)關(guān) Zull 會發(fā)現(xiàn)上面我們啟動的兩個服務(wù) eureka-client 和 eureka-consumer,這時候 Zuul 就會創(chuàng)建路由規(guī)則。
每個路由規(guī)則都包含兩部分,一部分是外部請求的匹配規(guī)則,另一部分是路由的服務(wù) ID。針對當(dāng)前示例的情況,Zuul 會創(chuàng)建下面的四個路由規(guī)則,其中:
- 轉(zhuǎn)發(fā)到
eureka-client服務(wù)的請求規(guī)則為:/eureka-client/** - 轉(zhuǎn)發(fā)到
eureka-consumer服務(wù)的請求規(guī)則為:/eureka-consumer/**

在之前的示例中,我們都是通過 http://127.0.0.1:3001/consumer 或者 http://127.0.0.1:3002/consumer 來調(diào)用 eureka-consumer 提供的服務(wù) /consumer。
在啟動了服務(wù)網(wǎng)關(guān)后,我們就可以通過 http://127.0.0.1:7001/eureka-consumer/consumer 來實現(xiàn)同樣的效果,該請求將最終被路由到 eureka-consumer的/consumer 接口上。

傳統(tǒng)路由配置
所謂的傳統(tǒng)路由配置方式就是在不依賴于服務(wù)發(fā)現(xiàn)機(jī)制的情況下,通過在配置文件中具體指定每個路由表達(dá)式與服務(wù)實例的映射關(guān)系來實現(xiàn) API 網(wǎng)關(guān)對外部請求的路由。
沒有 Eureka 服務(wù)治理框架幫助的時候,我們需要根據(jù)服務(wù)實例的數(shù)量采用不同方式的配置來實現(xiàn)路由規(guī)則。
單實例配置:
zuul.routes.eureka-consumer.path=/eureka-consumer/**
zuul.routes.eureka-consumer.url=http://127.0.0.1:3001/
多實例配置:由于存在多個實例,API 網(wǎng)關(guān)在進(jìn)行路由轉(zhuǎn)發(fā)時需要實現(xiàn)負(fù)載均衡策略,于是這里還需要 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/
不論是單實例還是多實例的配置方式,我們都需要為每一對映射關(guān)系指定一個名稱,也就是上面配置中的 <route>,每一個 <route> 就對應(yīng)了一條路由規(guī)則。
每條路由規(guī)則都需要通過 path 屬性來定義一個用來匹配客戶端請求的路徑表達(dá)式,并通過 url 或 serviceId 屬性來指定請求表達(dá)式映射具體實例地址或服務(wù)名。
服務(wù)路由配置
Spring Cloud Zuul 通過與 Spring Cloud Eureka 的整合,實現(xiàn)了對服務(wù)實例的自動化維護(hù),所以在使用服務(wù)路由配置的時候,我們不需要向傳統(tǒng)路由配置方式那樣為 serviceId 去指定具體的服務(wù)實例地址,只需要通過一組 zuul.routes.<route>.path 與 zuul.routes.<route>.serviceId 參數(shù)對的方式配置即可,例如:
zuul.routes.eureka-consumer.path=/eureka-consumer/**
zuul.routes.eureka-consumer.serviceId=eureka-consumer
對于面向服務(wù)的路由配置,除了使用 path 與 serviceId 映射的配置方式之外,還有一種更簡潔的配置方式:zuul.routes.<serviceId>=<path>,其中 <serviceId> 用來指定路由的具體服務(wù)名,<path>用來配置匹配的請求表達(dá)式,例如:
zuul.routes.eureka-consumer=/eureka-consumer/**
過濾器
思考這么一個問題:每個客戶端用戶請求微服務(wù)應(yīng)用提供的接口時,它們的訪問權(quán)限往往都需要有一定的限制,系統(tǒng)并不會將所有的微服務(wù)接口都對它們開放。為了實現(xiàn)對客戶端請求的安全校驗和權(quán)限控制,最簡單和粗暴的方法就是為每個微服務(wù)應(yīng)用都實現(xiàn)一套用于校驗簽名和鑒別權(quán)限的過濾器或攔截器。不過,這樣的做法并不可取,它會增加日后的系統(tǒng)維護(hù)難度,因為同一個系統(tǒng)中的各種校驗邏輯很多情況下都是大致相同或類似的,這樣的實現(xiàn)方式會使得相似的校驗邏輯代碼被分散到了各個微服務(wù)中去,冗余代碼的出現(xiàn)是我們不希望看到的。
對于這樣的問題,更好的做法是通過前置的網(wǎng)關(guān)服務(wù)來完成這些非業(yè)務(wù)性質(zhì)的校驗。由于網(wǎng)關(guān)服務(wù)的加入,外部客戶端訪問我們的系統(tǒng)已經(jīng)有了統(tǒng)一入口,既然這些校驗與具體業(yè)務(wù)無關(guān),那何不在請求到達(dá)的時候就完成校驗和過濾,而不是轉(zhuǎn)發(fā)后再過濾而導(dǎo)致更長的請求延遲。同時,通過在網(wǎng)關(guān)中完成校驗和過濾,微服務(wù)應(yīng)用端就可以去除各種復(fù)雜的過濾器和攔截器了,這使得微服務(wù)應(yīng)用的接口開發(fā)和測試復(fù)雜度也得到了相應(yīng)的降低。
Zuul 允許開發(fā)者在 API 網(wǎng)關(guān)上通過定義過濾器來實現(xiàn)對請求的攔截與過濾,實現(xiàn)的方法非常簡單,我們只需要繼承 ZuulFilter 抽象類并實現(xiàn)它定義的四個抽象函數(shù)就可以完成對請求的攔截和過濾了:
-
過濾類型
String filterType();在 Zuul 中默認(rèn)定義了四種不同生命周期的過濾器類型,具體如下:-
pre:可以在請求被路由之前調(diào)用。 -
routing:在路由請求時候被調(diào)用。 -
post:在routing和error過濾器之后被調(diào)用。 -
error:處理請求時發(fā)生錯誤時被調(diào)用。
-
-
執(zhí)行順序
int filterOrder();通過int值來定義過濾器的執(zhí)行順序,數(shù)值越小優(yōu)先級越高。 -
執(zhí)行條件
boolean shouldFilter();返回一個boolean類型來判斷該過濾器是否要執(zhí)行。我們可以通過此方法來指定過濾器的有效范圍。 -
具體操作
Object run();過濾器的具體邏輯。在該函數(shù)中,我們可以實現(xiàn)自定義的過濾邏輯,來確定是否要攔截當(dāng)前的請求,不對其進(jìn)行后續(xù)的路由,或是在請求路由返回結(jié)果之后,對處理結(jié)果做一些加工等。
路由功能在真正運行時,它的路由映射和請求轉(zhuǎn)發(fā)都是由幾個不同的過濾器完成的:
-
路由映射主要通過
pre類型的過濾器完成,它將請求路徑與配置的路由規(guī)則進(jìn)行匹配,以找到需要轉(zhuǎn)發(fā)的目標(biāo)地址; -
請求轉(zhuǎn)發(fā)由
route類型的過濾器來完成,對pre類型過濾器獲得的路由地址進(jìn)行轉(zhuǎn)發(fā)。
所以,過濾器可以說是 Zuul 實現(xiàn)服務(wù)網(wǎng)關(guān)功能最為核心的部件,每一個進(jìn)入 Zuul 的 HTTP 請求都會經(jīng)過一系列的過濾器處理鏈得到請求響應(yīng)并返回給客戶端。
圖片引自:http://blog.didispace.com/spring-cloud-source-zuul/

在服務(wù)網(wǎng)關(guān) api-gateway 中添加過濾器
我們在上面的項目 api-gateway 中創(chuàng)建 AccessFilter.java:
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.http.HttpServletRequest;
public class AccessFilter extends ZuulFilter {
private static Logger log = LoggerFactory.getLogger(AccessFilter.class);
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
log.info("send {} request to {}", request.getMethod(), request.getRequestURL().toString());
Object accessToken = request.getParameter("accessToken");
if (accessToken == null) {
log.warn("access token is empty");
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(401);
ctx.setResponseBody("unauthorized");
return null;
}
log.info("access token ok");
return null;
}
}
隨后在主程序中創(chuàng)建具體的 Bean:
@Bean
public AccessFilter accessFilter() {
return new AccessFilter();
}
重啟 api-gateway,訪問 http://127.0.0.1:7001/eureka-consumer/consumer:


加上 accessToken 參數(shù)訪問 http://127.0.0.1:7001/eureka-consumer/consumer?accessToken=12345:

核心過濾器

圖片引用自:http://blog.didispace.com/spring-cloud-zuul-exception-3/

拓展閱讀
引用自:
- Spring Cloud實戰(zhàn)小貼士:Zuul處理Cookie和重定向
- Spring Cloud實戰(zhàn)小貼士:Zuul統(tǒng)一異常處理(一)
- Spring Cloud實戰(zhàn)小貼士:Zuul統(tǒng)一異常處理(二)
- Spring Cloud實戰(zhàn)小貼士:Zuul統(tǒng)一異常處理(三)【Dalston版】
引用:
程序猿DD Spring Cloud基礎(chǔ)教程
Spring Cloud構(gòu)建微服務(wù)架構(gòu):服務(wù)網(wǎng)關(guān)(基礎(chǔ))【Dalston版】
Spring Cloud構(gòu)建微服務(wù)架構(gòu):服務(wù)網(wǎng)關(guān)(路由配置)【Dalston版】
Spring Cloud構(gòu)建微服務(wù)架構(gòu):服務(wù)網(wǎng)關(guān)(過濾器)【Dalston版】
Spring Cloud Dalston中文文檔