1. 程式人生 > >springboot2.0下的zuul路由閘道器初探

springboot2.0下的zuul路由閘道器初探

Zuul作為微服務系統的閘道器元件,用於構建邊界服務,致力於動態路由、過濾、監控、彈性伸縮和安全。

為什麼需要Zuul

Zuul、Ribbon以及Eureka結合可以實現智慧路由和負載均衡的功能;閘道器將所有服務的API介面統一聚合,統一對外暴露。外界呼叫API介面時,不需要知道微服務系統中各服務相互呼叫的複雜性,保護了內部微服務單元的API介面;閘道器可以做使用者身份認證和許可權認證,防止非法請求操作API介面;閘道器可以實現監控功能,實時日誌輸出,對請求進行記錄;閘道器可以實現流量監控,在高流量的情況下,對服務降級;API介面從內部服務分離出來,方便做測試。

Zuul通過Servlet來實現,通過自定義的ZuulServlet來對請求進行控制。核心是一系列過濾器,可以在Http請求的發起和響應返回期間執行一系列過濾器。Zuul採取了動態讀取、編譯和執行這些過濾器。過濾器之間不能直接通訊,而是通過RequestContext物件來共享資料,每個請求都會建立一個RequestContext物件。

Zuul生命週期如下圖。 當一個客戶端Request請求進入Zuul閘道器服務時,閘道器先進入”pre filter“,進行一系列的驗證、操作或者判斷。然後交給”routing filter“進行路由轉發,轉發到具體的服務例項進行邏輯處理、返回資料。當具體的服務處理完成後,最後由”post filter“進行處理,該型別的處理器處理完成之後,將Request資訊返回客戶端。 

Zuul是Netflix出品的一個基於JVM路由和服務端的負載均衡器.

  Zuul功能:

  • 認證
  • 壓力測試
  • 金絲雀測試
  • 動態路由
  • 負載削減
  • 安全
  • 靜態響應處理
  • 主動/主動交換管理

Zuul的規則引擎允許通過任何JVM語言來編寫規則和過濾器, 支援基於Java和Groovy的構建。

現在我們簡單的先搭建一個閘道器伺服器

首先單獨建一個專案,用來做閘道器伺服器

引入相關的主要jar包:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

引入spring-cloud-starter-netflix-eureka-client的目的是本身通過url對映的方式來實現zuul的轉發有侷限性,比如每增加一個服務就需要配置一條內容,另外後端的服務如果是動態來提供,就不能採用這種方案來配置了。實際上在實現微服務架構時,服務名與服務例項地址的關係在eureka server中已經存在了,所以只需要將Zuul註冊到eureka server上去發現其他服務,就可以實現對serviceId的對映。

加入yml的配置檔案,主要用來連線eureka,及配置閘道器路由規則

server:
  port: 8088
spring.application.name: startGateway
eureka:
  client:
    service-url: 
      defaultZone: http://root:[email protected]:10000/eureka
zuul.routes.three.path: /three/**
zuul.routes.three.service-id: three
zuul.routes.three.stripPrefix: false
zuul.routes.five.path: /five/**
zuul.routes.five.service-id: five
zuul.routes.five.stripPrefix: false

這邊有個坑:

    設定 zuul.prefix 可以為所有的匹配增加字首, 例如 /api,代理字首預設會從請求路徑中移除(通過 zuul.stripPrefix=false 可以關閉這個功能).

  1. 反響代理配置  
  2. 這裡的配置類似nginx的反響代理  
  3. 當請求/api/**會直接交給listOfServers配置的伺服器處理  
  4. 當stripPrefix=true的時候 (http://127.0.0.1:3333/api/user/list -> http://192.168.1.100:8080/user/list)  
  5. 當stripPrefix=false的時候(http://127.0.0.1:5555/api/user/list -> http://192.168.1.100:8080/api/user/list)  
  6. zuul.routes.api.path=/api/**  
  7. zuul.routes.api.stripPrefix=false  
  8. api.ribbon.listOfServers=192.168.1.100:8080,192.168.1.101:8080,192.168.1.102:8080

你也可以在指定服務中關閉這個功能:

zuul.routes.five.path: /five/**
zuul.routes.five.service-id: five
zuul.routes.five.stripPrefix: false
在這個例子中, 請求"/five/a"將被跳轉到"five"服務的"/five/a"上.如果不配置stripPrefix: false的話就會預設路由到/a上,忽略/five這個字首,導致404找不到資源

之後在啟動類上配置

package cn.chinotan;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.SpringCloudApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;

/**
 * @program: test
 * @description: 啟動類
 * @author: xingcheng
 * @create: 2018-12-9 15:39
 **/
