1. 程式人生 > >Spring Cloud系列五 之 服務閘道器

Spring Cloud系列五 之 服務閘道器

本篇文章內容簡單,但是沒有前面的基礎是很難理解的,所以推薦看Spring Cloud系列的其他四篇文章,程式碼實現簡單,主要是利用Netflix中的Zuul元件,但是總結起來沒有很長的架構師經驗是很難深刻理解的,故本文總結內容翻譯自程式猿DD Spring Cloud系列博文,所有內容本人都已經測試沒有問題,再次非常感謝程式猿DD,的優秀博文分享。本篇文章和前面配置資訊Server一樣的風格,從提出問題開始,來帶領大家一步一步解決問題,解決問題的過程成大家共學習,共成長希望對微服務架構感興趣的童鞋,有所指引。

通過之前翻譯Spring Cloud元件的介紹,已經能夠完成一個簡單的微服務架構,大家腦子裡都有一個大致的概念。這裡通過一個圖 描述一下。
架構圖

Spring Cloud Netflix中的Eureka實現了服務註冊中心以及服務註冊與發現;而服務間通過Ribbon或Feign實現服務的消費以及均衡負載;通過Spring Cloud Config實現了應用多環境的外部化配置以及版本管理。為了使得服務叢集更為健壯,使用Hystrix的融斷機制來避免在微服務架構中個別服務出現異常時引起的故障蔓延。

在該架構中,我們的服務叢集包含:內部服務Service A和Service B,他們都會註冊與訂閱服務至Eureka Server,而Open Service是一個對外的服務,通過均衡負載公開至服務呼叫方。本文我們把焦點聚集在對外服務這塊,這樣的實現是否合理,或者是否有更好的實現方式呢?

先來說說這樣架構需要做的一些事兒以及存在的不足:
  • 首先,破壞了服務無狀態特點。為了保證對外服務的安全性,我們需要實現對服務訪問的許可權控制,而開放服務的許可權控制機制將會貫穿並汙染整個開放服務的業務邏輯,這會帶來的最直接問題是,破壞了服務叢集中REST API無狀態的特點。從具體開發和測試的角度來說,在工作中除了要考慮實際的業務邏輯之外,還需要額外可續對介面訪問的控制處理。
  • 其次,無法直接複用既有介面。當我們需要對一個即有的叢集內訪問介面,實現外部服務訪問時,我們不得不通過在原有介面上增加校驗邏輯,或增加一個代理呼叫來實現許可權控制,無法直接複用原有的介面。
下面進入本文的正題:服務閘道器!

為了解決上面這些問題,我們需要將許可權控制這樣的東西從我們的服務單元中抽離出去,而最適合這些邏輯的地方就是處於對外訪問最前端的地方,我們需要一個更強大一些的均衡負載器,它就是本文將來介紹的:服務閘道器。

服務閘道器是微服務架構中一個不可或缺的部分。通過服務閘道器統一向外系統提供REST API的過程中,除了具備服務路由、均衡負載功能之外,它還具備了許可權控制等功能。Spring Cloud Netflix中的Zuul就擔任了這樣的一個角色,為微服務架構提供了前門保護的作用,同時將許可權控制這些較重的非業務邏輯內容遷移到服務路由層面,使得服務叢集主體能夠具備更高的可複用性和可測試性。

問題彙總

  • 1.如何配置路由伺服器
  • 2.Zuul服務路由元件兩種url對映關係配置

    • 2.1 url配置 zuul.routes.api-a-url.url
    • 2.2 serviceId配置 zuul.routes.api-b.serviceId
  • 3.如何對路由伺服器進行安全過濾措施
  • 4.專案演示

問題1:如何配置路由伺服器

引入依賴spring-cloud-starter-zuul、spring-cloud-starter-eureka,如果不是通過指定serviceId的方式,eureka依賴不需要,但是為了對服務叢集細節的透明性,還是用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>
  • 應用主類使用@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,主要目的還是簡化配置。這幾個註解的具體作用這裡就不做詳細介紹了,之前的文章已經都介紹過。

  • application.properties中配置Zuul應用的基礎資訊,如:應用名、服務埠等。
spring.application.name=api-gateway
server.port=5555

==配置完,服務閘道器就可以啟動了,重點來了,問題二==

