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: