服務網關zuul之一:入門介紹

經過以前幾篇Spring Cloud中幾個核心組件的介紹,咱們已經能夠構建一個簡略的(不夠完善)微服務架構了。好比下圖所示:html

 

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

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

先來講說這樣架構須要作的一些事兒以及存在的不足:web

  • 首先,破壞了服務無狀態特色。爲了保證對外服務的安全性,咱們須要實現對服務訪問的權限控制,而開放服務的權限控制機制將會貫穿並污染整個開放服務的業務邏輯,這會帶來的最直接問題是,破壞了服務集羣中REST API無狀態的特色。從具體開發和測試的角度來講,在工做中除了要考慮實際的業務邏輯以外,還須要額外可續對接口訪問的控制處理。
  • 其次,沒法直接複用既有接口。當咱們須要對一個即有的集羣內訪問接口,實現外部服務訪問時,咱們不得不經過在原有接口上增長校驗邏輯,或增長一個代理調用來實現權限控制,沒法直接複用原有的接口。

讓客戶端直接與各個微服務通信,會有如下的問題:spring

  • 客戶端會屢次請求不一樣的微服務,增長了客戶端的複雜性。
  • 存在跨域請求,在必定場景下處理相對複雜。
  • 認證複雜,每一個服務都須要獨立認證。
  • 難以重構,隨着項目的迭代,可能須要從新劃分微服務。例如,可能將多個服務合併成一個或者將一個服務拆分紅多個。若是客戶端直接與微服務通信,那麼重構將會很難實施。
  • 某些微服務可能使用了防火牆/瀏覽器不友好的協議,直接訪問會有必定困難。

面對相似上面的問題,咱們要如何解決呢?下面進入本文的正題:服務網關!apache

使用網關優勢:後端

  • 易於監控。可在微服務網關收集監控數據並將其推送到外部系統進行分析。
  • 易於認證。可在微服務網關上進行認證。而後再將請求轉發到後端的微服務,而無須在每一個微服務中進行認證。
  • 減小了客戶端與各個微服務之間的交互次數。

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

服務網關是微服務架構中一個不可或缺的部分。經過服務網關統一貫外系統提供REST API的過程當中,除了具有服務路由、均衡負載功能以外,它還具有了權限控制等功能。Spring Cloud Netflix中的Zuul就擔任了這樣的一個角色,爲微服務架構提供了前門保護的做用,同時將權限控制這些較重的非業務邏輯內容遷移到服務路由層面,使得服務集羣主體可以具有更高的可複用性和可測試性。跨域

下面咱們經過實例例子來使用一下Zuul來做爲服務的路有功能。瀏覽器

準備工做

在使用Zuul以前,咱們先構建一個服務註冊中心、以及兩個簡單的服務,好比:我構建了一個compute-service,一個compute-service-B。而後啓動eureka-server和這兩個服務。經過訪問eureka-server,咱們能夠看到compute-service和compute-service-B已經註冊到了服務中心。

compute-service:見《服務註冊發現Eureka之一:Spring Cloud Eureka的服務註冊與發現

compute-service-B:將compute-service拷貝一份,修改下項目名和服務名

 

若是您還不熟悉如何構建服務中心和註冊服務,請先閱讀見《服務註冊發現Eureka之一:Spring Cloud Eureka的服務註冊與發現》。

 

開始使用Zuul

  • 引入依賴spring-cloud-starter-zuul、spring-cloud-starter-eureka,若是不是經過指定serviceId的方式,eureka依賴不須要,可是爲了對服務集羣細節的透明性,仍是用serviceId來避免直接引用url的方式吧。

傳統路由方式::經過url直接映射,咱們能夠以下配置:

若是是多實例的話,用逗號分隔

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.dxz.zuul</groupId>
    <artifactId>api-gateway</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>api-gateway</name>
    <description>zuul project for Spring Boot</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.3.5.RELEASE</version>   <!--配合spring cloud版本 -->
        <relativePath /> <!-- lookup parent from repository -->
    </parent>
    <properties>
        <!--設置字符編碼及java版本 -->
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <!--增長zuul的依賴 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-zuul</artifactId>
        </dependency>
        </dependency>
        <!--用於測試的,本例可省略 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <!--依賴管理,用於管理spring-cloud的依賴 -->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-parent</artifactId>
                <version>Brixton.SR3</version>   <!--官網爲Angel.SR4版本,可是我使用的時候老是報錯 -->
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <!--使用該插件打包 -->
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

啓動類:

package com.dxz.zuul;

import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;

