Eclipse配置運行SpringCloud(Hoxton + 2.2.4)微服務框架 + 搭建服務網關Zuul

簡介

服務網關

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

服務網關 = 路由轉發 + 過濾器

  • 路由轉發:接收一切外界請求,轉發到後端的微服務上去;例如,/可以映射到您的Web應用程序,/api/users映射到用戶服務,並將/api/shop映射到商店服務。
  • 過濾器:在服務網關中可以完成一系列的橫切功能,例如權限校驗、限流以及監控等,這些都可以通過過濾器完成(其實路由轉發也是通過過濾器實現的)。

Zuul

Netflix使用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信息返回客戶端。 在其他階段發生錯誤時執行”error filter“。 除了默認的過濾器類型,Zuul還允許我們創建自定義的過濾器類型。例如,我們可以定製一種STATIC類型的過濾器,直接在Zuul中生成響應,而不將請求轉發到後端的微服務。
在這裏插入圖片描述

創建服務網關

在這裏插入圖片描述

添加依賴包文件POM.xml

<?xml version="1.0"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <modelVersion>4.0.0</modelVersion>
  <parent>
    <groupId>com.springcloud</groupId>
    <artifactId>springcloud-root</artifactId>
    <version>0.0.1-SNAPSHOT</version>
  </parent>
  <artifactId>springcloud-zuul</artifactId>
  <name>springcloud-zuul</name>
  <url>http://maven.apache.org</url>
  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>
  <dependencies>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>3.8.1</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
    </plugins>
  </build>
</project>

配置application.yml文件

在這裏插入圖片描述

  • application.yml
spring:
  application:
    name: springcloud-zuul
  freemarker:
    prefer-file-system-access: false
  security:
    user:
      name: admin
      password: 123456
    
server:
  port: 8120

eureka:
  instance:
    hostname: eureka-zuul.com
    instance-id: eureka-zuul
  client:
    service-url:
      defaultZone: http://${spring.security.user.name}:${spring.security.user.password}@eureka-peer1.com:8897/eureka/,http://${spring.security.user.name}:${spring.security.user.password}@eureka-peer2.com:8898/eureka/,http://${spring.security.user.name}:${spring.security.user.password}@eureka-peer3.com:8899/eureka/

