在〈使用 Zuul〉中談過,在採用閘道之後,如果想要知道,哪個客戶端的使用程度等資訊,或者是對某些客戶端加以控管,這類橫切的需求,也可以在閘道上實現,具體來說,可以透過過濾器來實現。
有關於 Zuul 過濾器的撰寫,在〈Router and Filter: Zuul〉有著詳細的說明,範例主要放在〈Custom Zuul Filter Examples〉該節。
簡單來說,可以繼承 ZuulFilter
類別來實現過濾器,ZuulFilter
是個抽象類別,實作了 IZuulFilter
介面(定義了 shouldFilter
、run
方法),並定義了抽象方法 filterOrder
、filterType
,因此在繼承之後,必須實作的方法有四個:
filterOrder
:傳回數字,用來判斷過濾器執行的順序,可以與其他過濾器具有相同數字,也不一定要連續。filterType
:過濾器的類型,可以是FilterConstants.PRE_TYPE
("pre"
)、FilterConstants.ROUTE_TYPE
("route"
)、FilterConstants.POST_TYPE
("post"
)或FilterConstants.ERROR_TYPE
("error"
)。shouldFilter
:是否執行過濾器的run
方法。run
:過濾的實現流程要定義在這個方法之中。
過濾器的執行時間點依序會是前置過濾器(Pre Filter)、路由過濾器(Route Filter)、目標服務、後置過濾器(Post Filter),Zuul 過濾器生命週期間若拋出例外,會由錯誤過濾器(Error Filter)處理,這個過程可以在 ZuulServlet.java 中看到:
@Override
public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException {
try {
init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse);
// Marks this request as having passed through the "Zuul engine", as opposed to servlets
// explicitly bound in web.xml, for which requests will not have the same data attached
RequestContext context = RequestContext.getCurrentContext();
context.setZuulEngineRan();
try {
preRoute();
} catch (ZuulException e) {
error(e);
postRoute();
return;
}
try {
route();
} catch (ZuulException e) {
error(e);
postRoute();
return;
}
try {
postRoute();
} catch (ZuulException e) {
error(e);
return;
}
} catch (Throwable e) {
error(new ZuulException(e, 500, "UNHANDLED_EXCEPTION_" + e.getClass().getName()));
} finally {
RequestContext.getCurrentContext().unset();
}
}
來看看如何定義一個前置過濾器,若請求參數中沒有 token
參數,就不進行路由:
package cc.openhome;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import javax.servlet.http.HttpServletRequest;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;
@Component
public class TokenParamPreFilter extends ZuulFilter {
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
if (request.getParameter("token") == null) {
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(403);
}
return null;
}
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}
@Override
public int filterOrder() {
return FilterConstants.PRE_DECORATION_FILTER_ORDER - 1;
}
}
Spring 如果可以在管理的 Bean 中發現 Zuul 過濾器,就會自動安裝,因此在這邊加註了 @Component
。
Zuul 使用 RequestContext
在過濾器執行期間傳遞訊息,個別請求的資訊是放在 ThreadLocal
中,在上例中,取得了 HttpServletRequest
以便檢查是否有 token
參數,若無就設置 setSendZuulResponse(false)
,這會使得 RequetContext
的 sendZuulResponse
傳回 false
。
還記得在〈使用 Zuul〉中加註了 @EnableZuulProxy
嗎?在〈@EnableZuulProxy Filters〉中可以看到,Spring Cloud 會據此而載入一些過濾器,其中包含了 RibbonRoutingFilter
,而 RibbonRoutingFilter
的 shouldFilter
是這麼實現的:
@Override
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
return (ctx.getRouteHost() == null && ctx.get(SERVICE_ID_KEY) != null
&& ctx.sendZuulResponse());
}
RibbonRoutingFilter
會判斷服務 ID,使用 HttpClient
來請求目標服務,在它 shouldFilter
中可以看到 ctx.sendZuulResponse()
是決定要不要執行 run
方法的依據之一,因此在 @EnableZuulProxy
下,想要拒絕路由,就是透過 setSendZuulResponse(false)
。
不過,要注意的是,過濾器是否執行 run
方法,完全是由 shouldFilter
來決定,而不是 setSendZuulResponse(false)
(令 sendZuulResponse
傳回 false
),如果你的過濾器 shouldFilter
並沒有依據 sendZuulResponse
來決定,setSendZuulResponse(false)
之後,過濾器還是會執行。
在設置 @EnableZuulProxy
中,Spring Cloud 會安裝 PreDecorationFilter
,用來確定路由的位置與方式,在上頭的 TokenParamPreFilter
中,filterOrder
傳回 FilterConstants.PRE_DECORATION_FILTER_ORDER - 1
,這表示 TokenParamPreFilter
會在 PreDecorationFilter
前執行。
在這邊就只簡介一些必須注意的地方,像是 shouldFilter
、setSendZuulResponse(false)
真正的作用,由於〈Router and Filter: Zuul〉寫得蠻詳細的,真想實現 Zuul 過濾器,可以仔細閱讀一下。