@EnableZuulProxy
@SpringBootApplication
public class ApiGatewayApplication {

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

配置:

spring.application.name=api-gateway
server.port=5555
zuul.routes.api-test.path=/api-test/**
zuul.routes.api-test.url=http://localhost:2223/,http://localhost:2221/
 

測試:

啓動相關服務:

瀏覽器訪問,結果以下:

 

面向服務的路由方式::經過url映射的方式對於Zuul來講,並非特別友好,Zuul須要知道咱們全部爲服務的地址,才能完成全部的映射配置。而實際上,咱們在實現微服務架構時,服務名與服務實例地址的關係在eureka server中已經存在了,因此只須要將Zuul註冊到eureka server上去發現其餘服務,咱們就能夠實現對serviceId的映射:

pom中增長:

        <!--增長eureka-server的依賴 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-eureka-server</artifactId>
        </dependency>

路由配置以下:

spring.application.name=api-gateway
server.port=5555
zuul.routes.api-test.path=/api-test/**
zuul.routes.api-test.url=http://localhost:2223/
zuul.routes.api-a.path=/api-a/** zuul.routes.api-a.serviceId=COMPUTE-SERVICE zuul.routes.api-b.path=/api-b/** zuul.routes.api-b.serviceId=COMPUTE-SERVICE-B eureka.client.serviceUrl.defaultZone=http://localhost:1111/eureka/

結果:


Zuul配置

完成上面的工做後,Zuul已經能夠運行了,可是如何讓它爲咱們的微服務集羣服務,還須要咱們另行配置,下面詳細的介紹一些經常使用配置內容。

服務路由

經過服務路由的功能,咱們在對外提供服務的時候,只須要經過暴露Zuul中配置的調用地址就可讓調用方統一的來訪問咱們的服務,而不須要了解具體提供服務的主機信息了。

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

  • 傳統路由方式:經過url直接映射,咱們能夠以下配置:
spring.application.name=api-gateway
server.port=5555
zuul.routes.api-test.path=/api-test/**
zuul.routes.api-test.url=http://localhost:2223/

其中,配置屬性zuul.routes.api-a-url.path中的api-a-url部分爲路由的名字,能夠任意定義,可是一組映射關係的path和url要相同,下面講serviceId時候也是如此。該配置,定義了,全部到Zuul的中規則爲:/api-a-url/**的訪問都映射到http://localhost:2222/上,也就是說當咱們訪問http://localhost:5555/api-a-url/add?a=1&b=2的時候,Zuul會將該請求路由到:http://localhost:2222/add?a=1&b=2上。

  • 面向服務路由方式:經過url映射的方式對於Zuul來講,並非特別友好,Zuul須要知道咱們全部爲服務的地址,才能完成全部的映射配置。而實際上,咱們在實現微服務架構時,服務名與服務實例地址的關係在eureka server中已經存在了,因此只須要將Zuul註冊到eureka server上去發現其餘服務,咱們就能夠實現對serviceId的映射。例如,咱們能夠以下配置:
    spring.application.name=api-gateway
    server.port=5555
    zuul.routes.api-a.path=/api-a/**
    zuul.routes.api-a.serviceId=COMPUTE-SERVICE
    zuul.routes.api-b.path=/api-b/**
    zuul.routes.api-b.serviceId=COMPUTE-SERVICE-B
    eureka.client.serviceUrl.defaultZone=http://localhost:1111/eureka/

推薦使用serviceId的映射方式,除了對Zuul維護上更加友好以外,serviceId映射方式還支持了斷路器,對於服務故障的狀況下,能夠有效的防止故障蔓延到服務網關上而影響整個系統的對外服務

服務過濾

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

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

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

複製代碼
package com.dxz;

import javax.servlet.http.HttpServletRequest;

import org.apache.log4j.Logger;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;

public class AccessFilter extends ZuulFilter {

    private static Logger log = Logger.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;
    }

}
複製代碼
  • filterType:返回一個字符串表明過濾器的類型,在zuul中定義了四種不一樣生命週期的過濾器類型,具體以下:自定義過濾器的實現,須要繼承ZuulFilter,須要重寫實現下面四個方法:
    • pre:能夠在請求被路由以前調用
    • routing:在路由請求時候被調用
    • post:在routing和error過濾器以後被調用
    • error:處理請求時發生錯誤時被調用
  • filterOrder:經過int值來定義過濾器的執行順序
  • shouldFilter:返回一個boolean類型來判斷該過濾器是否要執行,因此經過此函數可實現過濾器的開關。在上例中,咱們直接返回true,因此該過濾器老是生效。
  • run:過濾器的具體邏輯。須要注意,這裏咱們經過ctx.setSendZuulResponse(false)令zuul過濾該請求,不對其進行路由,而後經過ctx.setResponseStatusCode(401)設置了其返回的錯誤碼,固然咱們也能夠進一步優化咱們的返回,好比,經過ctx.setResponseBody(body)對返回body內容進行編輯等。

在實現了自定義過濾器以後,還須要實例化該過濾器才能生效,咱們只須要在應用主類中增長以下內容:

複製代碼
package com.dxz;

import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.client.SpringCloudApplication;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.context.annotation.Bean;

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

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

}
複製代碼

啓動該服務網關後,訪問:

  • http://localhost:5555/api-a/add?a=1&b=2:返回401錯誤
  • http://localhost:5555/api-a/add?a=1&b=2&accessToken=token:正確路由到server-A,並返回計算內容

對於其餘一些過濾類型,這裏就不一一展開了,根據以前對filterType生命週期介紹,能夠參考下圖去理解,並根據本身的須要在不一樣的生命週期中去實現不一樣類型的過濾器。

 

最後,總結一下爲何服務網關是微服務架構的重要部分,是咱們必需要去作的緣由:

  • 不只僅實現了路由功能來屏蔽諸多服務細節,更實現了服務級別、均衡負載的路由。
  • 實現了接口權限校驗與微服務業務邏輯的解耦。經過服務網關中的過濾器,在各生命週期中去校驗請求的內容,將本來在對外服務層作的校驗前移,保證了微服務的無狀態性,同時下降了微服務的測試難度,讓服務自己更集中關注業務邏輯的處理。
  • 實現了斷路器,不會由於具體微服務的故障而致使服務網關的阻塞,依然能夠對外服務。
相關文章
相關標籤/搜索