這篇筆記要記的是,在寫 WCF 4 用戶端程式來呼叫某個第三方 Java web service 時碰到的一些狀況與問題排除過程。
已知線索:
底下是呼叫該 web service 的 getFoo 方法時,SoapUI 產生的 SOAP 訊息:
一開始,我先寫個 Windows Forms 程式來做個簡單測試,使用 WSHttpBinding,結果伺服器總是傳回 HTTP 500 錯誤。在嘗試錯誤的過程中,我寫了一個方法,直接用 HttpClient 把 SOAP 內容透過 HTTP POST 的方式送給 server,並取得回應結果。程式碼如下:
當我將 SoapUI 產生的 SOAP 內容貼到 TestSoap.xml 檔案中,然後執行上述方法時,可順利呼叫 web service 方法並取得回傳結果。但如果用 WCF 類別來呼叫 web service 就只得到 HTTP 500 錯誤,例如:WCF Security processor was unable to find a security header in the message。
於是土法煉鋼:用 Fiddler 攔截用戶端發出的 HTTPS 請求(是的,Fiddler 可以解開 HTTPS 加密封包),取得 WCF 產生的 SOAP 訊息,然後與 SoapUI 產生的 SOAP 訊息比對差異,並逐一手動修改 WCF 產生的 SOAP 訊息,餵給上述方法測試,找出問題癥結。
我的主要錯誤是沒注意到那個 Java web service 其實是 SOAP 1.1 的規格,不是 SOAP 1.2。我一開始看到 SoapUI 對 WSDL 解析的結果是 SOAP 1.2(如下圖),便不疑有他(畢竟 SoapUI 可順利呼叫 web service)。
然而經過比對 SoapUI 發出的封包與 WCF 程式發出的封包內容,發現它們的 SOAP envelope 使用的 namespace 不一樣:
BasicHttpBinding, WSHttpBinding, CustomBinding
WCF 的 WSHttpBinding 內定採用 SOAP 1.2,所以產生出來的 SOAP 訊息會固定使用 SOAP 1.2 的 envelope namespace。BasicHttpBinding 則是使用 SOAP 1.1,所以改用 BasicHttpBinding 應該就行了。
底下範例是不使用組態檔,完全以程式碼的方式來呼叫 web service:
如此一來,SOAP envelope 的 namespace 就正確了。但又有個怪現象,即 Fiddler 顯示伺服器端傳回 HTTP 200 OK,而且 response body 的確有傳回正確的結果,但應用程式卻拋出 MessageSecurityException:
System.ServiceModel.Security.MessageSecurityException: Security processor was unable to find a security header in the message. This might be because the message is an unsecured fault or because there is a binding mismatch between the communicating parties. This can occur if the service is configured for security and the client is not using security.
也就是說,伺服器端沒問題,反而是 WCF 對 response 的內容有意見了。
Timestamp
Rick Strahl 指出,原因在於 WCF 預期回傳的 response header 裡面也要有時間戳記,若沒有發現時間戳記,就會拋出例外。可是對方是 Java web service,又不是 WCF service,header 裡面自然不一定有時間戳記。
解決方法是改用 CustomBinding,並將 SecurityBindingElement 的 IncludeTimestamp 屬性設為 false 便大功告成。
修改後的程式碼如下(同樣不使用組態檔):
Happy coding!
參考資料
已知線索:
- 欲呼叫的 Java web service 無公開的 WSDL 網址,但有提供一個 WSDL 檔案。
- 須使用 HTTPS 加密協定。
- SOAP header 裡面要指定 username 和 password。
- 已經用 SoapUI 測試過,確定可以成功呼叫 web service。(SoapUI 是好物!)
底下是呼叫該 web service 的 getFoo 方法時,SoapUI 產生的 SOAP 訊息:
<soapenv:Envelope xmlns:wsa="http://www.w3.org/2005/08/addressing" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ejb="http://ejb.services.qoo"> <soapenv:Header xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"> <wsse:Security> <wsse:UsernameToken Id="http://xmethods.net/xspace"> <wsse:Username>Michael</wsse:Username> <wsse:Password>guesswhat</wsse:Password> </wsse:UsernameToken> </wsse:Security> <wsa:To>https://target.service.company.com:123/qoo/QooServices</wsa:To> <wsa:Action>getFoo</wsa:Action> <wsa:MessageID>uuid:9a6DA6FF-011C-4000-E000-5E3A0A244C97</wsa:MessageID> </soapenv:Header> <soapenv:Body> <ejb:getFoo> <getFooRequest> <fooId>XYZ</fooId> </getFooRequest> </ejb:getFoo> </soapenv:Body> </soapenv:Envelope>
一開始,我先寫個 Windows Forms 程式來做個簡單測試,使用 WSHttpBinding,結果伺服器總是傳回 HTTP 500 錯誤。在嘗試錯誤的過程中,我寫了一個方法,直接用 HttpClient 把 SOAP 內容透過 HTTP POST 的方式送給 server,並取得回應結果。程式碼如下:
private void btnPostSoap_Click(object sender, EventArgs e) { string fname = Path.GetDirectoryName(Application.ExecutablePath) + @"\SoapSamples\TestSoap.xml"; string soap = File.ReadAllText(fname); HttpContent content = new StringContent(soap); HttpClient client = new HttpClient(); string endPoint = "https://target.service.company.com:123/qoo/QooServices"; client.PostAsync(endPoint, content).ContinueWith(task => { task.Result.Content.ReadAsStringAsync().ContinueWith(t => { var aDelegate = new Action<lstring>(UpdateUI); txtResult.Invoke(aDelegate, t.Result); }); }); } private void UpdateUI(string result) { txtResult.Text = result; }
當我將 SoapUI 產生的 SOAP 內容貼到 TestSoap.xml 檔案中,然後執行上述方法時,可順利呼叫 web service 方法並取得回傳結果。但如果用 WCF 類別來呼叫 web service 就只得到 HTTP 500 錯誤,例如:WCF Security processor was unable to find a security header in the message。
於是土法煉鋼:用 Fiddler 攔截用戶端發出的 HTTPS 請求(是的,Fiddler 可以解開 HTTPS 加密封包),取得 WCF 產生的 SOAP 訊息,然後與 SoapUI 產生的 SOAP 訊息比對差異,並逐一手動修改 WCF 產生的 SOAP 訊息,餵給上述方法測試,找出問題癥結。
註:要取得 SOAP 封包內容,除了 Fiddler,也可以用 WCF 的訊息記錄(message logging)功能:Configuring Message Logging。只是開啟這功能得在組態檔裡面加一堆東西,若只是臨時想要查看封包內容,Fiddler 還是方便得多。SOAP 1.1 vs 1.2
我的主要錯誤是沒注意到那個 Java web service 其實是 SOAP 1.1 的規格,不是 SOAP 1.2。我一開始看到 SoapUI 對 WSDL 解析的結果是 SOAP 1.2(如下圖),便不疑有他(畢竟 SoapUI 可順利呼叫 web service)。
然而經過比對 SoapUI 發出的封包與 WCF 程式發出的封包內容,發現它們的 SOAP envelope 使用的 namespace 不一樣:
- SoapUI 封包:xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" ==> 這是 SOAP 1.1
- WCF 程式封包: xmlns:s="http://www.w3.org/2003/05/soap-envelope" ==> 這是 SOAP 1.2
BasicHttpBinding, WSHttpBinding, CustomBinding
WCF 的 WSHttpBinding 內定採用 SOAP 1.2,所以產生出來的 SOAP 訊息會固定使用 SOAP 1.2 的 envelope namespace。BasicHttpBinding 則是使用 SOAP 1.1,所以改用 BasicHttpBinding 應該就行了。
底下範例是不使用組態檔,完全以程式碼的方式來呼叫 web service:
void CallServicesByCode() { string url = "https://target.service.company.com:123/qoo/QooServices"; var endPoint = new EndpointAddress(url); var binding = new BasicHttpBinding(BasicHttpSecurityMode.TransportWithMessageCredential); binding.Security.Message.ClientCredentialType = BasicHttpMessageCredentialType.UserName; binding.Security.Transport.ClientCredentialType = HttpClientCredentialType.None; var client = new Qoo.QooServicesClient(binding, endPoint); client.ClientCredentials.UserName.UserName = "Michael"; client.ClientCredentials.UserName.Password = "guesswhat"; var req = new Qoo.GetFooRequest(); req.fooId = "1234"; var resp = client.getFoo(req); txtResult = resp.fooName; }
註:由於對方的 web service 要求使用 HTTPS 加密傳輸,而且每一個 SOAP 訊息封包都必須包含 username 和 password,所以程式在設定 WCF 傳輸安全性時,是使用 TransportWithMessageCredential 模式。
如此一來,SOAP envelope 的 namespace 就正確了。但又有個怪現象,即 Fiddler 顯示伺服器端傳回 HTTP 200 OK,而且 response body 的確有傳回正確的結果,但應用程式卻拋出 MessageSecurityException:
System.ServiceModel.Security.MessageSecurityException: Security processor was unable to find a security header in the message. This might be because the message is an unsecured fault or because there is a binding mismatch between the communicating parties. This can occur if the service is configured for security and the client is not using security.
也就是說,伺服器端沒問題,反而是 WCF 對 response 的內容有意見了。
Timestamp
Rick Strahl 指出,原因在於 WCF 預期回傳的 response header 裡面也要有時間戳記,若沒有發現時間戳記,就會拋出例外。可是對方是 Java web service,又不是 WCF service,header 裡面自然不一定有時間戳記。
補充說明 1:呼叫此 web service 時,WCF 產生的 request header 裡面有時間戳記,試過有無皆可--對此 web service 而言。
補充說明 2:當 WCF binding 使用 message-layer security 時,時間戳記會被自動加入 SOAP envelope,以確保訊息傳遞的時效,避免訊息重發(message replay)攻擊。
解決方法是改用 CustomBinding,並將 SecurityBindingElement 的 IncludeTimestamp 屬性設為 false 便大功告成。
修改後的程式碼如下(同樣不使用組態檔):
void CallServicesByCode() { string url = "https://target.service.company.com:123/qoo/QooServices"; var endPoint = new EndpointAddress(url); var binding = new BasicHttpBinding(BasicHttpSecurityMode.TransportWithMessageCredential); binding.Security.Message.ClientCredentialType = BasicHttpMessageCredentialType.UserName; binding.Security.Transport.ClientCredentialType = HttpClientCredentialType.None; // 利用既有的 BasicHttpBinding 物件來建立 binding 元素集合,然後修改集合中的元素 var bindingElements = binding.CreateBindingElements(); var secbe = bindingElements.Find<System.ServiceModel.Channels.SecurityBindingElement>(); secbe.IncludeTimestamp = false; secbe.LocalServiceSettings.DetectReplays = false; secbe.LocalClientSettings.DetectReplays = false; // 建立 CustomBinding 物件,使用剛才的 binding 元素集合. var customBinding = new System.ServiceModel.Channels.CustomBinding(bindingElements); var client = new Qoo.QooServicesClient(customBinding, endPoint); // 這裡改用 customBinding 物件. client.ClientCredentials.UserName.UserName = "Michael"; client.ClientCredentials.UserName.Password = "guesswhat"; var req = new Qoo.GetFooRequest(); req.fooId = "1234"; var resp = client.getFoo(req); txtResult = resp.fooName; }
Happy coding!
參考資料
- Tracing WCF Messages by Rick Strahl
- Microsoft WCF 4.0 Cookbook for Developing SOA Applications
- Can a WSDL indicate the SOAP version (1.1 or 1.2) of the web service?
延伸閱讀
您好 我有一些Fiddler Script語法問題
回覆刪除不知道可否請教您?
這個我不熟耶,不好意思...
回覆刪除您好,
回覆刪除拜讀大作後,深感佩服!
小弟為此已經跟JAVA SERVER端攻防月餘,
請問大大是否有反方向的經驗?
也就是我們利用JAVA PROGRAMMER提供的WSDL/XSD檔案製作WS給JAVA SERVER使用的方法,謝謝。
使用 Java 端提供的 WSDL 檔案製作給對方用的 Web service 我沒有試過,最近倒是有試過對方提供一個 .NET 2.0 web service 的 WSDL,要我寫一個一模一樣的給另一個 ASP.NET 2.0 web app 使用。剛開始用 WCF service,發現對方無法順利呼叫,還得跟組態檔奮戰,太麻煩了,後來乾脆直接寫成傳統 web service,因為對方不是 .NET 3.5/WCF,所以用傳統 .NET 2.0 web service 很快就順利接上。參考看看囉!
回覆刪除您好,
回覆刪除我現在做的方案架構類似您文章 "攔截 WCF 服務往返的 SOAP 訊息 " ,
'http://huan-lin.blogspot.com/2013/03/intercepting-wcf-soap-messages.html'內第一張圖示,由Client App傳值給MyWebApp到DB抓資料組成XML給Third-party,Third-party會回覆結果給我,但是現在問題是Thirt-party要求我用他們給的憑證作WS-Security加密。
當我trace用aspx傳值給svc,再由svc request給Third-party時,我一直得到500伺服器內部錯誤,trace時則是得到"Incoming message does not contain required Security header",我有按照您這篇文章檢查我的程式,但是還是一樣,請問是否我哪裡遺漏了呢?
謝謝!
Hi Captain,
回覆刪除你有把 SOAP 封包攔下來查看裡面的內容嗎?SOAP 內容是否包含 third-party 要求的 security header?
你說「Thirt-party要求我用他們給的憑證作WS-Security加密」,這個部份我不太懂:這會影響程式的寫法嗎?
我覺得你也可以把問題張貼到 MSDN 論壇,讓多隻眼睛幫你一起看。畢竟留言板不適合貼程式碼和圖片,不容易把完整的問題情境描述清楚。
老師 您好,
回覆刪除「Thirt-party要求我用他們給的憑證作WS-Security加密」的意思是,Third-party給我一個.jdk的憑證,要求我使用此憑證內的三種鑰匙進行SOAP/XML的cert及encrypt。
我有在MSDN 提出問題,但是無法得到解答,
網址為 "http://social.msdn.microsoft.com/Forums/vstudio/en-US/53d68de3-8f15-4cf1-ae50-5fa020701a69/unable-connect-svc-in-iis60"
之前由於將aspx與svc放在同一專案,且使用VS ASP.NET進行瀏覽可以成功,我用FIDDLER抓到Header有將訊息傳送給Third-party,但是當我將aspx及svc放上IIS時,改了endpoint至server上的svc連結後,卻一直收到錯誤訊息,不知道是哪裡出了問題。
若老師需要更多的資訊,是否方便給我email,我傳訊息給老師呢?
謝謝!
老師 您好,
回覆刪除更新測試的狀況,看起來是使用VS ASPNET進行DEBUG時,可以抓的到憑證,但是將svc放到IIS之後,卻無法抓到憑證,我也將該憑證的私鑰授權給IIS的匿名存取的使用者帳戶了,為什麼還是抓不到憑證呢?
打擾之處,敬請見諒,感恩!
謝謝!
Hi Captain,
回覆刪除所以你的 message body 還要利用第三方提供的 SSL 憑證來加密,是這個意思嗎?
看起來,你碰到的狀況比我先前碰到的還複雜一些,光憑目前我看到的線索,不太好推測原因。要是我這邊有一個可以重現問題的 sample project 來測試就好了。
不知道你有沒有看過這帖: http://stackoverflow.com/questions/14740369/wcf-soap-1-1-and-ws-security-1-0-client-certificate-transport-auth-service-cer
其問題描述似乎與你的狀況有幾分相似。
我的 email 是 huanlin.tsai@gmail.com。
老師 您好:
回覆刪除謝謝您的資訊,現在測試上最奇怪的問題是,使用VS執行就可以順利開啟,但是一旦將svc安裝至IIS上,使用aspx呼叫就不行....
我有寄一封email至您的信箱,裡面有較詳細的說明,打擾之處,敬請見諒,
謝謝!
老師 您好:
回覆刪除非常感謝您的提示,果然還是跟IIS的設定有關係,APP POOL的IDENTITY一改為LOCAL SYSTEM後,就可以連線成功了!
但是我有另一個Third-party給我的WSDL,是要我開發Service端程式,使用加入服務參考後,Reference.vb卻沒有資料,我有再把WSDL寄給您了,煩請指導。
感謝!
已回信。簡單地說,你那 WSDL 檔案因為某些語法格式的問題,SvcUtil 無法正確處理,恐怕只能使用 Add Web Reference 的方式來產生 proxy class 了。
回覆刪除老師 您好
回覆刪除SOAP header 裡面除了在 Security 中指定 username 和 password 外,另一種是使用 憑證 的方式處理,像是要指定 BinarySecurityToken、Signature ... 等,不知老師您這邊是否曾經用過呢?這邊卡了好幾個月了 Orz
希望您能不吝指導一下方向
這種方式我就沒用過了,能夠提供的資訊恐怕很有限。所以還是建議把問題的來龍去脈貼在公共論壇,例如 MSDN 或 stackoverflow.com,比較容易獲得解答。
回覆刪除