1. 程式人生 > >Spring Boot + Spring Cloud 實現許可權管理系統 後端篇(二十一):服務閘道器(Zuul)

Spring Boot + Spring Cloud 實現許可權管理系統 後端篇(二十一):服務閘道器(Zuul)

線上演示

使用者名稱:admin 密碼:admin

技術背景

前面我們通過Ribbon或Feign實現了微服務之間的呼叫和負載均衡,那我們的各種微服務又要如何提供給外部應用呼叫呢。

當然,因為是REST API介面,外部客戶端直接呼叫各個微服務是沒有問題的,但出於種種原因,這並不是一個好的選擇。

讓客戶端直接與各個微服務通訊,會有以下幾個問題:

  • 客戶端會多次請求不同的微服務,增加了客戶端的複雜性。
  • 存在跨域請求,在一定場景下處理會變得相對比較複雜。
  • 實現認證複雜,每個微服務都需要獨立認證。
  • 難以重構,專案迭代可能導致微服務重新劃分。如果客戶端直接與微服務通訊,那麼重構將會很難實施。
  • 如果某些微服務使用了防火牆/瀏覽器不友好的協議,直接訪問會有一定困難。

面對類似上面的問題,我們要如何解決呢?答案就是:服務閘道器!

使用服務閘道器具有以下幾個優點:

  • 易於監控。可在微服務閘道器收集監控資料並將其推送到外部系統進行分析。
  • 易於認證。可在服務閘道器上進行認證,然後再轉發請求到微服務,無須在每個微服務中進行認證。
  • 客戶端只跟服務閘道器打交道,減少了客戶端與各個微服務之間的互動次數。
  • 多渠道支援,可以根據不同客戶端(WEB端、移動端、桌面端...)提供不同的API服務閘道器。

Spring Cloud Zuul

服務閘道器是微服務架構中一個不可或缺的部分。通過服務閘道器統一向外系統提供REST API的過程中,除了具備服務路由、均衡負載功能之外,它還具備了許可權控制等功能。

Spring Cloud Netflix中的Zuul就擔任了這樣的一個角色,為微服務架構提供了前門保護的作用,同時將許可權控制這些較重的非業務邏輯內容遷移到服務路由層面,使得服務叢集主體能夠具備更高的可複用性和可測試性。

在Spring Cloud體系中, Spring Cloud Zuul 封裝了Zuul元件,作為一個API閘道器,負責提供負載均衡、反向代理和許可權認證。

Zuul工作機制

過濾器機制

Zuul的核心是一系列的filters, 其作用類似Servlet框架的Filter,Zuul把客戶端請求路由到業務處理邏輯的過程中,這些filter在路由的特定時期參與了一些過濾處理,比如實現鑑權、流量轉發、請求統計等功能。Zuul的整個執行機制,可以用下圖來描述。

過濾器的生命週期

Filter的生命週期有4個,分別是“PRE”、“ROUTING”、“POST”、“ERROR”,整個生命週期可以用下圖來表示。

基於Zuul的這些過濾器,可以實現各種豐富的功能,而這些過濾器型別則對應於請求的典型生命週期。

PRE: 這種過濾器在請求被路由之前呼叫。我們可利用這種過濾器實現身份驗證、在叢集中選擇請求的微服務、記錄除錯資訊等。

ROUTING:這種過濾器將請求路由到微服務。這種過濾器用於構建傳送給微服務的請求,並使用Apache HttpClient或Netfilx Ribbon請求微服務。

POST:這種過濾器在路由到微服務以後執行。這種過濾器可用來為響應新增標準的HTTP Header、收集統計資訊和指標、將響應從微服務傳送給客戶端等。

ERROR:在其他階段發生錯誤時執行該過濾器。

除了預設的過濾器型別,Zuul還允許我們建立自定義的過濾器型別。例如,我們可以定製一種STATIC型別的過濾器,直接在Zuul中生成響應,而不將請求轉發到後端的微服務。

Zuul中預設實現的Filter

Zuul預設實現了很多Filter,這些Filter如下面表格所示。

