道與術

踩刀詩人發表於2022-01-26

 感謝HeapDump社群送的新年禮物

 

通過一個小案例,聊聊不同角色對於同一個問題的不同見地,立場不同,思考方式不同,答案自然不同,對於決策者,如何更好的“以道御術”。

 


問題背景

有個新部署的專案,客戶要使用https,希望我們支援一下,我心想這個在nginx端配置就可以,後臺程式碼不需要任何改造,立馬就答應了。

 

第二天,客戶將https證照提供過來,我協調運維2分鐘配置完成,似乎很美好,穩妥起見我還是決定先測試一下。

1.訪問https開頭的首頁可以正常開啟;

2.接著進行登入,點選登入按鈕以後頁面沒反應;

3.F12開啟控制檯,重複步驟2,可以看到登入正常,但是登入以後返回的重定向地址請求超時,如下圖所示;

長時間pending狀態

 最終連線超時 ERR_CONNECTION_TIMED_OUT

 

 

 

 


初步分析

其實開啟控制檯那一刻原因就很明確了,重定向返回的地址scheme變成了http,形如http://www.weixin.com,接著瀏覽器根據www.weixin.com:80建立連線,而www.weixin.com這個域名所在nginx並沒有開啟80埠,最終連線超時,瀏覽器就會報ERR_CONNECTION_TIMED_OUT。

 

為什麼成了http呢?

後端程式返回重定向地址時會呼叫javax.servlet.ServletRequest物件的getScheme方法獲取scheme然後拼接形成最終的重定向地址,比如req.getScheme()+"://"+uri。

getScheme這是一個介面方法,具體的實現由Servlet容器實現,比如Tomcat,Jetty等。

/**
 * Defines an object to provide client request information to a servlet. The servlet container creates a
 * <code>ServletRequest</code> object and passes it as an argument to the servlet's <code>service</code> method.
 *
 * <p>
 * A <code>ServletRequest</code> object provides data including parameter name and values, attributes, and an input
 * stream. Interfaces that extend <code>ServletRequest</code> can provide additional protocol-specific data (for
 * example, HTTP data is provided by {@link javax.servlet.http.HttpServletRequest}.
 * 
 * @author Various
 *
 * @see javax.servlet.http.HttpServletRequest
 *
 */
public interface ServletRequest {

 /**
   * Returns the name of the scheme used to make this request, for example, <code>http</code>, <code>https</code>, or
   * <code>ftp</code>. Different schemes have different rules for constructing URLs, as noted in RFC 1738.
   *
   * @return a <code>String</code> containing the name of the scheme used to make this request
   */
  public String getScheme();
 
 }

  至於Servlet容器背後的實現細節在此不做深究,最直觀的答案是req.getSheme取到的sheme不對。

 


初步解決

當我跟運維同學說了這個情況之後他立馬就給出了兩種解決方案,我們依次來看一下。

方案一:在nginx側增加響應頭 Strict-Transport-Security

add_header Strict-Transport-Security "max-age=86400";

看下rfc文件對於它的描述:

HTTP Strict Transport Security (HSTS)

Abstract

   This specification defines a mechanism enabling web sites to declare
   themselves accessible only via secure connections and/or for users to
   be able to direct their user agent(s) to interact with given sites
   only over secure connections.  This overall policy is referred to as
   HTTP Strict Transport Security (HSTS).  The policy is declared by web
   sites via the Strict-Transport-Security HTTP response header field
   and/or by other means, such as user agent configuration, for example.

  https://datatracker.ietf.org/doc/rfc6797/

 

HTTP Strict-Transport-Security響應標頭(通常縮寫為HSTS)通知瀏覽器該站點只能使用 HTTPS 訪問,並且將來使用 HTTP 訪問它的任何嘗試都應自動轉換為 HTTPS。

簡單來講就是告訴瀏覽器我的網站只支援https,下次遇到使用者訪問我的網站時強制使用https訪問。

看看效果如何

 

 

 瀏覽器對http的請求重新做了一次307 Internal Redirect,將scheme變成了https,對照下面流程圖幫助理解。

 

 

 看起來似乎很完美,難道真就無懈可擊了嗎,當然不是,背後是通過HSTS協議+瀏覽器配合完成,所以這個方案有一個繞不過去的問題就是瀏覽器相容性,看下developer.mozilla.org對於瀏覽器相容性的統計。

https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security

 

這麼一來,這個方案的普適性還是不夠強,需要探索更通用的方案。

 

方案二:使用nginx proxy_redirect http:// https://

proxy_redirect http:// https://

看下nginx官方文件的描述:

Syntax:  proxy_redirect default;
proxy_redirect off;
proxy_redirect redirect replacement;
Default:  
proxy_redirect default;
Context:  http, server, location

Sets the text that should be changed in the “Location” and “Refresh” header fields of a proxied server response

簡單來講就是告訴nginx在返回響應時,如果響應頭中攜帶了Location或者Refresh,就根據指定的規則對URL進行改寫。

比如上面提到的proxy_redirect http:// https://,會在返回響應時將URL中的http替換為https,從而解決我們遇到的問題,同樣貼個圖幫助理解。

 

 

 

這個方案沒有前面提到的瀏覽器相容性問題,但是存在場景相容性問題,怎麼講呢?proxy_redirect只適用於原生重定向這種場景,如果是非原生重定向場景這個指令其實是沒有功效的,接下來舉個我接觸過的真實案例。

 

筆者之前參與過異地雙活業務,簡單來講就是將使用者流量劃分到兩個不同的機房來分擔壓力、容災,流量劃分原則是通過使用者id取模,比如userid%2,這裡面就牽扯到一個域名下發的問題,請看下圖(為了保持簡單,邏輯有刪減):

 

 

 上圖中的2.2會在響應中寫入該使用者的歸屬域名,形如:

