2012年3月13日 星期二

Java call WCF service over SSL with UserName Credentials using NetBeans

過程中,主要有幾個問題:
一、WCF bindings設定的影響
二、SSL資料傳輸在Java環境中的設定
三、JDK版本的影響
四、Java FQDN造成的問題
五、NetBeans使用Web Service的方法和修改部份程式碼

一、WCF Binding設定
WCF服務繫結(binding)
  • BasicHttpBinding:一種 HTTP 通訊協定繫結,可用於連線至 Web 服務,並符合 WS-I Basic Profile 規格 (例如,以 ASP.NET Web 服務為基礎的服務)。
  • WSHttpBinding:一種互通的繫結,可用於連線至符合 WS-* 通訊協定的端點。
  • NetNamedPipeBinding:使用 .NET Framework 以連線至同一部電腦上的其他 WCF 端點。
  • NetMsmqBinding:使用 .NET Framework 以建立與其他 WCF 端點的佇列訊息連線。
所以要在Java環境下使用Web service時,WCF要在設定時要加入BasicHttpBinding, security mode 則是選擇TransportWithMessageCredential
注意:SecurityMode 設定為"TransportWithMessageCredential"時,會忽略transport項目的ClientCredentialType。

<security mode="TransportWithMessageCredential">
     < transport clientCredentialType="None" />
     < message clientCredentialType="UserName" />
</security>
二、解決windows環境下java的ssl資料存取問題
有兩種解決方法:
(一)、在程式中修正java keystore 使用SSL安全協定傳輸資料,在使用web service前,必需設定接受憑證政策。
Step1.匯入憑證(向senslink網站索取),利用java的keytool匯入(在jdk\bin\目錄下可找到),
輸入:keytool –keystore 匯入後的keystore存放位置 –alias 憑證匿名 –import –file 憑證位置 設定keystore密碼(後面會用到,要記住)
Step2.匯入成功後,可查詢已匯入的憑證(keytool –list –keystore keystore存放位置)
Step.3 設定授權政策
將以下程式片段加入main class

//.keystore位置
System.setProperty("javax.net.ssl.trustStore", "d:\\senslinkcert\\client.keystore");
System.setProperty("javax.net.ssl.keyStore", "d:\\senslinkcert\\client.keystore");

//.keystore密碼
System.setProperty("javax.net.ssl.trustStorePassword", "keystorePassword");
System.setProperty("javax.net.ssl.keyStorePassword", " keystorePassword ");


(二)、修正整個windows系統的keystore
開發時,需將信任憑證加入Java_Home\JDK1.xx.x\jre\bin環境中,因此以下指令皆需在此路徑下執行
Step1. 列出目前keystore裡的信任憑證
指令:keytool –list –keystore ..\lib\security\cacerts
系統預設密碼:changeit


Step2.加入信任憑證
●在瀏覽器中,將要加入信任的憑證匯出,然後匯入java keystore IE => 網際網路選項 => 內容 => 憑證,選擇憑證後,匯出DER編碼二位元X.509(.CER)
●將憑證匯入Java keystore
Ex: 匯入senslink憑證
指令:keytool –import –alias senslink –keystore ..\lib\security\cacerts -file d:\senslink\senslink.cer
系統預設密碼:changeit

●查詢是否匯入成功:
指令:keytool –list –alias senslink –keystore ..\lib\security\cacerts
系統預設密碼:changeit

三、在NetBeans建立新的專案
設定完上述的步驟後,在NetBeans建立新的Java Project
在Java環境下要可以使用parsing web service WSDL的定義語言,並產生相關的class,有不同的Library可以使用。
ex: Metro 2.0Apache Axis 2.0...等符合WS-Security標準的Library
本範例使用的是Metro 2.0
1.建立專案,並在Library加入Metro 2.0後,產生web service client

此時,可能會有一些錯誤
➀SSL交換訊息錯誤:
SSLException: HelloRequest followed by an unexpected handshake message
這個問題是在SSL連線時,無法解析Server端的訊息所產生的。
它是一個Java的安全性問題,可在這裡找到相關的訊息。

解決的方法:
將JDK更新到1.6.0 update 23以後的版本

②web service定義重複:
[ERROR] duplicate "message" entity: "IWcfRequestProcessor_ProcessRequests_InputMessage line 1 of https://xxxxxxxxxxxxxxxxxxxxxx/Service.svc?wsdl

