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

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

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

在之前的例子中汤锨,我們啟動(dòng)了一個(gè)外部服務(wù) eureka-consumer,端口 3001厌小。
同時(shí)我們也啟動(dòng)了兩個(gè)內(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è)我們啟動(dòng)了另外一個(gè)外部服務(wù) eureka-consumer揣钦,端口 3002。此時(shí)外部用戶只能通過 http://127.0.0.1:3002/consumer 來訪問漠酿,但是外部用戶可能并不知道 3002 這個(gè)端口冯凹。

服務(wù)網(wǎng)關(guān)是微服務(wù)架構(gòu)中一個(gè)不可或缺的部分。
通過服務(wù)網(wǎng)關(guān)統(tǒng)一向外系統(tǒng)提供 REST API 的過程中,除了具備服務(wù)路由宇姚、均衡負(fù)載功能之外匈庭,它還具備了權(quán)限控制等功能
Spring Cloud Netflix 中的 Zuul 就擔(dān)任了這樣的一個(gè)角色浑劳,為微服務(wù)架構(gòu)提供了前門保護(hù)的作用阱持,同時(shí)將權(quán)限控制這些較重的非業(yè)務(wù)邏輯內(nèi)容遷移到服務(wù)路由層面,使得服務(wù)集群主體能夠具備更高的可復(fù)用性和可測(cè)試性魔熏。

構(gòu)建服務(wù)網(wǎng)關(guān) api-gateway

可以通過如下的 Spring Assistant 插件來創(chuàng)建項(xiàng)目 api-gateway衷咽,添加 Zuul 等作為依賴。

api-gateway 的創(chuàng)建

api-gateway 的創(chuàng)建

api-gateway 的創(chuàng)建

pom.xml 中自動(dòng)導(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>

否則,啟動(dòng)時(shí)會(huì)報(bào)如下的錯(cuò)誤:

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ù)注冊(cè)中心的地址:

spring.application.name=api-gateway
server.port=7001

eureka.client.serviceUrl.defaultZone=http://localhost:1234/eureka/

最后通過 mvn spring-boot:run 啟動(dòng)該項(xiàng)目,它自己也作為一個(gè)服務(wù)注冊(cè)到 Eureka 服務(wù)注冊(cè)中心相赁。它除了會(huì)將自己注冊(cè)到 Eureka 服務(wù)注冊(cè)中心上之外相寇,也會(huì)從注冊(cè)中心獲取所有服務(wù)以及它們的實(shí)例清單。
因此服務(wù)網(wǎng)關(guān) Zuul 本身就已經(jīng)維護(hù)了系統(tǒng)中所有 serviceId 與實(shí)例地址的映射關(guān)系钮科,例如唤衫,它知道 eureka-consumer 這個(gè) serviceId 對(duì)應(yīng)到兩個(gè)地址:

當(dāng)有外部請(qǐng)求到達(dá)服務(wù)網(wǎng)關(guān) Zuul 的時(shí)候,根據(jù)請(qǐng)求的 URL 路徑找到最佳匹配的 path 規(guī)則跺嗽,將該請(qǐng)求路由到哪個(gè)具體的serviceId 上去战授,并且通過 Ribbon 來實(shí)現(xiàn)負(fù)載均衡策略。

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