型別順序過濾器功能
pre -3 ServletDetectionFilter 標記處理Servlet的型別
pre -2 Servlet30WrapperFilter 包裝HttpServletRequest請求
pre -1 FormBodyWrapperFilter 包裝請求體
route 1 DebugFilter 標記除錯標誌
route 5 PreDecorationFilter 處理請求上下文供後續使用
route 10 RibbonRoutingFilter serviceId請求轉發
route 100 SimpleHostRoutingFilter url請求轉發
route 500 SendForwardFilter forward請求轉發
post 0 SendErrorFilter 處理有錯誤的請求響應
post 1000 SendResponseFilter 處理正常的請求響應

禁用指定的Filter

可以在application.yml中配置需要禁用的filter,格式為zuul.<SimpleClassName>.<filterType>.disable=true

比如要禁用org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter就設定如下。

zuul:
  SendResponseFilter:
    post:
      disable: true

自定義Filter

實現自定義濾器需要繼承ZuulFilter,並實現ZuulFilter中的抽象方法。

public class MyFilter extends ZuulFilter {
    @Override
    String filterType() {
        return "pre"; // 定義filter的型別,有pre、route、post、error四種
    }

    @Override
    int filterOrder() {
        return 5; // 定義filter的順序,數字越小表示順序越高,越先執行
    }

    @Override
    boolean shouldFilter() {
        return true; // 表示是否需要執行該filter,true表示執行,false表示不執行
    }

    @Override
    Object run() {
        return null; // filter需要執行的具體操作
    }
}

實現案例

新建工程

新建一個專案 kitty-zuul 作為服務閘道器,工程結構如下圖。

新增依賴

新增 consul、zuul 相關依賴。

pom.xml

<?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>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.4.RELEASE</version>
        <relativePath /> <!-- lookup parent from repository -->
    </parent>

    <groupId>com.louis</groupId>
    <artifactId>kitty-zuul</artifactId>
    <version>${project.version}</version>
    <packaging>jar</packaging>

    <name>kitty-zuul</name>
    <description>kitty-zuul</description>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <project.version>1.0.0</project.version>
        <java.version>1.8</java.version>
        <spring-cloud.version>Finchley.RELEASE</spring-cloud.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-consul-discovery</artifactId>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <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>

啟動類

啟動類新增 @EnableZuulProxy 註解,開啟服務閘道器支援。

package com.louis.kitty.zuul;

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

@EnableZuulProxy
@SpringBootApplication
public class KittyZuulApplication {

    public static void main(String[] args) {
        SpringApplication.run(KittyZuulApplication.class, args);
    }
}

配置檔案

配置啟動埠為 8010, 註冊服務到註冊中心,配置zuul轉發規則。

這裡配置在訪問 locathost:8010/feigin/call 和 ribbon/call時,呼叫消費者對應介面。

server:
  port: 8010
spring:
  application:
    name: kitty-zuul
  cloud:
    consul:
      host: localhost
      port: 8500
      discovery:
        serviceName: ${spring.application.name}    # 註冊到consul的服務名稱
