我們瞭解了 Spring Cloud Zuul 作爲網關所具備的最基本功能:路由(Router),下面我們將關注 Spring Cloud Zuul 的另一核心功能:過濾器(Filter)
zuul的核心是一系列的filters, 其作用可以類比Servlet框架的Filter,或者AOP,本文我們就來具體介紹下自定義的zuul過濾器
1.Filter的使用場景
場景非常多:
請求鑑權:一般放在pre類型,如果發現沒有訪問權限,直接就攔截了
異常處理:一般會在error類型和post類型過濾器中結合來處理。
服務調用時長統計:pre和post結合使用。
2.ZuulFilter
ZuulFilter是過濾器的頂級父類。
在這裏我們看一下其中定義的4個最重要的方法:
public abstract ZuulFilter implements IZuulFilter{
abstract public String filterType();
abstract public int filterOrder();
boolean shouldFilter();// 來自IZuulFilter
Object run() throws ZuulException;// IZuulFilter
}
shouldFilter
:返回一個Boolean
值,判斷該過濾器是否需要執行。返回true執行,返回false不執行。run
:過濾器的具體業務邏輯。filterType
:返回字符串,代表過濾器的類型。包含以下4種:pre
:請求在被路由之前執行routing
:在路由請求時調用post
:在routing和errror過濾器之後調用error
:處理請求時發生錯誤調用
filterOrder
:通過返回的int值來定義過濾器的執行順序,數字越小優先級越高。
網關過濾器的自定義方法有四個,過濾器的類型有四個,分別如下:
3.Filter 的生命週期
這張是Zuul官網提供的請求生命週期圖,清晰的表現了一個請求在各個過濾器的執行順序。
Filter 的生命週期有 4 個,分別是 “PRE”、“ROUTING”、“POST” 和“ERROR”,整個生命週期可以用下圖來表示
Zuul 大部分功能都是通過過濾器來實現的,這些過濾器類型對應於請求的典型生命週期。
PRE:這種過濾器在請求被路由之前調用。我們可利用這種過濾器實現身份驗證、在集羣中選擇請求的微服務、記錄調試信息等。
ROUTING:這種過濾器將請求路由到微服務。這種過濾器用於構建發送給微服務的請求,並使用 Apache HttpClient 或 Netfilx Ribbon 請求微服務。
POST:這種過濾器在路由到微服務以後執行。這種過濾器可用來爲響應添加標準的 HTTP Header、收集統計信息和指標、將響應從微服務發送給客戶端等。
ERROR:在其他階段發生錯誤時執行該過濾器。 除了默認的過濾器類型,Zuul 還允許我們創建自定義的過濾器類型。例如,我們可以定製一種 STATIC 類型的過濾器,直接在 Zuul 中生成響應,而不將請求轉發到後端的微服務。
- 正常流程:
- 請求到達首先會經過pre類型過濾器,而後到達routing類型,進行路由,請求就到達真正的服務提供者,執行請求,返回結果後,會到達post過濾器。而後返回響應。
- 異常流程:
- 整個過程中,pre或者routing過濾器出現異常,都會直接進入error過濾器,再error處理完畢後,會將請求交給POST過濾器,最後返回給用戶。
- 如果是error過濾器自己出現異常,最終也會進入POST過濾器,而後返回。
- 如果是POST過濾器出現異常,會跳轉到error過濾器,但是與pre和routing不同的時,請求不會再到達POST過濾器了。
所有內置過濾器列表:
Zuul中默認實現的Filter執行順序:
如何禁用指定的Filter
可以在 application.yml 中配置需要禁用的 filter,格式爲
zuul.<SimpleClassName>.<filterType>.disable=true
。
比如要禁用org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter
就設置
zuul:
SendResponseFilter:
post:
disable: true
4.自定義Filter
4.1.創建Filter
首先自定義一個 Filter,繼承 ZuulFilter 抽象類,在 run() 方法中添加具體業務邏輯,具體如下:
package com.bruceliu.filter;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
/**
* @BelongsProject: springcloud0310
* @BelongsPackage: com.bruceliu.filter
* @Author: bruceliu
* @QQ:1241488705
* @CreateTime: 2020-03-12 12:18
* @Description: 自定義網關過濾器
*/
@Component
public class LogFilter extends ZuulFilter{
private Logger logger = LoggerFactory.getLogger(LogFilter.class);
/**
* 過濾方法
*/
@Override
public Object run() {
// 獲取Request上下文
RequestContext rc = RequestContext.getCurrentContext();
HttpServletRequest request = rc.getRequest();
logger.info("LogFilter .... 請求的路徑是{},請求提交的方式是{}", request.getRequestURL().toString(),request.getMethod());
return null;
}
/**
* 是否開啓過濾:默認false
*/
@Override
public boolean shouldFilter() {
// TODO Auto-generated method stub
return true;
}
/**
* 多個過濾器中的執行順序,數值越小,優先級越高
*/
@Override
public int filterOrder() {
// TODO Auto-generated method stub
return 0;
}
/**
* 過濾器的類型
*/
@Override
public String filterType() {
// TODO Auto-generated method stub
return "pre";
}
}
在上面實現的過濾器代碼中,我們通過繼承ZuulFilter抽象類並重寫了下面的四個方法來實現自定義的過濾器。這四個方法分別定義了:
filterType():過濾器的類型,它決定過濾器在請求的哪個生命週期中執行。這裏定義爲pre,代表會在請求被路由之前執行。
filterOrder():過濾器的執行順序。當請求在一個階段中存在多個過濾器時,需要根據該方法返回的值來依次執行。通過數字指定,數字越大,優先級越低。
shouldFilter():判斷該過濾器是否需要被執行。這裏我們直接返回了true,因此該過濾器對所有請求都會生效。實際運用中我們可以利用該函數來指定過濾器的有效範圍。
run():過濾器的具體邏輯。
4.2.啓動網關測試
重新啓動service-api-gateway,併發起下面的請求,對上面定義的過濾器做一個驗證:
啓動一個provider服務後,啓動我們的網關服務,然後請求訪問,查詢控制檯輸出
注意控制檯:
日誌有輸出,說明自定義的網關過濾器執行了。
5.過濾器小案例
5.1.服務鑑權
自定義過濾器:
package com.bruceliu.filter;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.Map;
import java.util.Set;
/**
* @BelongsProject: springcloud0310
* @BelongsPackage: com.bruceliu.filter
* @Author: bruceliu
* @QQ:1241488705
* @CreateTime: 2020-03-12 12:32
* @Description: TODO
*/
@Component
public class MyFilter extends ZuulFilter {
private Logger logger = LoggerFactory.getLogger(MyFilter.class);
/**
* 過濾器的類型,它決定過濾器在請求的哪個生命週期中執行。 這裏定義爲pre,代表會在請求被路由之前執行。
*
* @return
*/
@Override
public String filterType() {
return "pre";
}
/**
* filter執行順序,通過數字指定。 數字越大,優先級越低。
*
* @return
*/
@Override
public int filterOrder() {
return -1;
}
/**
* 判斷該過濾器是否需要被執行。這裏我們直接返回了true,因此該過濾器對所有請求都會生效。 實際運用中我們可以利用該函數來指定過濾器的有效範圍。
*
* @return
*/
@Override
public boolean shouldFilter() {
return true;
}
/**
* 過濾器的具體邏輯
*
* @return
*/
@Override
public Object run() {
//過濾器要執行的過濾操作
// 登錄校驗邏輯。
// 1)獲取Zuul提供的請求上下文對象
RequestContext ctx = RequestContext.getCurrentContext();
// 2) 從上下文中獲取request對象
HttpServletRequest req = ctx.getRequest();
System.out.println("=============================================");
System.out.println("用戶的請地址是:"+req.getRequestURI());
System.out.println("用戶的所有請求參數是:");
Map<String, String[]> map = req.getParameterMap();
Set<Map.Entry<String, String[]>> entries = map.entrySet();
for (Map.Entry<String, String[]> entry : entries) {
System.out.println(entry.getKey()+"---"+ Arrays.toString(entry.getValue()));
}
System.out.println("=============================================");
// 3) 從請求中獲取token
String token = req.getParameter("access-token");
// 4) 判斷
if(token == null || "".equals(token.trim())){
// 沒有token,登錄校驗失敗,攔截
ctx.setSendZuulResponse(false);
//設置信息
ctx.setResponseBody("No Rrights!");
// 返回401狀態碼。也可以考慮重定向到登錄頁。
ctx.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
}
// 校驗通過,可以考慮把用戶信息放入上下文,繼續向後執行
return null;
}
}
在上面實現的過濾器代碼中,我們通過繼承ZuulFilter抽象類並重寫了下面的四個方法來實現自定義的過濾器。這四個方法分別定義了:
filterType():過濾器的類型,它決定過濾器在請求的哪個生命週期中執行。這裏定義爲pre,代表會在請求被路由之前執行。
filterOrder():過濾器的執行順序。當請求在一個階段中存在多個過濾器時,需要根據該方法返回的值來依次執行。通過數字指定,數字越大,優先級越低。
shouldFilter():判斷該過濾器是否需要被執行。這裏我們直接返回了true,因此該過濾器對所有請求都會生效。實際運用中我們可以利用該函數來指定過濾器的有效範圍。
run():過濾器的具體邏輯。這裏我們通過ctx.setSendZuulResponse(false)令 Zuul 過濾該請求,不對其進行路由,然後通過ctx.setResponseStatusCode(401)設置了其返回的錯誤碼,當然我們也可以進一步優化我們的返回,比如,通過ctx.setResponseBody(body)對返回 body 內容進行編輯等。
重新啓動service-api-gateway,併發起下面的請求,對上面定義的過濾器做一個驗證:
訪問 http://127.0.0.1:2222/springcloud-consumer/test 返回 userToken is null
訪問 http://127.0.0.1:2222/springcloud-consumer/test?access-token=bruce