一個(gè)默認(rèn)的服務(wù)網(wǎng)關(guān)就構(gòu)建完畢了桨嫁。由于 Spring Cloud Zuul 在整合了 Eureka 之后,具備默認(rèn)的服務(wù)路由功能份帐,即:當(dāng)我們這里構(gòu)建的 api-gateway 應(yīng)用啟動(dòng)并注冊(cè)到 Eureka 之后璃吧,服務(wù)網(wǎng)關(guān) Zull 會(huì)發(fā)現(xiàn)上面我們啟動(dòng)的兩個(gè)服務(wù) eureka-clienteureka-consumer,這時(shí)候 Zuul 就會(huì)創(chuàng)建路由規(guī)則废境。
每個(gè)路由規(guī)則都包含兩部分畜挨,一部分是外部請(qǐng)求的匹配規(guī)則,另一部分是路由的服務(wù) ID噩凹。針對(duì)當(dāng)前示例的情況巴元,Zuul 會(huì)創(chuàng)建下面的四個(gè)路由規(guī)則,其中:

  • 轉(zhuǎn)發(fā)到 eureka-client 服務(wù)的請(qǐng)求規(guī)則為:/eureka-client/**
  • 轉(zhuǎn)發(fā)到 eureka-consumer 服務(wù)的請(qǐng)求規(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逮刨。
在啟動(dòng)了服務(wù)網(wǎng)關(guān)后,我們就可以通過 http://127.0.0.1:7001/eureka-consumer/consumer 來實(shí)現(xiàn)同樣的效果堵泽,該請(qǐng)求將最終被路由到 eureka-consumer/consumer 接口上修己。

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

傳統(tǒng)路由配置

所謂的傳統(tǒng)路由配置方式就是在不依賴于服務(wù)發(fā)現(xiàn)機(jī)制的情況下恢总,通過在配置文件中具體指定每個(gè)路由表達(dá)式與服務(wù)實(shí)例的映射關(guān)系來實(shí)現(xiàn) API 網(wǎng)關(guān)對(duì)外部請(qǐng)求的路由。

沒有 Eureka 服務(wù)治理框架幫助的時(shí)候睬愤,我們需要根據(jù)服務(wù)實(shí)例的數(shù)量采用不同方式的配置來實(shí)現(xiàn)路由規(guī)則片仿。
單實(shí)例配置:

zuul.routes.eureka-consumer.path=/eureka-consumer/**
zuul.routes.eureka-consumer.url=http://127.0.0.1:3001/

多實(shí)例配置:由于存在多個(gè)實(shí)例,API 網(wǎng)關(guān)在進(jìn)行路由轉(zhuǎn)發(fā)時(shí)需要實(shí)現(xiàn)負(fù)載均衡策略尤辱,于是這里還需要 Spring Cloud Ribbon 的配合砂豌。由于在 Spring Cloud Zuul 中自帶了對(duì) 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/

不論是單實(shí)例還是多實(shí)例的配置方式奸鸯,我們都需要為每一對(duì)映射關(guān)系指定一個(gè)名稱,也就是上面配置中的 <route>可帽,每一個(gè) <route> 就對(duì)應(yīng)了一條路由規(guī)則娄涩。
每條路由規(guī)則都需要通過 path 屬性來定義一個(gè)用來匹配客戶端請(qǐng)求的路徑表達(dá)式,并通過 urlserviceId 屬性來指定請(qǐng)求表達(dá)式映射具體實(shí)例地址或服務(wù)名映跟。

服務(wù)路由配置

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

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

對(duì)于面向服務(wù)的路由配置荸镊,除了使用 pathserviceId 映射的配置方式之外咽斧,還有一種更簡(jiǎn)潔的配置方式:zuul.routes.<serviceId>=<path>,其中 <serviceId> 用來指定路由的具體服務(wù)名躬存,<path>用來配置匹配的請(qǐng)求表達(dá)式张惹,例如:

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

過濾器

思考這么一個(gè)問題:每個(gè)客戶端用戶請(qǐng)求微服務(wù)應(yīng)用提供的接口時(shí),它們的訪問權(quán)限往往都需要有一定的限制岭洲,系統(tǒng)并不會(huì)將所有的微服務(wù)接口都對(duì)它們開放宛逗。為了實(shí)現(xiàn)對(duì)客戶端請(qǐng)求的安全校驗(yàn)和權(quán)限控制,最簡(jiǎn)單和粗暴的方法就是為每個(gè)微服務(wù)應(yīng)用都實(shí)現(xiàn)一套用于校驗(yàn)簽名和鑒別權(quán)限的過濾器或攔截器盾剩。不過雷激,這樣的做法并不可取,它會(huì)增加日后的系統(tǒng)維護(hù)難度告私,因?yàn)橥粋€(gè)系統(tǒng)中的各種校驗(yàn)邏輯很多情況下都是大致相同或類似的屎暇,這樣的實(shí)現(xiàn)方式會(huì)使得相似的校驗(yàn)邏輯代碼被分散到了各個(gè)微服務(wù)中去,冗余代碼的出現(xiàn)是我們不希望看到的驻粟。

對(duì)于這樣的問題根悼,更好的做法是通過前置的網(wǎng)關(guān)服務(wù)來完成這些非業(yè)務(wù)性質(zhì)的校驗(yàn)。由于網(wǎng)關(guān)服務(wù)的加入,外部客戶端訪問我們的系統(tǒng)已經(jīng)有了統(tǒng)一入口番挺,既然這些校驗(yàn)與具體業(yè)務(wù)無關(guān)唠帝,那何不在請(qǐng)求到達(dá)的時(shí)候就完成校驗(yàn)和過濾,而不是轉(zhuǎn)發(fā)后再過濾而導(dǎo)致更長(zhǎng)的請(qǐng)求延遲玄柏。同時(shí)襟衰,通過在網(wǎng)關(guān)中完成校驗(yàn)和過濾佑力,微服務(wù)應(yīng)用端就可以去除各種復(fù)雜的過濾器和攔截器了仲智,這使得微服務(wù)應(yīng)用的接口開發(fā)和測(cè)試復(fù)雜度也得到了相應(yīng)的降低叉钥。

Zuul 允許開發(fā)者在 API 網(wǎng)關(guān)上通過定義過濾器來實(shí)現(xiàn)對(duì)請(qǐng)求的攔截與過濾茄厘,實(shí)現(xiàn)的方法非常簡(jiǎn)單,我們只需要繼承 ZuulFilter 抽象類并實(shí)現(xiàn)它定義的四個(gè)抽象函數(shù)就可以完成對(duì)請(qǐng)求的攔截和過濾了:

  • 過濾類型 String filterType(); 在 Zuul 中默認(rèn)定義了四種不同生命周期的過濾器類型爹谭,具體如下:
    • pre:可以在請(qǐng)求被路由之前調(diào)用介时。
    • routing:在路由請(qǐng)求時(shí)候被調(diào)用报咳。
    • post:在routing和error過濾器之后被調(diào)用椎咧。
    • error:處理請(qǐng)求時(shí)發(fā)生錯(cuò)誤時(shí)被調(diào)用玖详。
  • 執(zhí)行順序 int filterOrder(); 通過 int 值來定義過濾器的執(zhí)行順序,數(shù)值越小優(yōu)先級(jí)越高勤讽。
  • 執(zhí)行條件 boolean shouldFilter(); 返回一個(gè) boolean 類型來判斷該過濾器是否要執(zhí)行蟋座。我們可以通過此方法來指定過濾器的有效范圍。
  • 具體操作 Object run(); 過濾器的具體邏輯脚牍。在該函數(shù)中向臀,我們可以實(shí)現(xiàn)自定義的過濾邏輯,來確定是否要攔截當(dāng)前的請(qǐng)求诸狭,不對(duì)其進(jìn)行后續(xù)的路由券膀,或是在請(qǐng)求路由返回結(jié)果之后,對(duì)處理結(jié)果做一些加工等驯遇。

路由功能在真正運(yùn)行時(shí)芹彬,它的路由映射和請(qǐng)求轉(zhuǎn)發(fā)都是由幾個(gè)不同的過濾器完成的:

  • 路由映射主要通過 pre 類型的過濾器完成,它將請(qǐng)求路徑與配置的路由規(guī)則進(jìn)行匹配妹懒,以找到需要轉(zhuǎn)發(fā)的目標(biāo)地址雀监;
  • 請(qǐng)求轉(zhuǎn)發(fā)route 類型的過濾器來完成,對(duì) pre 類型過濾器獲得的路由地址進(jìn)行轉(zhuǎn)發(fā)眨唬。

所以,過濾器可以說是 Zuul 實(shí)現(xiàn)服務(wù)網(wǎng)關(guān)功能最為核心的部件好乐,每一個(gè)進(jìn)入 Zuul 的 HTTP 請(qǐng)求都會(huì)經(jīng)過一系列的過濾器處理鏈得到請(qǐng)求響應(yīng)并返回給客戶端匾竿。

圖片引自:http://blog.didispace.com/spring-cloud-source-zuul/

請(qǐng)求生命周期

在服務(wù)網(wǎng)關(guān) api-gateway 中添加過濾器

我們?cè)谏厦娴捻?xiàng)目 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)錯(cuò)誤

401 未授權(quán)錯(cuò)誤

加上 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)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末蔚万,一起剝皮案震驚了整個(gè)濱河市岭妖,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖昵慌,帶你破解...
    沈念sama閱讀 217,542評(píng)論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件假夺,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡斋攀,警方通過查閱死者的電腦和手機(jī)已卷,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來淳蔼,“玉大人侧蘸,你說我怎么就攤上這事○睦妫” “怎么了讳癌?”我有些...
    開封第一講書人閱讀 163,912評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)存皂。 經(jīng)常有香客問我晌坤,道長(zhǎng),這世上最難降的妖魔是什么旦袋? 我笑而不...
    開封第一講書人閱讀 58,449評(píng)論 1 293
  • 正文 為了忘掉前任骤菠,我火速辦了婚禮,結(jié)果婚禮上猜憎,老公的妹妹穿的比我還像新娘娩怎。我一直安慰自己,他們只是感情好胰柑,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,500評(píng)論 6 392
  • 文/花漫 我一把揭開白布截亦。 她就那樣靜靜地躺著,像睡著了一般柬讨。 火紅的嫁衣襯著肌膚如雪崩瓤。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,370評(píng)論 1 302
  • 那天踩官,我揣著相機(jī)與錄音却桶,去河邊找鬼。 笑死蔗牡,一個(gè)胖子當(dāng)著我的面吹牛颖系,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播辩越,決...
    沈念sama閱讀 40,193評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼嘁扼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了黔攒?” 一聲冷哼從身側(cè)響起趁啸,我...
    開封第一講書人閱讀 39,074評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤强缘,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后不傅,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體旅掂,經(jīng)...
    沈念sama閱讀 45,505評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,722評(píng)論 3 335
  • 正文 我和宋清朗相戀三年访娶,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了商虐。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,841評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡震肮,死狀恐怖称龙,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情戳晌,我是刑警寧澤鲫尊,帶...
    沈念sama閱讀 35,569評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站沦偎,受9級(jí)特大地震影響疫向,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜豪嚎,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,168評(píng)論 3 328
  • 文/蒙蒙 一搔驼、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧侈询,春花似錦舌涨、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,783評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至革为,卻和暖如春扭粱,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背震檩。 一陣腳步聲響...
    開封第一講書人閱讀 32,918評(píng)論 1 269
  • 我被黑心中介騙來泰國打工琢蛤, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人抛虏。 一個(gè)月前我還...
    沈念sama閱讀 47,962評(píng)論 2 370
  • 正文 我出身青樓博其,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國和親迂猴。 傳聞我的和親對(duì)象是個(gè)殘疾皇子贺奠,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,781評(píng)論 2 354