解決的方法:
這個問題有可能是FQDN造成的錯誤,在網址使用domain的情況下,在Intranet內的電腦,有可能會有許多符合條件的路徑指向Service.svc,有可能是127.0.0.1找到一次,localhost又找到一次,所以會造成重複,但如果由Intranet外部使用網域名稱就可以解決這個問題

2.順利由WSDL產生服務相關的檔案後,
可利用NetBeans的Insert Code產生呼叫web service的程式片段,
在main class上點選右鍵,選擇Insert Code => Call web service operation =>選擇要使用的web service 方法


3.加入使用者認證的程式
利用WSBindingProvider.USERNAME_PROPERTY和WSBindingProvider.PASSWORD_PROPERTY加入認證的使用者名稱和密碼

import com.sun.xml.ws.developer.WSBindingProvider;
 

③執行程式呼叫web service後,可能會發生以下的錯誤:
javax.xml.ws.WebServiceException:
Cannot find 'https://xxxxxxxxxxxxxxxxxxxxxx/Service.svc?wsdl' wsdl. Place the resource correctly in the classpath.

解決方法:
修正service constructor程式片段
原本的程式:

static {
   SERVICE_WSDL_LOCATION = namespace.Service.class.getResource("https://xxxxxxxxxxxxxxxxxxxxxx/Service.svc?wsdl");
   WebServiceException e = null;
   if (SENSLINKSERVICE_WSDL_LOCATION == null) {
      e = new WebServiceException("Cannot find 'https://xxxxxxxxxxxxxxxxxxxxxx/Service.svc?wsdl' wsdl. Place the resource correctly in the classpath.");
   }
   SERVICE_EXCEPTION = e;
}
修正後:

static {
   URL url = null;
   WebServiceException e = null;
   try {
        URL baseUrl;
        baseUrl = namespace.Service.class.getResource(".");
        url = new URL(baseUrl, "https://xxxxxxxxxxxxxxxxxxxxxx/Service.svc?wsdl");
   } catch (java.net.MalformedURLException ex) {
      e = new WebServiceException("Cannot find 'https://xxxxxxxxxxxxxxxxxxxxxx/Service.svc?wsdl' wsdl. Place the resource correctly in the classpath.");
   }
   SERVICE_WSDL_LOCATION = url;
   SERVICE_EXCEPTION = e;
}
這樣一來,就可以正常的從Java client呼叫.net WCF Web Service

References:
1.WCF服務繫結詳細資料
2.Java jdk bug造成的SSL錯誤
3.Java FQDN造成的message entity duplicated錯誤
4.加入service後,卻在執行時找不到service路徑

2 則留言:

Unknown 提到...

kaochiuan,能否請教一些問題。
依著您所提供的資料﹐我試著練習﹐可是在最後執行卻出現了如下的錯誤
java.lang.ClassCastException: $Proxy41 cannot be cast to com.sun.xml.internal.ws.developer.WSBindingProvider
at web3.javaClient.main(javaClient.java:33)


第33行是 WSBindingProvider bp = (WSBindingProvider) port;

public static void main(String[] args) {
//.keystore位置
System.setProperty("javax.net.ssl.trustStore", "C:\\Java\\Certificate\\myTrustStore");
System.setProperty("javax.net.ssl.keyStore", "C:\\Java\\Certificate\\myTrustStore");

//.keystore密碼
System.setProperty("javax.net.ssl.trustStorePassword", "q2000s");
System.setProperty("javax.net.ssl.keyStorePassword", " q2000s");

org.tempuri.ProductsService service=null;
org.tempuri.IProductsService port=null;
try{
service = new org.tempuri.ProductsService();
port = service.getBasicHttpBindingIProductsService();

WSBindingProvider bp = (WSBindingProvider) port;
bp.getRequestContext().put(WSBindingProvider.USERNAME_PROPERTY, "testman");
bp.getRequestContext().put(WSBindingProvider.PASSWORD_PROPERTY, "testman123");

System.out.println(port.saySomething("Hello"));
}catch(Exception er){
er.printStackTrace();
}
}

請問這個錯誤知道是什麼原因嗎?煩請不吝指教﹐謝謝。

kaochiuan 提到...

不好意思,過這麼久才看見您的留言
我發現您的程式有一部份有問題
在這行程式中,
port = service.getBasicHttpBindingIProductsService();

port其實應該是要取得serviceEndPoint,不是直接使用Service,所以造成java在cast轉型時,發現型別不對丟出例外。