Spring Cloud構(gòu)建微服務(wù)架構(gòu)(五)服務(wù)網(wǎng)關(guān)

通過(guò)之前幾篇Spring Cloud中幾個(gè)核心組件的介紹开皿,我們已經(jīng)可以構(gòu)建一個(gè)簡(jiǎn)略的(不夠完善)微服務(wù)架構(gòu)了髓涯。比如下圖所示:

alt

我們使用Spring Cloud Netflix中的Eureka實(shí)現(xiàn)了服務(wù)注冊(cè)中心以及服務(wù)注冊(cè)與發(fā)現(xiàn)群发;而服務(wù)間通過(guò)Ribbon或Feign實(shí)現(xiàn)服務(wù)的消費(fèi)以及均衡負(fù)載媒区;通過(guò)Spring Cloud Config實(shí)現(xiàn)了應(yīng)用多環(huán)境的外部化配置以及版本管理纱控。為了使得服務(wù)集群更為健壯期贫,使用Hystrix的融斷機(jī)制來(lái)避免在微服務(wù)架構(gòu)中個(gè)別服務(wù)出現(xiàn)異常時(shí)引起的故障蔓延。

在該架構(gòu)中赞赖,我們的服務(wù)集群包含:內(nèi)部服務(wù)Service A和Service B滚朵,他們都會(huì)注冊(cè)與訂閱服務(wù)至Eureka Server,而Open Service是一個(gè)對(duì)外的服務(wù)前域,通過(guò)均衡負(fù)載公開至服務(wù)調(diào)用方始绍。本文我們把焦點(diǎn)聚集在對(duì)外服務(wù)這塊,這樣的實(shí)現(xiàn)是否合理话侄,或者是否有更好的實(shí)現(xiàn)方式呢亏推?

先來(lái)說(shuō)說(shuō)這樣架構(gòu)需要做的一些事兒以及存在的不足:

  • 首先学赛,破壞了服務(wù)無(wú)狀態(tài)特點(diǎn)。為了保證對(duì)外服務(wù)的安全性吞杭,我們需要實(shí)現(xiàn)對(duì)服務(wù)訪問(wèn)的權(quán)限控制盏浇,而開放服務(wù)的權(quán)限控制機(jī)制將會(huì)貫穿并污染整個(gè)開放服務(wù)的業(yè)務(wù)邏輯,這會(huì)帶來(lái)的最直接問(wèn)題是芽狗,破壞了服務(wù)集群中REST API無(wú)狀態(tài)的特點(diǎn)绢掰。從具體開發(fā)和測(cè)試的角度來(lái)說(shuō),在工作中除了要考慮實(shí)際的業(yè)務(wù)邏輯之外童擎,還需要額外可續(xù)對(duì)接口訪問(wèn)的控制處理滴劲。
  • 其次,無(wú)法直接復(fù)用既有接口顾复。當(dāng)我們需要對(duì)一個(gè)即有的集群內(nèi)訪問(wèn)接口班挖,實(shí)現(xiàn)外部服務(wù)訪問(wèn)時(shí),我們不得不通過(guò)在原有接口上增加校驗(yàn)邏輯芯砸,或增加一個(gè)代理調(diào)用來(lái)實(shí)現(xiàn)權(quán)限控制萧芙,無(wú)法直接復(fù)用原有的接口。

面對(duì)類似上面的問(wèn)題假丧,我們要如何解決呢双揪?下面進(jìn)入本文的正題:服務(wù)網(wǎng)關(guān)!

為了解決上面這些問(wèn)題包帚,我們需要將權(quán)限控制這樣的東西從我們的服務(wù)單元中抽離出去渔期,而最適合這些邏輯的地方就是處于對(duì)外訪問(wèn)最前端的地方,我們需要一個(gè)更強(qiáng)大一些的均衡負(fù)載器渴邦,它就是本文將來(lái)介紹的:服務(wù)網(wǎng)關(guān)疯趟。

服務(wù)網(wǎng)關(guān)是微服務(wù)架構(gòu)中一個(gè)不可或缺的部分。通過(guò)服務(wù)網(wǎng)關(guān)統(tǒng)一向外系統(tǒng)提供REST API的過(guò)程中几莽,除了具備服務(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è)試性。

下面我們通過(guò)實(shí)例例子來(lái)使用一下Zuul來(lái)作為服務(wù)的路有功能磷账。

準(zhǔn)備工作

在使用Zuul之前峭沦,我們先構(gòu)建一個(gè)服務(wù)注冊(cè)中心、以及兩個(gè)簡(jiǎn)單的服務(wù)逃糟,比如:我構(gòu)建了一個(gè)service-A吼鱼,一個(gè)service-B蓬豁。然后啟動(dòng)eureka-server和這兩個(gè)服務(wù)。通過(guò)訪問(wèn)eureka-server菇肃,我們可以看到service-A和service-B已經(jīng)注冊(cè)到了服務(wù)中心地粪。

