Spring Cloud系列五 之 服務(wù)網(wǎng)關(guān)

本篇文章內(nèi)容簡(jiǎn)單,但是沒有前面的基礎(chǔ)是很難理解的,所以推薦看Spring Cloud系列的其他四篇文章,代碼實(shí)現(xiàn)簡(jiǎn)單,主要是利用Netflix中的Zuul組件,但是總結(jié)起來沒有很長(zhǎng)的架構(gòu)師經(jīng)驗(yàn)是很難深刻理解的,故本文總結(jié)內(nèi)容翻譯自程序猿DD Spring Cloud系列博文,所有內(nèi)容本人都已經(jīng)測(cè)試沒有問題,再次非常感謝程序猿DD,的優(yōu)秀博文分享。本篇文章和前面配置信息Server一樣的風(fēng)格,從提出問題開始,來帶領(lǐng)大家一步一步解決問題,解決問題的過程成大家共學(xué)習(xí),共成長(zhǎng)希望對(duì)微服務(wù)架構(gòu)感興趣的童鞋,有所指引。

通過之前翻譯Spring Cloud組件的介紹,已經(jīng)能夠完成一個(gè)簡(jiǎn)單的微服務(wù)架構(gòu),大家腦子里都有一個(gè)大致的概念。這里通過一個(gè)圖 描述一下。


架構(gòu)圖

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ù)用原有的接口。
下面進(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è)試性。

問題匯總

  • 1.如何配置路由服務(wù)器

  • 2.Zuul服務(wù)路由組件兩種url映射關(guān)系配置

    • 2.1 url配置 zuul.routes.api-a-url.url
    • 2.2 serviceId配置 zuul.routes.api-b.serviceId
  • 3.如何對(duì)路由服務(wù)器進(jìn)行安全過濾措施

  • 4.項(xiàng)目演示

問題1:如何配置路由服務(wù)器

引入依賴spring-cloud-starter-zuul、spring-cloud-starter-eureka,如果不是通過指定serviceId的方式,eureka依賴不需要,但是為了對(duì)服務(wù)集群細(xì)節(jié)的透明性,還是用serviceId來避免直接引用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注解,之前沒有提過,通過源碼我們看到,它整合了@SpringBootApplication、@EnableDiscoveryClient、@EnableCircuitBreaker,主要目的還是簡(jiǎn)化配置。這幾個(gè)注解的具體作用這里就不做詳細(xì)介紹了,之前的文章已經(jīng)都介紹過。

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

==配置完,服務(wù)網(wǎng)關(guān)就可以啟動(dòng)了,重點(diǎn)來了,問題二==

問題2:Zuul服務(wù)路由組件兩種url映射關(guān)系配置

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

  • 服務(wù)路由

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

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

  • 2.1通過url直接映射,我們可以如下配置:
# routes to url 當(dāng)url地址是/api-a-url/**,會(huì)自動(dòng)路由到端口2222的服務(wù)上
zuul.routes.api-a-url.path=/api-a-url/**
zuul.routes.api-a-url.url=http://localhost:2222/
# routes to url 當(dāng)url地址是/api-a-url/**,會(huì)自動(dòng)路由到端口2222的服務(wù)上
zuul.routes.api-b-url.path=/api-b-url/**
zuul.routes.api-b-url.url=http://localhost:2223/

# 其中zuul.routes.{自定義}.url|path

  • 2.2通過ServiceID映射,我們可以如下配置:

通過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的映射。例如,我們可以如下配置:

#service-A/service-B是注冊(cè)到服務(wù)器上的Service服務(wù)名
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/

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

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

問題3:如何對(duì)路由服務(wù)器進(jìn)行安全過濾措施

  • 服務(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ò)誤。

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;
    }
}

自定義過濾器的實(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)后,訪問:

下面是過濾器的聲明周期圖


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

相關(guān)閱讀更多精彩內(nèi)容

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