Spring Cloud實戰小貼士:Zuul處理Cookie和重定向
由于我們在之前所有的入門教程中,對于HTTP請求都采用了簡單的接口實現。而實際使用過程中,我們的HTTP請求要復雜的多,比如當我們將Spring Cloud Zuul作為API網關接入網站類應用時,往往都會碰到下面這兩個非常常見的問題:
- 會話無法保持
- 重定向后的HOST錯誤
本文將幫助大家分析問題原因并給出解決這兩個常見問題的方法。
一、會話保持問題
通過跟蹤一個HTTP請求經過Zuul到具體服務,再到返回結果的全過程。我們很容易就能發現,在傳遞的過程中,HTTP請求頭信息中的Cookie和Authorization都沒有被正確地傳遞給具體服務,所以最終導致會話狀態沒有得到保持的現象。
那么這些信息是在哪里丟失的呢?我們從Zuul進行路由轉發的過濾器作為起點,來一探究竟。下面是RibbonRoutingFilter過濾器的實現片段:
- public class RibbonRoutingFilter extends ZuulFilter{
- ...
- protected ProxyRequestHelper helper;
- @Override
- public Object run() {
- RequestContext context = RequestContext.getCurrentContext();
- this.helper.addIgnoredHeaders();
- try {
- RibbonCommandContext commandContext = buildCommandContext(context);
- ClientHttpResponse response = forward(commandContext);
- setResponse(response);
- return response;
- }
- ...
- return null;
- }
- protected RibbonCommandContext buildCommandContext(RequestContext context) {
- HttpServletRequest request = context.getRequest();
- MultiValueMap<String, String> headers = this.helper
- .buildZuulRequestHeaders(request);
- MultiValueMap<String, String> params = this.helper
- .buildZuulRequestQueryParams(request);
- ...
- }
- }
這里有三個重要元素:
- 過濾器的核心邏輯run函數實現,其中調用了內部函數buildCommandContext來構建上下文內容
- 而buildCommandContext中調用了helper對象的buildZuulRequestHeaders方法來處理請求頭信息
- helper對象是ProxyRequestHelper類的實例
接下來我們再看看ProxyRequestHelper的實現:
- public class ProxyRequestHelper {
- public MultiValueMap<String, String> buildZuulRequestHeaders(
- HttpServletRequest request) {
- RequestContext context = RequestContext.getCurrentContext();
- MultiValueMap<String, String> headers = new HttpHeaders();
- Enumeration<String> headerNames = request.getHeaderNames();
- if (headerNames != null) {
- while (headerNames.hasMoreElements()) {
- String name = headerNames.nextElement();
- if (isIncludedHeader(name)) {
- Enumeration<String> values = request.getHeaders(name);
- while (values.hasMoreElements()) {
- String value = values.nextElement();
- headers.add(name, value);
- }
- }
- }
- }
- Map<String, String> zuulRequestHeaders = context.getZuulRequestHeaders();
- for (String header : zuulRequestHeaders.keySet()) {
- headers.set(header, zuulRequestHeaders.get(header));
- }
- headers.set(HttpHeaders.ACCEPT_ENCODING, "gzip");
- return headers;
- }
- public boolean isIncludedHeader(String headerName) {
- String name = headerName.toLowerCase();
- RequestContext ctx = RequestContext.getCurrentContext();
- if (ctx.containsKey(IGNORED_HEADERS)) {
- Object object = ctx.get(IGNORED_HEADERS);
- if (object instanceof Collection && ((Collection<?>) object).contains(name)) {
- return false;
- }
- }
- ...
- }
- }
從上述源碼中,我們可以看到構建頭信息的方法buildZuulRequestHeaders通過isIncludedHeader函數來判斷當前請求的各個頭信息是否在忽略的頭信息清單中,如果是的話就不組織到此次轉發的請求中去。那么這些需要忽略的頭信息是在哪里初始化的呢?在PRE階段的PreDecorationFilter過濾器中,我們可以找到答案:
- public class PreDecorationFilter extends ZuulFilter{
- ...
- public Object run() {
- RequestContext ctx = RequestContext.getCurrentContext();
- final String requestURI = this.urlPathHelper.getPathWithinApplication(ctx.getRequest());
- Route route = this.routeLocator.getMatchingRoute(requestURI);
- if (route != null) {
- String location = route.getLocation();
- if (location != null) {
- ctx.put("requestURI", route.getPath());
- ctx.put("proxy", route.getId());
- // 處理忽略頭信息的部分
- if (!route.isCustomSensitiveHeaders()) {
- this.proxyRequestHelper.addIgnoredHeaders(
- this.properties.getSensitiveHeaders()
- .toArray(new String[0]));
- } else {
- this.proxyRequestHelper.addIgnoredHeaders(
- route.getSensitiveHeaders()
- .toArray(new String[0]));
- }
- ...
- }
從上述源碼中,我們可以看到有一段if/else塊,通過調用ProxyRequestHelper的addIgnoredHeaders方法來添加需要忽略的信息到請求上下文中,供后續ROUTE階段的過濾器使用。這里的if/else塊分別用來處理全局設置的敏感頭信息和指定路由設置的敏感頭信息。而全局的敏感頭信息定義于ZuulProperties中:
- @Data
- @ConfigurationProperties("zuul")
- public class ZuulProperties{
- private Set<String> sensitiveHeaders = new LinkedHashSet<>(
- Arrays.asList("Cookie", "Set-Cookie", "Authorization"));
- ...
- }
所以解決該問題的思路也很簡單,我們只需要通過設置sensitiveHeaders即可,設置方法分為兩種:
1. 全局設置:
- zuul.sensitive-headers=
2. 指定路由設置:
- zuul.routes.
.sensitive-headers= - zuul.routes.
.custom-sensitive-headers=true
二、重定向問題
在使用Spring Cloud Zuul對接Web網站的時候,處理完了會話控制問題之后。往往我們還會碰到如下圖所示的問題,我們在瀏覽器中通過Zuul發起了登錄請求,該請求會被路由到某WebSite服務,該服務在完成了登錄處理之后,會進行重定向到某個主頁或歡迎頁面。此時,仔細的開發者會發現,在登錄完成之后,我們瀏覽器中URL的HOST部分發生的改變,該地址變成了具體WebSite服務的地址了。這就是在這一節,我們將分析和解決的重定向問題!
出現該問題的根源是Spring Cloud Zuul沒有正確的處理HTTP請求頭信息中的Host導致。在Brixton版本中,Spring Cloud Zuul的PreDecorationFilter過濾器實現時完全沒有考慮這一問題,它更多的定位于REST API的網關。所以如果要在Brixton版本中增加這一特性就相對較為復雜,不過好在Camden版本之后,Spring Cloud Netflix 1.2.x版本的Zuul增強了該功能,我們只需要通過配置屬性zuul.add-host-header=true就能讓原本有問題的重定向操作得到正確的處理。關于更多Host頭信息的處理,讀者可以參考本文之前的分析思路,可以通過查看PreDecorationFilter過濾器的源碼來詳細更多實現細節。
【本文為51CTO專欄作者“翟永超”的原創稿件,轉載請通過51CTO聯系作者獲取授權】