原文鏈接:http://blog.didispace.com/springcloud5/
通過之前幾篇Spring Cloud中幾個(gè)核心組件的介紹,我們已經(jīng)可以構(gòu)建一個(gè)簡略的(不夠完善)微服務(wù)架構(gòu)了。比如下圖所示:
alt
我們使用Spring Cloud Netflix中的Eureka實(shí)現(xiàn)了服務(wù)注冊(cè)中心以及服務(wù)注冊(cè)與發(fā)現(xiàn);而服務(wù)間通過Ribbon或Feign實(shí)現(xiàn)服務(wù)的消費(fèi)以及均衡負(fù)載;通過Spring Cloud Config實(shí)現(xiàn)了應(yīng)用多環(huán)境的外部化配置以及版本管理。為了使得服務(wù)集群更為健壯,使用Hystrix的融斷機(jī)制來避免在微服務(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ù),通過均衡負(fù)載公開至服務(wù)調(diào)用方。本文我們把焦點(diǎn)聚集在對(duì)外服務(wù)這塊,這樣的實(shí)現(xiàn)是否合理,或者是否有更好的實(shí)現(xiàn)方式呢?
先來說說這樣架構(gòu)需要做的一些事兒以及存在的不足:
首先,破壞了服務(wù)無狀態(tài)特點(diǎn)。為了保證對(duì)外服務(wù)的安全性,我們需要實(shí)現(xiàn)對(duì)服務(wù)訪問的權(quán)限控制,而開放服務(wù)的權(quán)限控制機(jī)制將會(huì)貫穿并污染整個(gè)開放服務(wù)的業(yè)務(wù)邏輯,這會(huì)帶來的最直接問題是,破壞了服務(wù)集群中REST API無狀態(tài)的特點(diǎn)。從具體開發(fā)和測(cè)試的角度來說,在工作中除了要考慮實(shí)際的業(yè)務(wù)邏輯之外,還需要額外可續(xù)對(duì)接口訪問的控制處理。
其次,無法直接復(fù)用既有接口。當(dāng)我們需要對(duì)一個(gè)即有的集群內(nèi)訪問接口,實(shí)現(xiàn)外部服務(wù)訪問時(shí),我們不得不通過在原有接口上增加校驗(yàn)邏輯,或增加一個(gè)代理調(diào)用來實(shí)現(xiàn)權(quán)限控制,無法直接復(fù)用原有的接口。
面對(duì)類似上面的問題,我們要如何解決呢?下面進(jìn)入本文的正題:服務(wù)網(wǎng)關(guān)!
為了解決上面這些問題,我們需要將權(quán)限控制這樣的東西從我們的服務(wù)單元中抽離出去,而最適合這些邏輯的地方就是處于對(duì)外訪問最前端的地方,我們需要一個(gè)更強(qiáng)大一些的均衡負(fù)載器,它就是本文將來介紹的:服務(wù)網(wǎng)關(guān)。
服務(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è)試性。
下面我們通過實(shí)例例子來使用一下Zuul來作為服務(wù)的路有功能。
在使用Zuul之前,我們先構(gòu)建一個(gè)服務(wù)注冊(cè)中心、以及兩個(gè)簡單的服務(wù),比如:我構(gòu)建了一個(gè)service-A,一個(gè)service-B。然后啟動(dòng)eureka-server和這兩個(gè)服務(wù)。通過訪問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
引入依賴spring-cloud-starter-zuul、spring-cloud-starter-eureka,如果不是通過指定serviceId的方式,eureka依賴不需要,但是為了對(duì)服務(wù)集群細(xì)節(jié)的透明性,還是用serviceId來避免直接引用url的方式吧。
org.springframework.cloud
spring-cloud-starter-zuul
org.springframework.cloud
spring-cloud-starter-eureka
應(yīng)用主類使用@EnableZuulProxy注解開啟Zuul
@EnableZuulProxy
@SpringCloudApplication
publicclassApplication{
publicstaticvoidmain(String[] args){
newSpringApplicationBuilder(Application.class).web(true).run(args);
}
}
這里用了@SpringCloudApplication注解,之前沒有提過,通過源碼我們看到,它整合了@SpringBootApplication、@EnableDiscoveryClient、@EnableCircuitBreaker,主要目的還是簡化配置。這幾個(gè)注解的具體作用這里就不做詳細(xì)介紹了,之前的文章已經(jīng)都介紹過。
application.properties中配置Zuul應(yīng)用的基礎(chǔ)信息,如:應(yīng)用名、服務(wù)端口等。
spring.application.name=api-gateway
server.port=5555
完成上面的工作后,Zuul已經(jīng)可以運(yùn)行了,但是如何讓它為我們的微服務(wù)集群服務(wù),還需要我們另行配置,下面詳細(xì)的介紹一些常用配置內(nèi)容。
通過服務(wù)路由的功能,我們?cè)趯?duì)外提供服務(wù)的時(shí)候,只需要通過暴露Zuul中配置的調(diào)用地址就可以讓調(diào)用方統(tǒng)一的來訪問我們的服務(wù),而不需要了解具體提供服務(wù)的主機(jī)信息了。
在Zuul中提供了兩種映射方式:
通過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/**的訪問都映射到http://localhost:2222/上,也就是說當(dāng)我們?cè)L問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í)候也是如此。
通過url映射的方式對(duì)于Zuul來說,并不是特別友好,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的映射。例如,我們可以如下配置:
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來分別映射。另外為了讓Zuul能發(fā)現(xiàn)service-A和service-B,也加入了eureka的配置。
接下來,我們將eureka-server、service-A、service-B以及這里用Zuul實(shí)現(xiàn)的服務(wù)網(wǎng)關(guān)啟動(dòng)起來,在eureka-server的控制頁面中,我們可以看到分別注冊(cè)了service-A、service-B以及api-gateway
alt
嘗試通過服務(wù)網(wǎng)關(guān)來訪問service-A和service-B,根據(jù)配置的映射關(guān)系,分別訪問下面的url
http://localhost:5555/api-a/add?a=1&b=2:通過serviceId映射訪問service-A中的add服務(wù)
http://localhost:5555/api-b/add?a=1&b=2:通過serviceId映射訪問service-B中的add服務(wù)
http://localhost:5555/api-a-url/add?a=1&b=2:通過url映射訪問service-A中的add服務(wù)
推薦使用serviceId的映射方式,除了對(duì)Zuul維護(hù)上更加友好之外,serviceId映射方式還支持了斷路器,對(duì)于服務(wù)故障的情況下,可以有效的防止故障蔓延到服務(wù)網(wǎng)關(guān)上而影響整個(gè)系統(tǒng)的對(duì)外服務(wù)
在完成了服務(wù)路由之后,我們對(duì)外開放服務(wù)還需要一些安全措施來保護(hù)客戶端只能訪問它應(yīng)該訪問到的資源。所以我們需要利用Zuul的過濾器來實(shí)現(xiàn)我們對(duì)外服務(wù)的安全控制。
在服務(wù)網(wǎng)關(guān)中定義過濾器只需要繼承ZuulFilter抽象類實(shí)現(xiàn)其定義的四個(gè)抽象函數(shù)就可對(duì)請(qǐng)求進(jìn)行攔截與過濾。
比如下面的例子,定義了一個(gè)Zuul過濾器,實(shí)現(xiàn)了在請(qǐng)求被路由之前檢查請(qǐng)求中是否有accessToken參數(shù),若有就進(jìn)行路由,若沒有就拒絕訪問,返回401 Unauthorized錯(cuò)誤。
publicclassAccessFilterextendsZuulFilter{
privatestaticLogger log = LoggerFactory.getLogger(AccessFilter.class);
@Override
publicStringfilterType(){
return"pre";
? ? }
@Override
publicintfilterOrder(){
return0;
? ? }
@Override
publicbooleanshouldFilter(){
returntrue;
? ? }
@Override
publicObjectrun(){
? ? ? ? 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);
returnnull;
? ? ? ? }
log.info("access token ok");
returnnull;
? ? }
}
自定義過濾器的實(shí)現(xiàn),需要繼承ZuulFilter,需要重寫實(shí)現(xiàn)下面四個(gè)方法:
filterType:返回一個(gè)字符串代表過濾器的類型,在zuul中定義了四種不同生命周期的過濾器類型,具體如下:
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)用
filterOrder:通過int值來定義過濾器的執(zhí)行順序
shouldFilter:返回一個(gè)boolean類型來判斷該過濾器是否要執(zhí)行,所以通過此函數(shù)可實(shí)現(xiàn)過濾器的開關(guān)。在上例中,我們直接返回true,所以該過濾器總是生效。
run:過濾器的具體邏輯。需要注意,這里我們通過ctx.setSendZuulResponse(false)令zuul過濾該請(qǐng)求,不對(duì)其進(jìn)行路由,然后通過ctx.setResponseStatusCode(401)設(shè)置了其返回的錯(cuò)誤碼,當(dāng)然我們也可以進(jìn)一步優(yōu)化我們的返回,比如,通過ctx.setResponseBody(body)對(duì)返回body內(nèi)容進(jìn)行編輯等。
在實(shí)現(xiàn)了自定義過濾器之后,還需要實(shí)例化該過濾器才能生效,我們只需要在應(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)后,訪問:
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ì)于其他一些過濾類型,這里就不一一展開了,根據(jù)之前對(duì)filterType生命周期介紹,可以參考下圖去理解,并根據(jù)自己的需要在不同的生命周期中去實(shí)現(xiàn)不同類型的過濾器。
alt
最后,總結(jié)一下為什么服務(wù)網(wǎng)關(guān)是微服務(wù)架構(gòu)的重要部分,是我們必須要去做的原因:
不僅僅實(shí)現(xiàn)了路由功能來屏蔽諸多服務(wù)細(xì)節(jié),更實(shí)現(xiàn)了服務(wù)級(jí)別、均衡負(fù)載的路由。
實(shí)現(xiàn)了接口權(quán)限校驗(yàn)與微服務(wù)業(yè)務(wù)邏輯的解耦。通過服務(wù)網(wǎng)關(guān)中的過濾器,在各生命周期中去校驗(yàn)請(qǐng)求的內(nèi)容,將原本在對(duì)外服務(wù)層做的校驗(yàn)前移,保證了微服務(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ù)。