zuul:
  #接口前綴(v1作爲版本號)
  prefix: /v1
  routes:
    hiapi:
      path: /hiapi/**
      serviceId: springcloud-eureka-provider
    ribbonapi:
      path: /ribbonapi/**
      serviceId: springcloud-ribbon
    feignapi:
      path: /feignapi/**
      serviceId: springcloud-feign

修改C:\Windows\System32\drivers\etc\hosts

127.0.0.1 eureka-zuul.com

禁用指定的Filter

可以在 application.yml 中配置需要禁用的 filter,格式爲

zuul.<SimpleClassName>.<filterType>.disable=true

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

zuul:
  SendResponseFilter:
    post:
      disable: true

添加服務網關啓動類

在這裏插入圖片描述

  • ZuulApplication.java

加上註解@EnableZuulProxy

package org.springcloud.zuul;

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

@SpringBootApplication
@EnableZuulProxy
public class ZuulApplication {

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

依次啓動項目

springcloud-eureka-cluster-peer1
springcloud-eureka-cluster-peer2
springcloud-eureka-cluster-peer3
springcloud-eureka-provider1
springcloud-eureka-provider2
springcloud-eureka-provider3
springcloud-ribbon
springcloud-feign
springcloud-zuul

在這裏插入圖片描述

  1. 瀏覽器多次訪問http://eureka-zuul.com:8120/v1/hiapi/hi?name=zhaojq
    在這裏插入圖片描述
    zuul在路由轉發做了負載均衡

  2. 瀏覽器多次訪問http://eureka-zuul.com:8120/v1/ribbonapi/hi?name=zhaojq
    在這裏插入圖片描述
    Zuul和Ribbon相結合,實現了負載均衡(隨機策略)

  3. 瀏覽器多次訪問http://eureka-zuul.com:8120/v1/feignapi/hi?name=zhaojq
    在這裏插入圖片描述
    Zuul和Feign相結合,實現了負載均衡(隨機策略)

在服務網關Zuul上配置熔斷器

實現熔斷器功能需要實現FallbackProvider接口,實現該接口的兩個方法,一個是getRoute(),用於指定熔斷器功能應用於哪些路由的服務;另一個方法fallbackResponse()爲進入熔斷器功能時執行的邏輯。
在這裏插入圖片描述

  • ZuulHystrix.java

如果需要所有的路由服務都加熔斷功能,需要在getRoute()方法上返回”*“的匹配符

package org.springcloud.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 ZuulHystrix implements FallbackProvider {
    @Override
    //指定熔斷器功能應用於哪些路由的服務
    public String getRoute() {
        //return "springcloud-eureka-provider";
    	return "*";
    }

    @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("eureka-provider is down!! this is the fallback.".getBytes());
            }

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

全部正常啓動後,停止 springcloud-eureka-provider2 提供者,端口爲:8002服務
訪問命令窗口curl http://eureka-zuul.com:8120/v1/hiapi/hi?name=zhaojq,斷路器已經生效,提示:提供者服務掛了
在這裏插入圖片描述

自定義過濾器Zuul Filter

Zuul包括以下4中過濾器

  • PRE過濾器:是在請求路由到具體服務之前執行的,可以做安全驗證,如身份驗證,參數驗證。
  • ROUTING過濾器:它用於將請求 路由到具體的微服務實例。默認使用Http Client進行網絡請求。
  • POST過濾器:在請求已被路由到微服務後執行的。可用作收集統計信息、指標,以及將響應傳輸到客戶端。
  • ERROR過濾器:在其他過濾器發生錯誤時執行。

Zuul中默認實現的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 處理正常的請求響應

實現自定義濾器需要繼承ZuulFilter,實現ZuulFilter中的抽象方法,包括filterType(),filterOrder()以及IZuulFilter的shouldFilter()和run()方法。

  • filterType()爲過濾器類型,有4中類型:pre、post、routing和error。
  • filterOrder()是過濾順序,它爲一個int類型的值,值越小,越早執行該過濾器。
  • shouldFilter()表示是否需要執行該過濾器邏輯,true表示執行,false表示不執行,如果true則執行run()方法。
  • run()方法是具體的過濾的邏輯。本例中檢查請求的參數中是否傳了token或password這個參數,如果沒有傳,則請求不被路由到具體的服務實例,直接返回響應,狀態碼爲401。

TokenFilter 過濾器

在這裏插入圖片描述

  • TokenFilter.java
package org.springcloud.zuul;

import java.io.IOException;

import javax.servlet.http.HttpServletRequest;

import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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

public class TokenFilter extends ZuulFilter {

    private static Logger LOGGER=LoggerFactory.getLogger(TokenFilter.class);

    @Override
    //定義filter的類型,有pre、route、post、error四種
    public String filterType() {
        //可以在請求被路由之前調用
    	return "pre"; 
    }

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

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

    @Override
    //filter需要執行的具體操作
    public Object run() throws ZuulException { 
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();
        LOGGER.info("--->>> TokenFilter {},{}", request.getMethod(), request.getRequestURL().toString());
        
        //獲取請求的參數
        String token = request.getParameter("token");
        
        if (StringUtils.isNotBlank(token)) {
        	//對請求進行路由
            ctx.setSendZuulResponse(true);
            ctx.setResponseStatusCode(200);
            ctx.set("isSuccess", true);
            return null;
        } else {
        	LOGGER.warn("token is empty");
            //不對請求進行路由
            ctx.setSendZuulResponse(false);
            ctx.setResponseStatusCode(401);
            try {
                ctx.getResponse().getWriter().write("token is empty");
            } catch (IOException e) {
                e.printStackTrace();
            }
            ctx.set("isSuccess", false);
            return null;
        }
    }
}

開啓過濾器,在程序的啓動類 ZuulFilterApplication 添加 Bean

  • ZuulApplication.java
package org.springcloud.zuul;

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

@SpringBootApplication
@EnableZuulProxy
public class ZuulApplication {

    public static void main(String[] args) {
        SpringApplication.run(ZuulApplication.class, args);
    }
    
    @Bean
    public TokenFilter TokenFilter() {
        return new TokenFilter();
    }
}

重啓項目後,訪問http://eureka-zuul.com:8120/v1/hiapi/hi?name=zhaojq
在這裏插入圖片描述
提示 token is empty。

訪問http://eureka-zuul.com:8120/v1/hiapi/hi?name=zhaojq&token=cc
在這裏插入圖片描述
可見,TokenFilter這個Bean注入IOC容器後,對請求進行了過濾,並在請求路由轉發之前進行了邏輯判斷。

提示 token is empty顯示兩遍錯誤

在這裏插入圖片描述
在這裏插入圖片描述
把自定義TokenFilter的@Component取消掉就可以了,不讓它被Spring容器管理。

PasswordFilter 過濾器

在這裏插入圖片描述

  • PasswordFilter.java
package org.springcloud.zuul;

import java.io.IOException;

import javax.servlet.http.HttpServletRequest;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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

public class PasswordFilter extends ZuulFilter {

    private final Logger LOGGER = LoggerFactory.getLogger(PasswordFilter.class);

    @Override
    public String filterType() {
    	//請求已被路由到微服務後執行
        return "post";
    }

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

    @Override
    public boolean shouldFilter() {
        RequestContext ctx = RequestContext.getCurrentContext();
        //判斷上一個過濾器結果爲true,否則就不走下面過濾器,直接跳過後面的所有過濾器並返回 上一個過濾器不通過的結果
        return (boolean) ctx.get("isSuccess");     
    }

    @Override
    public Object run() throws ZuulException {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();
        LOGGER.info("--->>> PasswordFilter {},{}", request.getMethod(), request.getRequestURL().toString());

        String username = request.getParameter("password");
        if (null != username && username.equals("123456")) {
            ctx.setSendZuulResponse(true);
            ctx.setResponseStatusCode(200);
            ctx.set("isSuccess", true);
            return null;
        } else {
            ctx.setSendZuulResponse(false);
            ctx.setResponseStatusCode(401);
            try {
                ctx.getResponse().getWriter().write("The password cannot be empty");
            } catch (IOException e) {
                e.printStackTrace();
            }
            ctx.set("isSuccess", false);
            return null;
        }
    }
}

開啓過濾器,在程序的啓動類 ZuulFilterApplication 添加 Bean

  • ZuulApplication.java
package org.springcloud.zuul;

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

@SpringBootApplication
@EnableZuulProxy
public class ZuulApplication {

    public static void main(String[] args) {
        SpringApplication.run(ZuulApplication.class, args);
    }
    
    @Bean
    public TokenFilter TokenFilter() {
        return new TokenFilter();
    }

    @Bean
    public PasswordFilter PasswordFilter() {
        return new PasswordFilter();
    }
}

訪問http://eureka-zuul.com:8120/v1/hiapi/hi?name=zhaojq&token=cc&password=123456
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章