alt

如果您還不熟悉如何構(gòu)建服務(wù)中心和注冊(cè)服務(wù),請(qǐng)先閱讀Spring Cloud構(gòu)建微服務(wù)架構(gòu)(一)服務(wù)注冊(cè)與發(fā)現(xiàn)琐谤。

如果您不想自己動(dòng)手準(zhǔn)備蟆技,可以從這里獲取示例代碼:http://git.oschina.net/didispace/SpringBoot-Learning

開始使用Zuul

  • 引入依賴spring-cloud-starter-zuul、spring-cloud-starter-eureka斗忌,如果不是通過(guò)指定serviceId的方式质礼,eureka依賴不需要,但是為了對(duì)服務(wù)集群細(xì)節(jié)的透明性织阳,還是用serviceId來(lái)避免直接引用url的方式吧眶蕉。
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-zuul</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
  • 應(yīng)用主類使用@EnableZuulProxy注解開啟Zuul
@EnableZuulProxy
@SpringCloudApplication
public class Application {

    public static void main(String[] args) {
        new SpringApplicationBuilder(Application.class).web(true).run(args);
    }

}

這里用了@SpringCloudApplication注解,之前沒(méi)有提過(guò)陈哑,通過(guò)源碼我們看到妻坝,它整合了@SpringBootApplication@EnableDiscoveryClient惊窖、@EnableCircuitBreaker刽宪,主要目的還是簡(jiǎn)化配置。這幾個(gè)注解的具體作用這里就不做詳細(xì)介紹了界酒,之前的文章已經(jīng)都介紹過(guò)圣拄。

  • application.properties中配置Zuul應(yīng)用的基礎(chǔ)信息,如:應(yīng)用名毁欣、服務(wù)端口等庇谆。
spring.application.name=api-gateway
server.port=5555

Zuul配置

完成上面的工作后,Zuul已經(jīng)可以運(yùn)行了凭疮,但是如何讓它為我們的微服務(wù)集群服務(wù)饭耳,還需要我們另行配置,下面詳細(xì)的介紹一些常用配置內(nèi)容执解。

服務(wù)路由

通過(guò)服務(wù)路由的功能寞肖,我們?cè)趯?duì)外提供服務(wù)的時(shí)候,只需要通過(guò)暴露Zuul中配置的調(diào)用地址就可以讓調(diào)用方統(tǒng)一的來(lái)訪問(wèn)我們的服務(wù)衰腌,而不需要了解具體提供服務(wù)的主機(jī)信息了新蟆。

在Zuul中提供了兩種映射方式:

  • 通過(guò)url直接映射,我們可以如下配置:
