一個HTTP Basic Authentication引起的異常

這幾天在作一個功能,其實很簡單。就是調用幾個外部的API,返回數據後進行組裝而後成爲新的接口。其中一個API是一個很奇葩的API,雖然是基於HTTP的,但既沒有基於SOAP規範,也不是Restful風格的接口。還好使用它也沒有複雜的場景。只是構造出URL,發送一個HTTP的get請求,而後給我返回一個XML結構的數據。html

我使用了Spring MVC中的RestTemplate做爲客戶端,而後引入了Jackson-dataformat-xml做爲xml映射爲對象的工具庫。因爲集成外部API的事情已經作了不少次了,集成這個API也是輕車熟路,三下五除二就完成了。java

接下來爲了驗證連通性,我先在SoapUI裏配置了該外部API的某個測試環境,嘗試發送了一個Get請求,成功收到了Response。而後我把本身的程序運行起來,嘗試經過本身的程序調用該API,結果返回了HTTP 500錯誤,即「internal server error」。spring

這可奇了怪了。我第一反應是程序中對外部API的配置和SoapUI中的配置不同。我仔細對比了發送請求的URL,須要的HTTP header以及用做驗證的username和password都是徹底一致的。這個問題被排除。apache

接下來我想再仔細看看Response,可否找到什麼蛛絲馬跡。仔細查看了Response的header和body,發現header一切正常,body是個空的body,沒有提供任何的可用信息。服務器

而後我能想到的另外一個解決方案就是聯繫該外部API的團隊,讓他們幫忙看看我發送了請求以後,爲何服務器會返回500。但惋惜這是一個很老的服務了,找到該團隊的人而且排期幫我看log至少要花好幾天的時間了。並且既然SoapUI能調用成功,而應用程序卻調用不成功,問題多半仍是出在咱們這。app

接下來我想既然問題有可能出在咱們這,那麼確定是request有差別。因爲我發的是一個Get請求,沒有body實體,URL又徹底同樣,那麼問題極可能出在request的header上。這個API須要request中包含兩個自定義的header,而我在SoapUI以及本身的程序中都已經配置了。那問題會在哪裏哪?less

既然在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.


擴展閱讀:

相關文章
相關標籤/搜索