javascript
Spring Cloud构建微服务架构(五)服务网关
通過(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的方式吧。
|
? 1 2 3 4 5 6 7 8 9 |
? <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
|
? 1 2 3 4 5 6 7 8 9 |
? @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ù)端口等。
|
? 1 2 |
? 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直接映射,我們可以如下配置:
|
? 1 2 3 |
? # 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ò)url映射的方式對(duì)于Zuul來(lái)說(shuō),并不是特別友好,Zuul需要知道我們所有為服務(wù)的地址,才能完成所有的映射配置。而實(shí)際上,我們?cè)趯?shí)現(xiàn)微服務(wù)架構(gòu)時(shí),服務(wù)名與服務(wù)實(shí)例地址的關(guān)系在eureka server中已經(jīng)存在了,所以只需要將Zuul注冊(cè)到eureka server上去發(fā)現(xiàn)其他服務(wù),我們就可以實(shí)現(xiàn)對(duì)serviceId的映射。例如,我們可以如下配置:
|
? 1 2 3 4 5 6 7 8 |
? 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ò)誤。
|
? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
? 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)容:
|
? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
? @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
【轉(zhuǎn)載請(qǐng)注明出處】:http://blog.didispace.com/springcloud5/
總結(jié)
以上是生活随笔為你收集整理的Spring Cloud构建微服务架构(五)服务网关的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: Spring Cloud构建微服务架构(
- 下一篇: Spring Cloud构建微服务架构(