Spring Cloud 學(xué)習(xí)筆記 - No.5 服務(wù)網(wǎng)關(guān) Zuul

請先閱讀之前的內(nèi)容:

什么是服務(wù)網(wǎng)關(guān)

在之前的例子中,我們啟動了一個外部服務(wù) eureka-consumer,端口 3001。
同時我們也啟動了兩個內(nèi)部服務(wù) eureka-client,端口 20012002,該外部服務(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 等作為依賴。

api-gateway 的創(chuàng)建

api-gateway 的創(chuàng)建

api-gateway 的創(chuàng)建

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ù)載均衡策略。

http://127.0.0.1:1234/ Eureka 服務(wù)注冊中心

一個默認(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-clienteureka-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/**
Zuul 創(chuàng)建的路由規(guī)則

在之前的示例中,我們都是通過 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 接口上。

我們就可以通過 http://127.0.0.1:7001/eureka-consumer/consumer 來實現(xiàn)同樣的效果

傳統(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á)式,并通過 urlserviceId 屬性來指定請求表達(dá)式映射具體實例地址或服務(wù)名。

服務(wù)路由配置

Spring Cloud Zuul 通過與 Spring Cloud Eureka 的整合,實現(xiàn)了對服務(wù)實例的自動化維護(hù),所以在使用服務(wù)路由配置的時候,我們不需要向傳統(tǒng)路由配置方式那樣為 serviceId 去指定具體的服務(wù)實例地址,只需要通過一組 zuul.routes.<route>.pathzuul.routes.<route>.serviceId 參數(shù)對的方式配置即可,例如:

zuul.routes.eureka-consumer.path=/eureka-consumer/**
zuul.routes.eureka-consumer.serviceId=eureka-consumer

對于面向服務(wù)的路由配置,除了使用 pathserviceId 映射的配置方式之外,還有一種更簡潔的配置方式: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

401 未授權(quán)錯誤

401 未授權(quán)錯誤

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

正常訪問

核心過濾器

核心過濾器

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

Spring Cloud Zuul 自帶的核心過濾器

拓展閱讀

引用自:


引用:
程序猿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中文文檔

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容