問題2:Zuul服務路由元件兩種url對映關係配置

完成上面的工作後,Zuul已經可以運行了,但是如何讓它為我們的微服務叢集服務,還需要我們另行配置,下面詳細的介紹一些常用配置內容。

  • 服務路由

通過服務路由的功能,我們在對外提供服務的時候,只需要通過暴露Zuul中配置的呼叫地址就可以讓呼叫方統一的來訪問我們的服務,而不需要了解具體提供服務的主機資訊了。

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

  • 2.1通過url直接對映,我們可以如下配置:
# routes to url 當url地址是/api-a-url/**,會自動路由到埠2222的服務上
zuul.routes.api-a-url.path=/api-a-url/**
zuul.routes.api-a-url.url=http://localhost:2222/
# routes to url 當url地址是/api-a-url/**,會自動路由到埠2222的服務上
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對映的方式對於Zuul來說,並不是特別友好,Zuul需要知道我們所有為服務的地址,才能完成所有的對映配置。而實際上,我們在實現微服務架構時,服務名與服務例項地址的關係在eureka server中已經存在了,所以只需要將Zuul註冊到eureka server上去發現其他服務,我們就可以實現對serviceId的對映。例如,我們可以如下配置:

#service-A/service-B是註冊到伺服器上的Service服務名
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/

嘗試通過服務閘道器來訪問service-A和service-B,根據配置的對映關係,分別訪問下面的url

推薦使用serviceId的對映方式,除了對Zuul維護上更加友好之外,serviceId對映方式還支援了斷路器,對於服務故障的情況下,可以有效的防止故障蔓延到服務閘道器上而影響整個系統的對外服務

問題3:如何對路由伺服器進行安全過濾措施

  • 服務過濾

在完成了服務路由之後,我們對外開放服務還需要一些安全措施來保護客戶端只能訪問它應該訪問到的資源。所以我們需要利用Zuul的過濾器來實現我們對外服務的安全控制。

在服務閘道器中定義過濾器只需要繼承ZuulFilter抽象類實現其定義的四個抽象函式就可對請求進行攔截與過濾。

比如下面的例子,定義了一個Zuul過濾器,實現了在請求被路由之前檢查請求中是否有accessToken引數,若有就進行路由,若沒有就拒絕訪問,返回401 Unauthorized錯誤。

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

自定義過濾器的實現,需要繼承ZuulFilter,需要重寫實現下面四個方法:

  • filterType:返回一個字串代表過濾器的型別,在zuul中定義了四種不同生命週期的過濾器型別,具體如下:
    • pre:可以在請求被路由之前呼叫
    • routing:在路由請求時候被呼叫
    • post:在routing和error過濾器之後被呼叫
    • error:處理請求時發生錯誤時被呼叫
  • filterOrder:通過int值來定義過濾器的執行順序
  • shouldFilter:返回一個boolean型別來判斷該過濾器是否要執行,所以通過此函式可實現過濾器的開關。在上例中,我們直接返回true,所以該過濾器總是生效。
  • run:過濾器的具體邏輯。需要注意,這裡我們通過ctx.setSendZuulResponse(false)令zuul過濾該請求,不對其進行路由,然後通過ctx.setResponseStatusCode(401)設定了其返回的錯誤碼,當然我們也可以進一步優化我們的返回,比如,通過ctx.setResponseBody(body)對返回body內容進行編輯等。

在實現了自定義過濾器之後,還需要例項化該過濾器才能生效,我們只需要在應用主類中增加如下內容:

@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();
    }
}

啟動該服務閘道器後,訪問:

下面是過濾器的宣告週期圖

最後,總結一下為什麼服務閘道器是微服務架構的重要部分,是我們必須要去做的原因:

  • 不僅僅實現了路由功能來遮蔽諸多服務細節,更實現了服務級別、均衡負載的路由。
  • 實現了介面許可權校驗與微服務業務邏輯的解耦。通過服務閘道器中的過濾器,在各生命週期中去校驗請求的內容,將原本在對外服務層做的校驗前移,保證了微服務的無狀態性,同時降低了微服務的測試難度,讓服務本身更集中關注業務邏輯的處理。
  • 實現了斷路器,不會因為具體微服務的故障而導致服務閘道器的阻塞,依然可以對外服務。