@SpringCloudApplication
@EnableEurekaClient
@EnableZuulProxy
public class StartGateway {

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

}

就完成了一個簡單的閘道器路由

其中@EnableZuulProxy是@EnableZuulServer的加強,@SpringCloudApplication會包含@EnableEurekaClient,所以其實@EnableEurekaClient不需要寫

之後在寫兩個測試controller進行路由判斷:

package cn.chinotan.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @program: test
 * @description: zuul測試控制器
 * @author: xingcheng
 * @create: 2018-12-08 18:09
 **/
@RestController
@RequestMapping("/five")
public class ZuulTestFiveController {
    
    @GetMapping("/hello/{name}")
    public String ZuulTestFive(@PathVariable("name") String name) {
        return "hello " + name + "  this is ZuulTestFive";
    }
    
}

 

package cn.chinotan.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @program: test
 * @description: zuul測試控制器
 * @author: xingcheng
 * @create: 2018-12-08 18:09
 **/
@RestController
@RequestMapping("/three")
public class ZuulTestThreeController {
    
    @GetMapping("/hello/{name}")
    public String ZuulTestFive(@PathVariable("name") String name) {
        return "hello " + name + " this is ZuulTestThree";
    }
    
}

當然這些服務也得連線到euerka上,好讓代理閘道器可以根據service-id進行服務發現

其中,euerka上的連線情況如下:

可以看到閘道器伺服器的埠是8088,兩個服務的閘道器是5555-five和3333-three

接下來,我們不直接請求提供服務的three和five伺服器,而是請求閘道器代理

可以看到閘道器服務成功的路由了這兩次請求

服務過濾

Zuul還有一個主要的功能,便是服務過濾,比如,使用者在登入前,可以將服務請求過濾到指定的頁面去。 
在專案中,新增一個MyFilter類。程式碼如下:

@Component
public class MyFilter extends ZuulFilter {
    @Override
    public String filterType() {
        return FilterConstants.PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        return 0;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run(){
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();
        Object accessToken = request.getParameter("token");
        if(accessToken == null) {
            ctx.setSendZuulResponse(false);
            ctx.setResponseStatusCode(401);
            try {
                ctx.getResponse().setHeader("Content-Type", "text/html;charset=UTF-8");
                ctx.getResponse().getWriter().write("登入資訊為空!");
            }catch (Exception e){}
            return null;
        }
        return null;
    }
}

其中,filterType方法,返回一個字串代表過濾器的型別,在zuul中定義了四種不同生命週期的過濾器型別,具體如下:pre:路由之前 
routing:路由之時 
post: 路由之後 
error:傳送錯誤呼叫 
filterOrder:過濾的順序 
shouldFilter:這裡可以寫邏輯判斷,是否要開啟過濾 
run:過濾器的具體邏輯。可用很複雜,包括查sql,nosql去判斷該請求到底有沒有許可權訪問。
一般我們在使用時,不手打“pre”這些型別,而是通過呼叫Zuul中已寫好的FilterConstants類,其中封裝了所有的過濾器型別。這樣可以避免打錯字元而導致錯誤的發生。 
接下來,我們訪問http://localhost:9005/feign/welcome?name=Cheng 顯示:

登入資訊為空!

在後面加上token,http://localhost:9005/feign/welcome?name=Cheng&token=111就可以正常訪問了

類似的過濾器很多,我們可以自定義,從而實現統一的閘道器控制、監控、跨域、流量控制、負載均衡,身份認證,服務降級等等。