Spring Boot + Spring Cloud 實現權限管理系統 後端篇(二十一):服務網關(Zuul)

在線演示

演示地址:http://139.196.87.48:9002/kitty前端

用戶名:admin 密碼:adminjava

技術背景

前面咱們經過Ribbon或Feign實現了微服務之間的調用和負載均衡,那咱們的各類微服務又要如何提供給外部應用調用呢。git

固然,由於是REST API接口,外部客戶端直接調用各個微服務是沒有問題的,但出於種種緣由,這並非一個好的選擇。spring

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

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

面對相似上面的問題,咱們要如何解決呢?答案就是:服務網關!後端

使用服務網關具備如下幾個優勢:跨域

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

Spring Cloud Zuul

服務網關是微服務架構中一個不可或缺的部分。經過服務網關統一貫外系統提供REST API的過程當中,除了具有服務路由、均衡負載功能以外,它還具有了權限控制等功能。瀏覽器

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

在Spring Cloud體系中, Spring Cloud Zuul 封裝了Zuul組件,做爲一個API網關,負責提供負載均衡、反向代理和權限認證。app

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/

測試效果

依次啓動註冊中心、監控、服務提供者、服務消費者、服務網關等項目。

訪問 http://localhost:8010/ribbon/call, 效果以下圖。

訪問 http://localhost:8010/feign/call, 效果以下圖。

說明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 的方式訪問。

訪問 http://localhost:8010/kitty-consumer/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; } }; } }

從新啓動,訪問 http://localhost:8010/kitty-consumer/feign/call, 能夠正常訪問

停掉 kitty-consumer 服務, 再次訪問 http://localhost:8010/kitty-consumer/feign/call, 效果以下。

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

自定義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項目,訪問 http://localhost:8010/kitty-consumer/feign/call,效果以下。

請求時帶上token,訪問 http://localhost:8010/kitty-consumer/feign/call?token=111,效果以下。

高可用性

Zuul做爲API服務網關,不一樣的客戶端使用不一樣的負載將請求統一分發到後端的Zuul,再有Zuul轉發到後端服務。所以,爲了保證Zuul的高可用性,前端能夠同時開啓多個Zuul實例進行負載均衡,另外,在Zuul的前端還可使用Nginx或者F5再次進行負載轉發,從而保證Zuul的高可用性。

 

源碼下載

後端:https://gitee.com/liuge1988/kitty

前端:https://gitee.com/liuge1988/kitty-ui.git


做者:朝雨憶輕塵
出處:https://www.cnblogs.com/xifengxiaoma/ 版權全部,歡迎轉載,轉載請註明原文做者及出處。

相關文章
相關標籤/搜索