# routes to url
zuul.routes.api-a-url.path=/api-a-url/**
zuul.routes.api-a-url.url=http://localhost:2222/

該配置右蕊,定義了琼稻,所有到Zuul的中規(guī)則為:/api-a-url/**的訪問(wèn)都映射到http://localhost:2222/上,也就是說(shuō)當(dāng)我們?cè)L問(wèn)http://localhost:5555/api-a-url/add?a=1&b=2的時(shí)候饶囚,Zuul會(huì)將該請(qǐng)求路由到:http://localhost:2222/add?a=1&b=2上帕翻。

其中鸠补,配置屬性zuul.routes.api-a-url.path中的api-a-url部分為路由的名字,可以任意定義嘀掸,但是一組映射關(guān)系的path和url要相同莫鸭,下面講serviceId時(shí)候也是如此。

  • 通過(guò)serviceId映射横殴,通過(guò)url映射的方式對(duì)于Zuul來(lái)說(shuō)被因,并不是特別友好,Zuul需要知道我們所有為服務(wù)的地址衫仑,才能完成所有的映射配置梨与。而實(shí)際上,我們?cè)趯?shí)現(xiàn)微服務(wù)架構(gòu)是文狱,服務(wù)名與服務(wù)實(shí)例地址的關(guān)系在eureka server中已經(jīng)存在了粥鞋,所以只需要將Zuul注冊(cè)到eureka server上去發(fā)現(xiàn)其他服務(wù),我們就可以實(shí)現(xiàn)對(duì)serviceId的映射瞄崇。例如呻粹,我們可以如下配置:

zuul.routes.api-a.path=/api-a/**
zuul.routes.api-a.serviceId=service-A

zuul.routes.api-b.path=/api-b/**
zuul.routes.api-b.serviceId=service-B

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

針對(duì)我們?cè)跍?zhǔn)備工作中實(shí)現(xiàn)的兩個(gè)微服務(wù)service-A和service-B,定義了兩個(gè)路由api-a和api-b來(lái)分別映射苏研。另外為了讓Zuul能發(fā)現(xiàn)service-A和service-B等浊,也加入了eureka的配置。

接下來(lái)摹蘑,我們將eureka-server筹燕、service-A、service-B以及這里用Zuul實(shí)現(xiàn)的服務(wù)網(wǎng)關(guān)啟動(dòng)起來(lái)衅鹿,在eureka-server的控制頁(yè)面中撒踪,我們可以看到分別注冊(cè)了service-A、service-B以及api-gateway

alt

嘗試通過(guò)服務(wù)網(wǎng)關(guān)來(lái)訪問(wèn)service-A和service-B大渤,根據(jù)配置的映射關(guān)系制妄,分別訪問(wèn)下面的url

  • http://localhost:5555/api-a/add?a=1&b=2:通過(guò)serviceId映射訪問(wèn)service-A中的add服務(wù)
  • http://localhost:5555/api-b/add?a=1&b=2:通過(guò)serviceId映射訪問(wèn)service-B中的add服務(wù)
  • http://localhost:5555/api-a-url/add?a=1&b=2:通過(guò)url映射訪問(wèn)service-A中的add服務(wù)

推薦使用serviceId的映射方式,除了對(duì)Zuul維護(hù)上更加友好之外泵三,serviceId映射方式還支持了斷路器耕捞,對(duì)于服務(wù)故障的情況下,可以有效的防止故障蔓延到服務(wù)網(wǎng)關(guān)上而影響整個(gè)系統(tǒng)的對(duì)外服務(wù)

服務(wù)過(guò)濾

在完成了服務(wù)路由之后切黔,我們對(duì)外開放服務(wù)還需要一些安全措施來(lái)保護(hù)客戶端只能訪問(wèn)它應(yīng)該訪問(wèn)到的資源砸脊。所以我們需要利用Zuul的過(guò)濾器來(lái)實(shí)現(xiàn)我們對(duì)外服務(wù)的安全控制具篇。

在服務(wù)網(wǎng)關(guān)中定義過(guò)濾器只需要繼承ZuulFilter抽象類實(shí)現(xiàn)其定義的四個(gè)抽象函數(shù)就可對(duì)請(qǐng)求進(jìn)行攔截與過(guò)濾纬霞。

比如下面的例子,定義了一個(gè)Zuul過(guò)濾器驱显,實(shí)現(xiàn)了在請(qǐng)求被路由之前檢查請(qǐng)求中是否有accessToken參數(shù)诗芜,若有就進(jìn)行路由瞳抓,若沒(méi)有就拒絕訪問(wèn),返回401 Unauthorized錯(cuò)誤伏恐。


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(String.format("%s request to %s", 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);
            return null;
        }
        log.info("access token ok");
        return null;
    }

}

自定義過(guò)濾器的實(shí)現(xiàn)孩哑,需要繼承ZuulFilter,需要重寫實(shí)現(xiàn)下面四個(gè)方法:

  • filterType:返回一個(gè)字符串代表過(guò)濾器的類型翠桦,在zuul中定義了四種不同生命周期的過(guò)濾器類型横蜒,具體如下:
  • pre:可以在請(qǐng)求被路由之前調(diào)用
  • routing:在路由請(qǐng)求時(shí)候被調(diào)用
  • post:在routing和error過(guò)濾器之后被調(diào)用
  • error:處理請(qǐng)求時(shí)發(fā)生錯(cuò)誤時(shí)被調(diào)用
  • filterOrder:通過(guò)int值來(lái)定義過(guò)濾器的執(zhí)行順序
  • shouldFilter:返回一個(gè)boolean類型來(lái)判斷該過(guò)濾器是否要執(zhí)行,所以通過(guò)此函數(shù)可實(shí)現(xiàn)過(guò)濾器的開關(guān)销凑。在上例中丛晌,我們直接返回true,所以該過(guò)濾器總是生效斗幼。
  • run:過(guò)濾器的具體邏輯澎蛛。需要注意,這里我們通過(guò)ctx.setSendZuulResponse(false)令zuul過(guò)濾該請(qǐng)求蜕窿,不對(duì)其進(jìn)行路由谋逻,然后通過(guò)ctx.setResponseStatusCode(401)設(shè)置了其返回的錯(cuò)誤碼,當(dāng)然我們也可以進(jìn)一步優(yōu)化我們的返回桐经,比如毁兆,通過(guò)ctx.setResponseBody(body)對(duì)返回body內(nèi)容進(jìn)行編輯等。

在實(shí)現(xiàn)了自定義過(guò)濾器之后阴挣,還需要實(shí)例化該過(guò)濾器才能生效荧恍,我們只需要在應(yīng)用主類中增加如下內(nèi)容:

@EnableZuulProxy
@SpringCloudApplication
public class Application {

    public static void main(String[] args) {
        new SpringApplicationBuilder(Application.class).web(true).run(args);
    }

    @Bean
    public AccessFilter accessFilter() {
        return new AccessFilter();
    }

}

啟動(dòng)該服務(wù)網(wǎng)關(guān)后,訪問(wèn):

  • http://localhost:5555/api-a/add?a=1&b=2:返回401錯(cuò)誤
  • http://localhost:5555/api-a/add?a=1&b=2&accessToken=token:正確路由到server-A屯吊,并返回計(jì)算內(nèi)容

對(duì)于其他一些過(guò)濾類型送巡,這里就不一一展開了,根據(jù)之前對(duì)filterType生命周期介紹盒卸,可以參考下圖去理解骗爆,并根據(jù)自己的需要在不同的生命周期中去實(shí)現(xiàn)不同類型的過(guò)濾器。

alt

最后蔽介,總結(jié)一下為什么服務(wù)網(wǎng)關(guān)是微服務(wù)架構(gòu)的重要部分稼跳,是我們必須要去做的原因:

  • 不僅僅實(shí)現(xiàn)了路由功能來(lái)屏蔽諸多服務(wù)細(xì)節(jié),更實(shí)現(xiàn)了服務(wù)級(jí)別流强、均衡負(fù)載的路由蜡感。
  • 實(shí)現(xiàn)了接口權(quán)限校驗(yàn)與微服務(wù)業(yè)務(wù)邏輯的解耦。通過(guò)服務(wù)網(wǎng)關(guān)中的過(guò)濾器薇组,在各生命周期中去校驗(yàn)請(qǐng)求的內(nèi)容外臂,將原本在對(duì)外服務(wù)層做的校驗(yàn)前移,保證了微服務(wù)的無(wú)狀態(tài)性律胀,同時(shí)降低了微服務(wù)的測(cè)試難度宋光,讓服務(wù)本身更集中關(guān)注業(yè)務(wù)邏輯的處理貌矿。
  • 實(shí)現(xiàn)了斷路器,不會(huì)因?yàn)榫唧w微服務(wù)的故障而導(dǎo)致服務(wù)網(wǎng)關(guān)的阻塞罪佳,依然可以對(duì)外服務(wù)逛漫。

本文完整示例可參考:Chapter9-1-5

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市赘艳,隨后出現(xiàn)的幾起案子酌毡,更是在濱河造成了極大的恐慌,老刑警劉巖蕾管,帶你破解...
    沈念sama閱讀 216,324評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件阔馋,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡娇掏,警方通過(guò)查閱死者的電腦和手機(jī)呕寝,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)婴梧,“玉大人下梢,你說(shuō)我怎么就攤上這事∪洌” “怎么了孽江?”我有些...
    開封第一講書人閱讀 162,328評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)番电。 經(jīng)常有香客問(wèn)我岗屏,道長(zhǎng),這世上最難降的妖魔是什么漱办? 我笑而不...
    開封第一講書人閱讀 58,147評(píng)論 1 292
  • 正文 為了忘掉前任这刷,我火速辦了婚禮,結(jié)果婚禮上娩井,老公的妹妹穿的比我還像新娘暇屋。我一直安慰自己,他們只是感情好洞辣,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,160評(píng)論 6 388
  • 文/花漫 我一把揭開白布咐刨。 她就那樣靜靜地躺著,像睡著了一般扬霜。 火紅的嫁衣襯著肌膚如雪定鸟。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,115評(píng)論 1 296
  • 那天著瓶,我揣著相機(jī)與錄音联予,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛躯泰,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播华糖,決...
    沈念sama閱讀 40,025評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼麦向,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了客叉?” 一聲冷哼從身側(cè)響起诵竭,我...
    開封第一講書人閱讀 38,867評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎兼搏,沒(méi)想到半個(gè)月后卵慰,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,307評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡佛呻,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,528評(píng)論 2 332
  • 正文 我和宋清朗相戀三年裳朋,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片吓著。...
    茶點(diǎn)故事閱讀 39,688評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡鲤嫡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出绑莺,到底是詐尸還是另有隱情暖眼,我是刑警寧澤,帶...
    沈念sama閱讀 35,409評(píng)論 5 343
  • 正文 年R本政府宣布纺裁,位于F島的核電站诫肠,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏欺缘。R本人自食惡果不足惜栋豫,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,001評(píng)論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望谚殊。 院中可真熱鬧笼才,春花似錦、人聲如沸络凿。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,657評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)絮记。三九已至摔踱,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間怨愤,已是汗流浹背派敷。 一陣腳步聲響...
    開封第一講書人閱讀 32,811評(píng)論 1 268
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人篮愉。 一個(gè)月前我還...
    沈念sama閱讀 47,685評(píng)論 2 368
  • 正文 我出身青樓腐芍,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親试躏。 傳聞我的和親對(duì)象是個(gè)殘疾皇子猪勇,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,573評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容