{
"success":false,
"code":"cross_domain"
 "target_domain":"B.xxx.com"
}

  

裝置端會在接收響應時判斷是否需要切換域名重新登入,判斷的依據就是根據響應報文中的success和code,最終的新域名是響應報文中的target_domain欄位,這個場景中也是實現了重定向的效果,但是卻沒有用http中301+location那種方式(千萬別問為什麼不用301+location),所以proxy_redirect自然也就用不上了。

 

那怎麼辦呢,接下來看我的終極解決辦法。

 


終極解決

其實這個問題的始作俑者還是後端服務獲取不到真實的scheme導致,所以終極解決辦法就是糾正源頭,至於源頭為什麼不對呢,相信大夥都猜到了,因為請求鏈路上多了一層反向代理導致,隱藏了真實的請求資訊,比如ip、scheme等,為了方便理解依然貼個圖:

  

 

 

 

瀏覽器發出的原始請求是https://weixin.com:443,nginx作為反向代理,內部將請求轉發到http://192.168.1.1/2:8001,這個過程中scheme由https變成了http,埠由443變成了8081,所以處在nginx後方的業務服務想獲取最原始的請求資訊似乎不可能。

 

現實當然是沒有那麼糟糕,反向代理並不是不講武德的流氓軟體(前段時間通過一個非官方網站下載軟體,軟體倒是安裝上了,但是電腦多了幾十個垃圾軟體),接下來簡單瞭解下反向代理的武德是什麼。

 

rfc文件專門針對這種經過代理導致原始資訊丟失的情況增加了相應的擴充套件頭,比如常見的X-Forwarded-For(獲取原始請求IP)、X-Forwarded-Proto(獲取原始請求協議)等,在反向代理軟體中也已經得到支援,所以只要正確的配置反向代理,將擴充套件頭往後傳遞,處於它後方的Servlet容器就可以正確識別相應資訊。

https://datatracker.ietf.org/doc/html/rfc7239#section-5.3

Forwarded HTTP Extension

Abstract

   This document defines an HTTP extension header field that allows
   proxy components to disclose information lost in the proxying
   process, for example, the originating IP address of a request or IP
   address of the proxy on the user-agent-facing interface.  In a path
   of proxying components, this makes it possible to arrange it so that
   each subsequent component will have access to, for example, all IP
   addresses used in the chain of proxied HTTP requests.

   This document also specifies guidelines for a proxy administrator to
   anonymize the origin of a request.

  

具體到這個案例中分兩步:

1.nginx中增加X-Forwarded-Proto

proxy_set_header X-Forwarded-Proto $scheme;

2.Servert容器中開啟Forward協議的解析器,以Jetty為例

class ForwardHeadersCustomizer implements JettyServerCustomizer {

  @Override
  public void customize(Server server) {
    ForwardedRequestCustomizer customizer = new ForwardedRequestCustomizer();
    for (Connector connector : server.getConnectors()) {
      for (ConnectionFactory connectionFactory : connector.getConnectionFactories()) {
        if (connectionFactory instanceof HttpConfiguration.ConnectionFactory) {
          ((HttpConfiguration.ConnectionFactory) connectionFactory).getHttpConfiguration()
              .addCustomizer(customizer);
        }
      }
    }
  }

}

  

經過這兩步的調整,資料的源頭已經被糾正,對比之前兩個方案它的通用性更強,沒有瀏覽器相容性問題,沒有使用場景的限制。

 

 


 道法術

道法術出自老子《道德經》,道,是規則、自然法則,上乘。法,是方法、法理,中乘。術,是行為、方式,下乘。“以道御術”即以道義來承載智術,悟道比修煉法術更高一籌。“術”要符合“法”,“法”要基於“道”,道法術三者兼備才能做出最好的策略。

    以上內容摘自百度百科

 

回顧前面的三種“術”,都是具體的方法、行為,它們都遵循的道是“返回正確的URL給瀏覽器”,我們在思考問題時應該以誰為出發點呢,這個問題沒有標準答案,簡單說說我的拙見吧。

 

如果一開始就陷入“術”,也就是追求具體的解決辦法,會讓思路變得狹窄,在本次案例中,起初我的關注點一直在糾結為什麼業務服務拿到的sheme是不正確的,所以眼光只是看到了糾正源頭這個點,過多的關注細節,或許這是開發人員的固有思維?這種固有思維容易讓我陷入死衚衕,需要適當的跳出來,站高一線的思考問題,這樣才能收穫更多的解法,充分對比,找到更優。

 

運維同學提供的兩種方案確實讓我開了眼,他把後端應用完全當成一個黑盒,輸出不對那就藉助外力再修改一次,只要結果是正確的就行,似乎有點“以道御術”的味道了。

 

講一個我遇到的例子,在我剛參加工作沒幾天的時候,研發經理給我分配了一個疑難bug讓我處理,我當時就蒙了,簡單的還沒弄明白呢,怎麼一上來就玩高難度了,研發經理跟我說:“不要有壓力,之所以讓你試試是因為你對這個問題一無所知,不會有固化思維,不用考慮太多歷史包袱,你可以用一切辦法去解決它。“

最終結果確實是好的,當然中途也被pass過好多方案,事後反觀最終方案,確實沒什麼難度,只是其他人沒想到。

 

所以結果是什麼呢?哈哈,自己悟吧。

 


推薦閱讀

rfc Strict-Transport-Security

Strict-Transport-Security Browser Compatibility

nginx proxy_redirect

rfc Forwarded HTTP

 

 

相關文章