zuul:
  routes:
    ribbon:
      path: /ribbon/**
      serviceId: kitty-consumer  # 轉發到消費者 /ribbon/
    feign:
      path: /feign/**
      serviceId: kitty-consumer  # 轉發到消費者 /feign/

測試效果

依次啟動註冊中心、監控、服務提供者、服務消費者、服務閘道器等專案。

說明Zuul已經成功轉發請求,併成功呼叫後端微服務。

配置介面字首

如果想給每個服務的API介面加上一個字首,可使用zuul.prefix進行配置。

例如http://localhost:8010/v1/feign/call,即在所有的API介面上加一個v1作為版本號。

zuul:
  prefix: /v1
  routes:
    ribbon:
      path: /ribbon/**
      serviceId: kitty-consumer  # 轉發到消費者 /ribbon/
    feign:
      path: /feign/**
      serviceId: kitty-consumer  # 轉發到消費者 /feign/

預設路由規則

上面我們是通過新增路由配置進行請求轉發的,內容如下。

zuul:
  routes:
    ribbon:
      path: /ribbon/**
      serviceId: kitty-consumer  # 轉發到消費者 /ribbon/
    feign:
      path: /feign/**
      serviceId: kitty-consumer  # 轉發到消費者 /feign/

但是如果後端微服務服務非常多的時候,每一個都這樣配置還是挺麻煩的,所以Spring Cloud Zuul已經幫我們做了預設配置。預設情況下,Zuul會代理所有註冊到註冊中心的微服務,並且Zuul的預設路由規則如下:http://ZUUL_HOST:ZUUL_PORT/微服務在註冊中心的serviceId/**會被轉發到serviceId對應的微服務,所以說如果遵循預設路由規則,基本上就沒什麼配置了。

我們移除上面的配置,直接通過 serviceId/feign/call 的方式訪問。

結果也是可用訪問的,說明ZUUL預設路由規則正在產生作用。

路由熔斷

Zuul作為Netflix元件,可以與Ribbon、Eureka和Hystrix等元件相結合,實現負載均衡、熔斷器的功能。預設情況下Zuul和Ribbon相結合,實現了負載均衡。實現熔斷器功能需要實現FallbackProvider介面,實現該介面的兩個方法,一個是getRoute(),用於指定熔斷器功能應用於哪些路由的服務;另一個方法fallbackResponse()為進入熔斷器功能時執行的邏輯。

建立 MyFallbackProvider 類,getRoute()方法返回"kitty-consumer",只針對consumer服務進行熔斷。如果需要所有的路由服務都加熔斷功能,需要在getRoute()方法上返回”*“的匹配符。getBody()方法返回傳送熔斷時的反饋資訊,這裡在傳送熔斷時返回資訊:"Sorry, the service is unavailable now." 。

MyFallbackProvider.java

package com.louis.kitty.zuul;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;

import org.springframework.cloud.netflix.zuul.filters.route.FallbackProvider;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.stereotype.Component;

@Component
public class MyFallbackProvider implements FallbackProvider {
    @Override
    public String getRoute() {
        return "kitty-consumer";
    }

    @Override
    public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
        System.out.println("route:"+route);
        System.out.println("exception:"+cause.getMessage());
        return new ClientHttpResponse() {
            @Override
            public HttpStatus getStatusCode() throws IOException {
                return HttpStatus.OK;
            }

            @Override
            public int getRawStatusCode() throws IOException {
                return 200;
            }

            @Override
            public String getStatusText() throws IOException {
                return "ok";
            }

            @Override
            public void close() {

            }

            @Override
            public InputStream getBody() throws IOException {
                return new ByteArrayInputStream("Sorry, the service is unavailable now.".getBytes());
            }

            @Override
            public HttpHeaders getHeaders() {
                HttpHeaders headers = new HttpHeaders();
                headers.setContentType(MediaType.APPLICATION_JSON);
                return headers;
            }
        };
    }
}

說明我們自定義的熔斷器已經起作用了。

自定義Filter

建立一個MyFilter, 繼承ZuulFilter類,覆寫run()方法邏輯,在轉發請求前進行token認證,如果請求沒有攜帶token,返回"there is no request token"提示。

package com.louis.kitty.zuul;

import java.io.IOException;

import javax.servlet.http.HttpServletRequest;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

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

@Component
public class MyFilter extends ZuulFilter {

    private static Logger log=LoggerFactory.getLogger(MyFilter.class);

    @Override
    public String filterType() {
        return "pre"; // 定義filter的型別,有pre、route、post、error四種
    }

    @Override
    public int filterOrder() {
        return 0; // 定義filter的順序,數字越小表示順序越高,越先執行
    }

    @Override
    public boolean shouldFilter() {
        return true; // 表示是否需要執行該filter,true表示執行,false表示不執行
    }

    @Override
    public Object run() throws ZuulException {
        // filter需要執行的具體操作
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();
        String token = request.getParameter("token");
        System.out.println(token);
        if(token==null){
            log.warn("there is no request token");
            ctx.setSendZuulResponse(false);
            ctx.setResponseStatusCode(401);
            try {
                ctx.getResponse().getWriter().write("there is no request token");
            } catch (IOException e) {
                e.printStackTrace();
            }
            return null;
        }
        log.info("ok");
        return null;
    }
}

OK,這樣就行了,Zuul會自動載入Filter執行過濾的。

高可用性

Zuul作為API服務閘道器,不同的客戶端使用不同的負載將請求統一分發到後端的Zuul,再有Zuul轉發到後端服務。因此,為了保證Zuul的高可用性,前端可以同時開啟多個Zuul例項進行負載均衡,另外,在Zuul的前端還可以使用Nginx或者F5再次進行負載轉發,從而保證Zuul的高可用性。

原始碼下載