一個HTTP Basic Authentication引發的異常

黃博文發表於2018-02-02

這幾天在做一個功能,其實很簡單。就是呼叫幾個外部的API,返回資料後進行組裝然後成為新的介面。其中一個API是一個很奇葩的API,雖然是基於HTTP的,但既沒有基於SOAP規範,也不是Restful風格的介面。還好使用它也沒有複雜的場景。只是構造出URL,傳送一個HTTP的get請求,然後給我返回一個XML結構的資料。

我使用了Spring MVC中的RestTemplate作為客戶端,然後引入了Jackson-dataformat-xml作為xml對映為物件的工具庫。由於整合外部API的事情已經做了很多次了,整合這個API也是輕車熟路,三下五除二就完成了。

接下來為了驗證連通性,我先在SoapUI裡配置了該外部API的某個測試環境,嘗試傳送了一個Get請求,成功收到了Response。然後我把自己的程式執行起來,嘗試通過自己的程式呼叫該API,結果返回了HTTP 500錯誤,即“internal server error”。

這可奇了怪了。我第一反應是程式中對外部API的配置和SoapUI中的配置不一樣。我仔細對比了傳送請求的URL,需要的HTTP header以及用作驗證的username和password都是完全一致的。這個問題被排除。

接下來我想再仔細看看Response,能否找到什麼蛛絲馬跡。仔細檢視了Response的header和body,發現header一切正常,body是個空的body,沒有提供任何的可用資訊。

然後我能想到的另一個解決方案就是聯絡該外部API的團隊,讓他們幫忙看看我傳送了請求之後,為什麼伺服器會返回500。但可惜這是一個很老的服務了,找到該團隊的人並且排期幫我看log至少要花好幾天的時間了。而且既然SoapUI能呼叫成功,而應用程式卻呼叫不成功,問題多半還是出在我們這。

接下來我想既然問題有可能出在我們這,那麼肯定是request有差異。由於我發的是一個Get請求,沒有body實體,URL又完全一樣,那麼問題很可能出在request的header上。這個API需要request中包含兩個自定義的header,而我在SoapUI以及自己的程式中都已經配置了。那問題會在哪裡哪?

既然在SoapUI裡無法重現這個問題,我就使用了Chrome外掛版的POSTMAN,通過它配置了該API的呼叫。然後奇蹟出現了,我竟然在POSTMAN中重現了這個問題。當我看到在POSTMAN也返回了500 error後,我思考了5秒鐘,猜到了原因。問題很可能是出在了Authentication這個header上面。

要說這個問題,還要從HTTP的Basic Authentication說起。Basic Authentication是HTTP實現訪問控制的最簡單的一種技術。HTTP Client端會將使用者名稱和密碼組合後使用Base64加密,生成key為‘Authentication’,value為‘Basic BASE64CODE’的HTTP header,傳送給伺服器端以便進行Basic認證方式。

但這個經典的Basic Authentication是要經歷兩步的。第一步,客戶端傳送不帶Authentication header的HTTP請求,伺服器檢查後發現受訪的資源需要認證,就會返回HTTP Status 401,表示未授權,客戶端發現伺服器端返回401後,會再構造一個新的請求,這次包含了Authentication header,伺服器接收後驗證通過,返回資源。

那麼我在自己的應用程式和POSTMAN中呼叫返回500 internal server error的原因是當第一次給Server傳送不帶Authentication header的HTTP請求時,Server竟然返回了HTTP Status 500。其實它應該返回401,這樣HTTP Client會再發一個包含了Authentication的新請求。由於它返回了500,HTTP Client認為伺服器有問題,就停止處理了。

那為什麼在SoapUI中呼叫可以成功那?那是因為SoapUI使用的Http client在發第一次請求時就已經設定了Authentication header,所以就沒有問題。這樣可以避免重複發請求的現象。這種行為叫做‘preemptive authentication’(搶先驗證),在SoapUI中你可以選擇是否啟用該行為。具體可以參見How To Authenticate SOAP Requests in SoapUI

所以問題的根源在於該外部API在實現Basic Authentication時沒有完全遵循規範,這鍋我們不背

解決方案有兩種。第一種是讓該外部API遵循Basic Authentication的規範,如果請求未授權應該返回401而不是500。不過我說過這是一個很古老的API了,讓它們改要等到猴年馬月了。

第二種就是我的應用程式在給該外部API傳送請求時,第一次就設定Authentication header。我們用的是RestTemplate,而RestTemplate底層使用的是Apache Http Client 4.0+版本。要注入這個header很簡單,在例項化RestTemplate後,給其多加一個Intecepter。

1
2
restTemplate.getInterceptors().add(
  new BasicAuthorizationInterceptor("username", "password"));

加上這一行程式碼後,執行程式,順利的得到了Response,世界清靜了。

最後一個問題,為什麼Http Client當配置了使用者名稱和密碼後,不主動的啟用‘preemptive authentication’那?畢竟可以少發很多請求啊。這是Apache官方給出的原因:

HttpClient does not support preemptive authentication out of the box, because if misused or used incorrectly the preemptive authentication can lead to significant security issues, such as sending user credentials in clear text to an unauthorized third party. Therefore, users are expected to evaluate potential benefits of preemptive authentication versus security risks in the context of their specific application environment. Nonetheless one can configure HttpClient to authenticate preemptively by prepopulating the authentication data cache.


擴充套件閱讀:

相關文章