1. 程式人生 > >SpringCloud Zuul配置 微服務閘道器 詳解

SpringCloud Zuul配置 微服務閘道器 詳解

目錄

筆者在,已經介紹瞭如何配置獨立的認證中心和SSO客戶端,以及完成配套的資源伺服器配置。但是,在實際生產環境中使用會存在一些安全問題。例如所有微服務的真實地址和使用的真實埠都被暴露給了使用者,容易被不法之徒進行非法攻擊,再對外服務時如果做隔離,對每個服務都需要做單獨的配置,無論是通過NGINX代理服務或硬體閘道器等方式處理,工作都很繁瑣,當有成百的微服務部署時,這個工程量可想而知多麼浩大。另外,當所有微服務都要在呼叫前後進行一些通用的處理時,通過微服務閘道器新增過濾器進行預處理也是極為方便的事。因此利用SpringCloud+Zuul來配置統一的微服務閘道器,達到上述需求。

例如當前有下列相關資源

認證服務端  http://localhost:8000

應用客戶端  http://localhost:8080

資源服務端  http://localhost:7779

訪問邏輯需下圖:

使用zuul微服務閘道器後訪問例項

首先可在ZUUL將微服務路徑進行對映

認證服務端  http://localhost:8000 ===>  http://localhost/sso

應用客戶端  http://localhost:8080 ===>  http://localhost/manage

資源服務端  http://localhost:7779 ===>  http://localhost/product

需要新增加微服務閘道器的服務

微服務閘道器 開啟預設的80或443埠(實際上訪問localhost)

預期的加入微服務閘道器後使用示例情況如下圖:

開啟微服務閘道器後,實現服務實際地址向虛擬地址的對映,所有對虛擬地址的操作均可以透明反應到真實的微服務上。

二、zuul微服務閘道器主要配置

首先是需要引入pom的依賴

<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/maven-v4_0_0.xsd">

      <modelVersion>4.0.0</modelVersion>

      <parent>

           <groupId>org.springframework.boot</groupId>

           <artifactId>spring-boot-starter-parent</artifactId>

           <version>1.5.13.RELEASE</version>

           <relativePath></relativePath>

      </parent>

      <artifactId>gateway</artifactId>

      <packaging>jar</packaging>

      <name>gateway</name>

      <properties>

           <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

           <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>

           <java.version>1.8</java.version>

      </properties>

      <dependencyManagement>

           <dependencies>

                 <dependency>

                      <groupId>org.springframework.cloud</groupId>

                      <artifactId>spring-cloud-dependencies</artifactId>

                      <version>Dalston.SR5</version>

                      <type>pom</type>

                      <scope>import</scope>

                 </dependency>

           </dependencies>

      </dependencyManagement>

      <dependencies>

           <dependency>

                 <groupId>org.springframework.boot</groupId>

                 <artifactId>spring-boot-starter-thymeleaf</artifactId>

           </dependency>

           <dependency>

                 <groupId>org.springframework.boot</groupId>

                 <artifactId>spring-boot-starter-web</artifactId>

           </dependency>

           <dependency>

                 <groupId>org.springframework.cloud</groupId>

                 <artifactId>spring-cloud-starter-eureka</artifactId>

           </dependency>

           <dependency>

                 <groupId>org.springframework.cloud</groupId>

                 <artifactId>spring-cloud-starter-config</artifactId>

           </dependency>

           <dependency>

                 <groupId>org.springframework.cloud</groupId>

                 <artifactId>spring-cloud-starter-zuul</artifactId>

           </dependency>

           <dependency>

                 <groupId>org.springframework.cloud</groupId>

                 <artifactId>spring-cloud-starter-hystrix</artifactId>

           </dependency>

           <dependency>

                 <groupId>com.google.code.gson</groupId>

                 <artifactId>gson</artifactId>

           </dependency>

      </dependencies>

      <build>

           <plugins>

                 <plugin>

                      <groupId>org.springframework.boot</groupId>

                      <artifactId>spring-boot-maven-plugin</artifactId>

                      <configuration>

                            <fork>true</fork>

                      </configuration>

                 </plugin>

           </plugins>

      </build>

</project>

===========接下來編寫入口程式===========

@SpringBootApplication

@EnableZuulProxy

public class GatwayApplication {

      public static void main(String[] args) {

           SpringApplication.run(GatwayApplication.class, args);

      }

}

很簡單,只需要增加一個開啟@EnableZuulProxy註解的配置即可

============接下來配置zuul的屬性配置檔案即可============

#關閉彈出的預設認證登入框

security.basic.enabled=false

#忽略框架預設的服務對映路徑

zuul.ignoredServices='*'

#不忽略框架與許可權相關的頭資訊

zuul.ignoreSecurityHeaders=false

#不忽略任何頭部資訊,所有header都轉發到下游的資源伺服器

zuul.sensitiveHeaders=

#以下是自定義服務與路徑的對映關係,也可以通過path和url直接對映

zuul.product.serviceId=product

zuul.product.path=/product/**

zuul.sso.serviceId=sso

zuul.sso.path=/sso/**

zuul.manage.serviceId=manage

zuul.manage.path=/manage/**

#閘道器的session名字,建議每個微服務都單獨命名

server.session.cookie.name= GATWAY_SESSION

server.port=80

spring.application.name=gatway

#配置註冊中心的地址,以便根據serviceID去發現這些service

eureka.instance.hostname=localhost

eureka.client.serviceUrl.defaultZone=http://localhost:7771/eureka

OK,如此簡單,微服務配置既完成了。

如果微服務沒有許可權驗證,通過對映的地址:例如

在瀏覽器輸入http://localhost/manage/index即可訪問到http://localhost:8080/index相同的頁面資訊了。

如下所示訪問http://localhost/manage/index

在訪問http://localhost:8080/index

都返回了正確的、真實的資源頁面。訪問REST服務介面也是同樣。

三、Zuul微服務閘道器預設配置的坑

當訪問的微服務某一個頁面若發生重定向,重定向會把真實的URL地址和真實的埠暴露在位址列中,且登入認證成功後也無法正常回調到需要鑑權的應用頁面。如下面流轉邏輯示例:

我們的理想如下圖:

但現實很殘酷,實際情況是這樣:

查了下網上解決方案,說的要在配置檔案中加上如下屬性:

zuul.addHostHeader=true

加上上面這個配置後,情況變得更槽糕了,情況如下圖邏輯所示

顯然這個返回有問題,並沒有將當前服務的字首載入Host後面。

實際上,zuul閘道器是在這裡做了一個向後臺實際微服務請求的動作,並重新組裝成了返回給客戶端的Response。

經過斷點發現,當請求的頁面發生重定向,並且配置zuul.addHostHeader=true後,返回reponse的header中location地址會直接將真實地址的Host:port替換為閘道器的Host:port。例如返回的真實地址是localhost:8000/login,會直接被替換為localhost/login。但由於localhost:8000/login實際應該對映到localhost/sso/login,因此造成了返回地址404-Not-Found(但筆者實在沒有找到這個替換的動作在哪裡執行的)

下面說下筆者如何填這個坑,可能辦法有點土,但能解決實際問題。

四、重定向無法獲取正確路徑的填坑過程

解決思路

微服務閘道器對後臺服務資源的代理請求均是在RibbonRoutingFilter這個過濾器中實現的,具體業務處理方法為

@Override

      public Object run() {

           RequestContext context = RequestContext.getCurrentContext();

           this.helper.addIgnoredHeaders();

           try {

                 RibbonCommandContext commandContext = buildCommandContext(context);

                 ClientHttpResponse response = forward(commandContext);

                 setResponse(response);

                 return response;

           }

           catch (ZuulException ex) {

                 throw new ZuulRuntimeException(ex);

           }

           catch (Exception ex) {

                 throw new ZuulRuntimeException(ex);

           }

      }

只要覆寫這個方法,將微服務返回的Response進行處理後再提交給客戶端即可。當然覆寫後,要用自定義的過濾器來替換掉原來註冊的過濾器。

具體改造方案如下:

======首先新建一個過濾器,直接繼承自RibbonRoutingFilter=======

package ywcai.ls.gateway;

//開啟這個註解,zuul會在配置時自動新增至過濾器鏈

@Component

public class FixRibbonRoutingFilter extends RibbonRoutingFilter{

//例項化過濾器需要的輔助類,在自動化配置中已經有例項,直接註解注入即可

@Autowired

ProxyRequestHelper helper;

//例項化過濾器需要的工廠類,在自動化配置中已經有例項,直接註解注入即可

@Autowired

RibbonCommandFactory<?> ribbonCommandFactory;

//如果重定向之前沒有頁面,則給一個預設的地址  

String defaultSuccessUrl="/index";

//輔助方法,下面會介紹

      public void setDefaultSuccessUrl(String url)

      {

           if(url.equals("/")||url.equals(""))

           {

                 return ;

           }

           this.defaultSuccessUrl=url.startsWith("/")?url:"/"+url;

      }

//構造類,整合付方法即可

      public FixRibbonRoutingFilter(ProxyRequestHelper helper, RibbonCommandFactory<?> ribbonCommandFactory) {

           super(helper, ribbonCommandFactory,Collections.emptyList());

           // TODO Auto-generated constructor stub

      }

//輔助方法類,下面會介紹

      private void addPathCache(String requestPath,String requestServiceId )

      {

           if(requestPath.equals("/")||requestPath.equals(""))

           {

                 requestPath=defaultSuccessUrl;

           }

           HttpSession cache=RequestContext.getCurrentContext().getRequest().getSession();

           if(!isHasCache(requestPath))

           {

                 cache.setAttribute(requestPath,requestServiceId);

           }

      }

//輔助方法類,判斷是否有快取

      private boolean isHasCache(String requestPath)

      {

           HttpSession cache=RequestContext.getCurrentContext().getRequest().getSession();

           return cache.getAttribute(requestPath)!=null?true:false;

      }

//輔助方法類,下面會介紹

      private String getServiceIdAndRemove(String requestPath)

      {

           HttpSession cache=RequestContext.getCurrentContext().getRequest().getSession();

           String serviceId="";

           if(isHasCache(requestPath))

           {

                 serviceId= (String) cache.getAttribute(requestPath);

                 cache.removeAttribute(requestPath);

           }

           return serviceId;

      }

//輔助方法類,組裝正確的重定向地址

      private void assembleRealPath(ClientHttpResponse response, URI location,

                 String nowPath,String serviceId) {

           int nowPort=location.getPort()<=0?80:location.getPort();

           String newPath=

                      location.getScheme()+"://"+location.getHost()+":"

                                  +nowPort+"/"+serviceId+nowPath;

      newPath=location.getQuery()==null?newPath:(newPath+"?"+location.getQuery());

      newPath=location.getFragment()==null?newPath:(newPath+"#"+location.getFragment());

           URI newLocation=null;

           try {

                 newLocation = new URI(newPath);

           } catch (URISyntaxException e) {

                 // TODO Auto-generated catch block

                 e.printStackTrace();

           }

           response.getHeaders().setLocation(newLocation);

      }

//核心業務邏輯,其他都不變,增加對reponse的處理邏輯即可

      @Override

      public Object run() {

           // TODO Auto-generated method stub

           RequestContext context = RequestContext.getCurrentContext();

           this.helper.addIgnoredHeaders();

           try {

                 RibbonCommandContext commandContext = buildCommandContext(context);

//獲取重定向前訪問的url的資源路徑,這個地址是不包含host的

                 String preUrl=commandContext.getUri();

                 //如果現在被重定向到的是登入頁面,則快取訪問前一刻資源的路徑和服務ID,並且只快取記錄這個SESSION訪問的第一個ServiceID

                 if(preUrl.equals("/login"))

                 {

//記錄登入時的serviceID作為預設的serviceID

      addPathCache(defaultSuccessUrl,commandContext.getServiceId());

                 }

                 ClientHttpResponse response = forward(commandContext);

//下面是具體的reponse處理邏輯

                 URI location=response.getHeaders().getLocation(); 

      if(response.getStatusCode()==HttpStatus.FOUND&&location!=null)

                 {    

          

                      String nowPath=location.getPath();  

                      if(nowPath.equals("/login"))

                      {

      //如果是被重定向了,則記錄之前的路徑

                            String serviceId=commandContext.getServiceId();

                            addPathCache(preUrl,serviceId);

                            assembleRealPath(response,location,nowPath,  serviceId);

                      }

                      else if(isHasCache(nowPath))

                      {

//如果是快取過這個頁面,則獲取快取路徑重新封裝並重定向到快取位置

                            String serviceId=getServiceIdAndRemove(nowPath);

                      assembleRealPath(response,location,nowPath,serviceId);

                      }

                     

                      else if(nowPath.equals("/")||nowPath.equals(""))

                      {

//如果資源是"/"或為空,則代表是直接在瀏覽器輸入login頁面登入的,轉到預設頁面。                         

String serviceId=getServiceIdAndRemove(defaultSuccessUrl);              assembleRealPath(response,location,defaultSuccessUrl,serviceId);

                      }

                      //其他情況則什麼也不做

                      else

                      {

                      }

                 }

                 setResponse(response);

                 return response;

           }

           catch (ZuulException ex) {

                 throw new ZuulRuntimeException(ex);

           }

           catch (Exception ex) {

                 throw new ZuulRuntimeException(ex);

           }

      }

}

======然後在啟動類中將原來的RibbonRoutingFilter剔除======  

package ywcai.ls.gateway;

@SpringBootApplication

@EnableZuulProxy

public class GatwayApplication {

      //RibbonRoutingFilter

      public static void main(String[] args) {

           // TODO Auto-generated method stub

           SpringApplication.run(GatwayApplication.class, args);

           removeDefaultRibbonFilter();

      }

      private static void removeDefaultRibbonFilter() {

//只需要刪除系統的ribbonRoutingFilter即可,自定義的會自動注入。

           FilterRegistry r=FilterRegistry.instance();

           r.remove("ribbonRoutingFilter");

      }

}

==============很簡單,看下改造後的效果==============

訪問localhost/manage/test

經過多次重定向後,最後定向到認證中心的login頁面-Localhost/sso/login

提交賬號密碼後,又經過多次認證,重定向。返回到了第一次我想要訪問的頁面-locahlost/manage/test

五、關於熔斷和超時設定的問題

設定熔斷的時間,如果發生了熔斷,預設為進行一次請求重發。

這個配置建議在閘道器和微服務都需要設定,否則會有一個預設值生效

hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=40000

設定ribbon的網路請求超時時間。

ribbon.ReadTimeout=30000

ribbon.ConnectTimeout=30000

建議在閘道器和微服務也都進行配置

如果本身是微服務A,又訪問了其他的微服務B,那這兩個配置會對A造成影響,微服務B超時會引起微服務A報超時錯誤。

總的來說,就是時間設定較小的設定會影響另外的配置。

一般熔斷超時時間應該設定的比ribbon的網路請求超時時間長。

相關原始碼請檢視作者git: