java调用WCF问题

过去微软.NET的ASMX Web Service已被大家广泛应用﹐但在信息安全日愈重视之下﹐微软有意以WCF取代原有的 ASMX Web Service。WCF 具有许多先进的技术﹐而跨平台作业已是现在不可避免的问题﹐同样是微软的 Solution之下如何使用WCF应该不是什么问题﹐但在不同的平台上是否有那么容易呢?因此这里以 Java 实作如何来调用具有使用身份验证的 WCF﹐并以WCF 预设的wsHttpBinding 及一般常使用的 basicHttpBinding 的系结方式实作。

我本身并非专研 Java﹐但既然日后使用了 WCF 也势必面临 Java 或其它平台的呼叫﹐Java 是Open Source 具有多种 Framework﹐且有多种开发工具﹐参考了网络上许多范例与讨论﹐Java 对于WCF比较常用的是 ASIX 和 Metro 套件﹐因此这里主要使用 NetBeans IDE 搭配 Metro﹐eclipse 搭配asix 这两种﹐不过因为不是专研 Java﹐故仍有些地方不是实作的很完全﹐这里就抛砖引玉﹐期待高手来解惑了。

 

这里使用的 NetBeans 版本为 NetBeans IDE 7.2 (Build 201207301726)﹐JDK 是1.6.0_37。NetBeans可至Oracle官网(http://netbeans.org/downloads/index.html)下载。

在 Viusal Studio 上建立WCF 项目时﹐预设所产生的就是使用 wsHttpBinding 的系结。wsHttpBinding 预设的安全性模式为 Message(讯息模式加密)﹐在 WCF 中也是普遍的被使用。当我想以 Java 调用时却发现在网络上有许多人询问﹐但很少看到一个完整的范例。同时所看到的讨论回复都很片断﹐因此实作过程并不容易。在这里分别以 Java Application 与 Web应用程序当Client程序调用WCF为范例。

这一篇是搭配使用者认证的WCF服务—wsHttpBinding系结所建置的WCF 服务。

1. 自订使用者账号/密码﹐Java Application client 不以Glassfish为container

1.1. 汇入凭证档

因为WCF自订使用者账号/密码认证是需要X.509凭证﹐因此在开始之前必须先取得凭证放置到Java可以读取的位置。

这里的凭证继续延用WCF自定义用户账号密码之使用者认证的WCF服务wsHttpBinding总结这一篇中的凭证﹐其凭证主体为 MyWCFCert。首先使用windows凭证管理将之前制作WCF时所制作的凭证先汇出。

 java调用WCF问题_第1张图片
 

将汇出的凭证档档名命名为 MyWCFCert.cer﹐接着使用 JDK 所提供的工具 keytool 指令建立放置凭证的 keystore 或汇入已存在的 keystore(金钥库)。Keytool.exe是java的凭证管理工具。

指令:

把一个凭证档导入到指定的keystore www.it165.net

keytool -import -file MyWCFCert.cer -keystore my.TrustStore -alias wcfsvrkey

用-keystore 参数指定 keystore 档案﹐my.TrustStore 是我自己建立的 keystore﹐如果不存在﹐会自动建立同时会询问keystore的密码。

-alias则是为这个汇入的凭证建立一个别名。

 

汇入成功后应该做一下检查﹐同样使用keytool指令

指令:

列出keystore中的内容信息

keytool -list -v -keystore my.TrustStore

执行之后可以检视这个keystore所有的凭证档。

 

java调用WCF问题_第2张图片
 

如果有删除凭证的需要时同样使用keytool

指令:

删除指定的keystore中的凭证

keytool -delete -alias wcfsvrkey -keystore my.TrustStore

 

1.2. 下载 METRO 2.2.1

我由Oracle 官网下载使用的NetBeans IDE是7.2 版﹐内附 METRO 是 2.0 版﹐对于要使用具有使用者账号密码认证的WCF服务在国外论坛上有不少﹐有许多人都说必须将Metro更新到2.0版才行﹐不过经过我实测后发现必须使用 2.2.1版才行。

Metro 2.2.1 版可至 http://metro.java.net/2.2.1/ 此处下载。将下载的metro-standalone-2.2.1.zip档案解压至自订的路径之下﹐例如以我个人放置到D:\Java\metro-2_2_1。然后开启NetBeans﹐到工具/链接库将 METRO 2.2.1 加入链接库。

 

java调用WCF问题_第3张图片

在链接库管理器中﹐按下[加入JAR/数据夹]指向Metor 2.2.1的位置﹐之后在NetBeans 中就可以直接选择了。

 

java调用WCF问题_第4张图片
 

1.3. 建立项目

开启NetBeans新增项目﹐左侧选择Java﹐右侧选择Java Application﹐[下一步]继续。

 

java调用WCF问题_第5张图片
 

输入项目名称﹑项目位置﹑Class文件等各项信息﹐按下[完成]。

 

java调用WCF问题_第6张图片
 

专案名称:wsHttpClient

Class名称:JavaClient

 

1.4. 加入 Web Service Client

项目建立后﹐在该项目的名称上以鼠标右键选择 New/Web Service Client。

 

java调用WCF问题_第7张图片
 

完成后回到NetBeans IDE接口等一下﹐NetBeans正在产生WSDL档及自动产生一些相关的设定档和程序代码。NetBeans跑完后大约如下图。

 

java调用WCF问题_第8张图片
 

1.5. 档案转换为 UT-8 格式

检视项目之下有个Generated Source﹐将其展开后会如下﹐在Generated Source是NetBeans依WSDL所自动产生的class檔。

 

java调用WCF问题_第9张图片
 

点选开启 GetProduct.java﹐画面会出现警告﹐这是因为NetBeans自动Generated的档案为ANSI格式﹐但这个档案中有中文﹐而NetBeans要读取的档案为UTF-8﹐故这里NetBeans跳出了警告。

 

java调用WCF问题_第10张图片
 

因为档案带有中文字﹐因此必须要先将档案改存为UTF-8才行﹐不然后续做 Builde Project 时是会出错的。可以用记事本开启档案﹐再以另存新档方式变更格式再回存。不过因为档案不只一个﹐这样改比较慢﹐可以去网络找一个老牌的工具 ConverZ 做批次转换。 www.it165.net

 

1.6. 编辑 Web 服务属性

接着在Web服务参照下的MyProducts按下鼠标右键﹐选择[编辑Web服务属性]。

 

java调用WCF问题_第11张图片
 

开出的是Web服务属性的画面﹐这里我们只要设定Quality Of Service页签下关于安全的部分。

 

java调用WCF问题_第12张图片
 

在编辑Web服务属性的设定画面上有一个[使用开发默认值]的复选框﹐点击一下﹐画面会跳出一些讯息。

 

java调用WCF问题_第13张图片
 

这个讯息是询问我们,是否使用METRO Library﹐如果要使用则会移除JAX-WS library﹐因为JAX-WS已被包含于METRO之中。按下Yes则NetBeans会帮我们项目加上METRO。如下图﹐回到Project中展开链接库﹐就可以看到METRO 2.0已被加入。

 

java调用WCF问题_第14张图片
 

再回到编辑Web服务属性设定画面﹐刚刚所点击的[使用开发默认值]的复选框如果已经有被勾选了﹐请将勾选取消。然后先离开编辑Web服务属性设定画面。

 

1.7. 加入CallbackHandler 档案

这里需要加入一个继承CallbackHandler的档案

TrustStoreCallbackHandler.java

 

view source
print ?
01. public class TrustStoreCallbackHandler implements CallbackHandler {
02. KeyStore keyStore = null
03. String password = "123456";  // keystore的密码
04. public TrustStoreCallbackHandler() { 
05. System.out.println("Truststore CBH.CTOR Called.........."); 
06. InputStream is null
07. try 
08. keyStore = KeyStore.getInstance("JKS"); 
09. String keystoreURL = "C:\\Java\\Certificate\\myTrustStore"//放置凭证档的keystore
10. is new FileInputStream(keystoreURL); 
11. keyStore.load(is, password.toCharArray()); 
12. catch (IOException ex) { 
13. Logger.getLogger(TrustStoreCallbackHandler.class.getName()).log(Level.SEVERE,null, ex); 
14. throw new RuntimeException(ex); 
15. catch (NoSuchAlgorithmException ex) { 
16. Logger.getLogger(TrustStoreCallbackHandler.class.getName()).log(Level.SEVERE,null, ex); 
17. throw new RuntimeException(ex); 
18. catch (CertificateException ex) { 
19. Logger.getLogger(TrustStoreCallbackHandler.class.getName()).log(Level.SEVERE,null, ex); 
20. throw new RuntimeException(ex); 
21. catch (KeyStoreException ex) { 
22. Logger.getLogger(TrustStoreCallbackHandler.class.getName()).log(Level.SEVERE,null, ex); 
23. throw new RuntimeException(ex); 
24. finally 
25. try 
26. is.close(); 
27. catch (IOException ex) { 
28. Logger.getLogger(TrustStoreCallbackHandler.class.getName()).log(Level.SEVERE,null, ex); 
29.
30.
31. }
32.  
33. public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { 
34. System.out.println("Truststore CBH.handle() Called.........."); 
35. for (int i = 0; i < callbacks.length; i++) {
36. if (callbacks[i] instanceof KeyStoreCallback) { 
37. KeyStoreCallback cb = (KeyStoreCallback) callbacks[i]; 
38. print(cb.getRuntimeProperties()); 
39. cb.setKeystore(keyStore); 
40. else 
41. throw new UnsupportedCallbackException(callbacks[i]); 
42.
43.
44. }
45.  
46. private void print(Map context) { 
47. Iterator it = context.keySet().iterator(); 
48. while (it.hasNext()) { 
49. System.out.println("Prop " + it.next()); 
50.
51. }
52. }


 

1.8. 重回编辑 Web 服务属性

重新开启 编辑Web服务属性

 

java调用WCF问题_第15张图片
 

将[认证凭证]选择动态。变更完后画面会改变。

 

java调用WCF问题_第16张图片
 

在[使用者名称回呼处理程序]及[密码回呼处理程序]后方的浏览键按下后选择刚刚所建立的CallbackHandler档案TrustStoreCallbackHandler.java。

接下来点选画面上的[信任库]按键。

 

java调用WCF问题_第17张图片
 

画面将出现信任库配置的画面。



 

java调用WCF问题_第18张图片
 

位置请选择凭证文件所在的keystor﹐同时在信任库密码输入keystore的密码。这时候如果要去选择别名是选不到的﹐请先按下「OK」键﹐并离开编辑Web服务属性的画面回到NetBeans IDE画面。

观察Project之下在原始码套件/META-INF之下多了两个档案﹐MyProducts.svc.xml及wsit-client.xml﹐这是刚刚的设定之后产生的。

 

java调用WCF问题_第19张图片
 

现在必须先开启MyProducts.svc.xml做些修改。修改下的type属性﹐type 必须改为 JKS。

MyProducts.svc.xml 取需要修改的部分

 

view source
print ?
01. <wsp1:Policy wsu:Id="WSHttpBinding_IProductServicePolicy">
02. <wsp1:ExactlyOne>
03. <wsp1:All>
04. <sc:TrustStore wspp:visibility="private" type="JKS" storepass="123456"location="C:\Java\Certificate\myTrustStore" />
05. <sc:CallbackHandlerConfiguration wspp:visibility="private">
06. <sc:CallbackHandler name="usernameHandler"classname="wshttpclient.TrustStoreCallbackHandler"/>
07. <sc:CallbackHandler name="passwordHandler"classname="wshttpclient.TrustStoreCallbackHandler"/>
08. sc:CallbackHandlerConfiguration>
09. wsp1:All>
10. wsp1:ExactlyOne>
11. wsp1:Policy>


 

上述修改之后﹐再次回到Web服务属性编辑画面并点选信任库﹐这时再去拉选别名﹐就可以选择到凭证档的别名了﹐按下OK后并离开Web服务属性编辑画面﹐再次检视刚刚刚的MyProducts.svc.xml可以发现设定多了peeralais的设定。

        location="C:\Java\Certificate\myTrustStore" peeralias="win7svrkey"/>

 
1.9. 撰写 Client 程序

前面的设定已完成了工作中的大部分﹐剩下client程序。

wcfClient.java

 

view source
print ?
01. public class wcfClient {
02. public static void main(String[] args) {
03. org.tempuri.ProductService client;
04. org.tempuri.IProductService port;
05.  
06. Product product;
07. try{
08. client=new org.tempuri.ProductService();
09. port=client.getWSHttpBindingIProductService();
10.  
11. //设定呼叫WCF的使用者账号/密码
12. ((BindingProvider)port).getRequestContext().put(BindingProvider.USERNAME_PROPERTY,"testman");
13. ((BindingProvider)port).getRequestContext().put(BindingProvider.PASSWORD_PROPERTY,"a0987");
14.  
15. String result= port.saySomething("Hello~~");
16. System.out.println(result);
17. System.out.println();
18.  
19. product=port.getProduct("P-001");
20. System.out.println("产品编号:"+product.getNo().getValue());
21. System.out.println("产品名称:"+product.getName().getValue());
22. System.out.println("单价:"+product.getPrice());
23. System.out.println("数量:"+product.getQuantity());
24. }catch(Exception er){
25. System.out.println(er.getMessage());
26. }
27. }
28. }

1.10. 测试程序

执行Build Project﹐如果没有任何错误﹐那么就直接执行程序。结果﹐失败~~

 

view source
print ?
01. java.util.logging.ErrorManager: 5
02. java.lang.NullPointerException
03. at java.util.PropertyResourceBundle.handleGetObject(PropertyResourceBundle.java:136)
04. at java.util.ResourceBundle.getObject(ResourceBundle.java:368)
05. at java.util.ResourceBundle.getString(ResourceBundle.java:334)
06. at java.util.logging.Formatter.formatMessage(Formatter.java:108)
07. at java.util.logging.SimpleFormatter.format(SimpleFormatter.java:63)
08. at java.util.logging.StreamHandler.publish(StreamHandler.java:177)
09. at java.util.logging.ConsoleHandler.publish(ConsoleHandler.java:88)
10. at java.util.logging.Logger.log(Logger.java:478)
11. at java.util.logging.Logger.doLog(Logger.java:500)
12. at java.util.logging.Logger.log(Logger.java:589)
13. at com.sun.xml.ws.security.impl.policy.CertificateRetriever.digestBST(CertificateRetriever.java:136)
14. at com.sun.xml.wss.jaxws.impl.SecurityClientTube.processRequest(SecurityClientTube.java:211)
15. at com.sun.xml.ws.api.pipe.Fiber.__doRun(Fiber.java:629)
16. at com.sun.xml.ws.api.pipe.Fiber._doRun(Fiber.java:588)
17. at com.sun.xml.ws.api.pipe.Fiber.doRun(Fiber.java:573)
18. com.sun.org.apache.xml.internal.security.exceptions.Base64DecodingException: It should be divisible by four
19. at com.sun.xml.ws.api.pipe.Fiber.runSync(Fiber.java:470)
20. at com.sun.xml.ws.client.Stub.process(Stub.java:319)
21. at com.sun.xml.ws.client.sei.SEIStub.doProcess(SEIStub.java:157)
22. at com.sun.xml.ws.client.sei.SyncMethodHandler.invoke(SyncMethodHandler.java:109)
23. at com.sun.xml.ws.client.sei.SyncMethodHandler.invoke(SyncMethodHandler.java:89)
24. at com.sun.xml.ws.client.sei.SEIStub.invoke(SEIStub.java:140)
25. at $Proxy43.saySomething(Unknown Source)
26. at wshttpclient.wcfClient.main(wcfClient.java:31)

java调用WCF问题_第20张图片
 

接着在[链接库]上按鼠标右键选择[Add Library…]﹐点选之前所加入的Metro 2.2.1按下[加入链接库]就可以了。

 

java调用WCF问题_第21张图片   java调用WCF问题_第22张图片 

java调用WCF问题_第23张图片
 

重新再Build一次程序后﹐再一次执行。结果﹐再次失败~~~

错误的讯息

2012/12/22 上午 10:44:09 [com.sun.xml.ws.policy.parser.PolicyConfigParser]  parse
信息: WSP5018: 已从档案 file:/D:/MyProject/WCF/WCFSite/WCFSolution/Java/NetBeansProjects/wsHttpClient/build/classes/META-INF/wsit-client.xml 载入 WSIT 组态.
Truststore CBH.CTOR Called..........
Truststore CBH.CTOR Called..........
2012/12/22 上午 10:44:12 com.sun.xml.wss.jaxws.impl.SecurityClientTube processClientResponsePacket
严重的: WSSTUBE0025: 验证输入讯息中的安全性时发生错误.
com.sun.xml.wss.impl.PolicyViolationException: ERROR: No security header found in the message
at com.sun.xml.wss.impl.policy.verifier.MessagePolicyVerifier.verifyPolicy(MessagePolicyVerifier.java:138)
at com.sun.xml.ws.security.opt.impl.incoming.SecurityRecipient.createMessage(SecurityRecipient.java:1016)
at com.sun.xml.ws.security.opt.impl.incoming.SecurityRecipient.validateMessage(SecurityRecipient.java:252)
at com.sun.xml.wss.jaxws.impl.SecurityTubeBase.verifyInboundMessage(SecurityTubeBase.java:455)
at com.sun.xml.wss.jaxws.impl.SecurityClientTube.processClientResponsePacket(SecurityClientTube.java:434)
at com.sun.xml.wss.jaxws.impl.SecurityClientTube.processResponse(SecurityClientTube.java:362)
at com.sun.xml.ws.api.pipe.Fiber.__doRun(Fiber.java:1074)
at com.sun.xml.ws.api.pipe.Fiber._doRun(Fiber.java:979)
at com.sun.xml.ws.api.pipe.Fiber.doRun(Fiber.java:950)
at com.sun.xml.ws.api.pipe.Fiber.runSync(Fiber.java:825)
at com.sun.xml.ws.client.Stub.process(Stub.java:443)
at com.sun.xml.ws.client.sei.SEIStub.doProcess(SEIStub.java:174)
at com.sun.xml.ws.client.sei.SyncMethodHandler.invoke(SyncMethodHandler.java:119)
at com.sun.xml.ws.client.sei.SyncMethodHandler.invoke(SyncMethodHandler.java:102)
WSSTUBE0025: 验证输入讯息中的安全性时发生错误.
at com.sun.xml.ws.client.sei.SEIStub.invoke(SEIStub.java:154)
at $Proxy41.saySomething(Unknown Source)
at wshttpclient.wcfClient.main(wcfClient.java:31)
成功建置 (总时间:3 秒)
失败的原因是什么呢?这里必须回到 WCF 的设定﹐检视 WCF 下的 wcf.config 中的 Binding 设定

修改前

 

   1: 
   2:    
   3:         
   4:           
   5:             
   6:           

   7:         

   8:   

   9: 

修改后
   1: 
   2:    
   3:       
   4:           
   5:                 6:                       negotiateServiceCredential="false"
   7:                       algorithmSuite="Basic128"
   8:                       establishSecurityContext="false" />
   9:           

  10:       

  11:   

  12: 

在message标签中negotiateServiceCredential预设是true﹐这个属性和Windows认证有关﹐可以参考MSDN的文章http://msdn.microsoft.com/zh-tw/library/system.servicemodel.messagesecurityoverhttp.negotiateservicecredential.aspx 及http://paper.dic123.com/lunwen_234540427/ ﹐不是很好了解﹐这里Java要呼叫必须将 negotiateServiceCredential 设为 false。
另外algorithmSuite原本默认值为Basic256必须要修改为Basic128﹐理由是什么…我记得曾在一篇国外讨论区看到说Java不支持到256的长度﹐文章已遗落在茫茫网海中﹐这给Java高手去回答吧。establishSecurityContext默认值为true也必须改为false。
注意﹐WCF的web.config一旦有改变﹐原本呼叫此WCF的Client程序必须跟着修改设定。
之后重新执行Build Project﹐那么就直接执行程序。结果﹐这次总算成功~~

 java调用WCF问题_第24张图片
 

网络上有许多人询问Java可否调用wsHttpBinding的WCF呢?根据实作后的经验﹐当然是可以﹐不过为什么会有人有疑惑?首先﹐WCF具有多项认证技术﹐例如Windows认证﹑SQL Membership﹑使用者账号密码…等﹐而在wsHttpBinding其预设是使用Windows认证﹐这在微软各项Solution中不会有什么太大的问题﹐但是像Java就不一定。在这一小节里提到negotiateServiceCredential这个属性必须改为false﹐就是因为这个设定和 Windows 认证有关﹐当然可以在WCF组态设定变更认证方式﹐但一奱更之后就跟随着必须提供凭证﹐例如本次的范例实作﹐这一部分是常被大家所混淆的。

2. 自订使用者账号/密码﹐Java Client 以Glassfish为container

2.1. 建立项目

档案/New Project/Java Web/Web应用程序﹐建立一个Web应用程序。这次就不再示范无认证的WCF﹐直接跑有认证的WCF。专案名称:wsHttpWebAppUseAuth

 

java调用WCF问题_第25张图片 

java调用WCF问题_第26张图片
 

下一步之后这里要选择所使用的容器﹐这里选择的是GlassFish Server 3.1.2。

 

java调用WCF问题_第27张图片
 

按下[完成]后﹐可以看到所产生的项目架构。

 

java调用WCF问题_第28张图片
 

2.2. 加入 Web Service Client

在项目名称上鼠标右键选择New/Web Service Client。

 

java调用WCF问题_第29张图片
 

选择WSDL URL并输入具有认证的WCF URL。

WSDL URL:http://10.0.100.101:85/wsHttpUserAuth.webhost/MyProducts.svc?wsdl

 java调用WCF问题_第30张图片
 

[完成]之后在项目中可以看到同样产生了Generated Sources还有服务参照。请记得Generated出来的class档带有中文﹐要先将档案格式转换成UTF-8﹐不然在最后做builder时会失败。

 

java调用WCF问题_第31张图片
 

2.3. 编辑 Web 服务属性

在Web服务参照之下的MyProducts按下鼠标右键选择[编辑Web服务属性]。

 

java调用WCF问题_第32张图片
 

到此和之前的Java Application的做法都相同﹐但这次是搭配GlassFish做container﹐因此不需要METRO﹐故接下来直接点[信任库]设定keystore就可以。

 

java调用WCF问题_第33张图片
 

将位置改选择放置凭证的keystore档案﹐并输入keystore的密码。同样的现在是选不到别名的。请按下[OK]回到前一个画面后也按下[OK]离开Web服务属性的编辑。

 

java调用WCF问题_第34张图片
 

重新检视项目﹐在原始码套件/META-INF之下多了MyProducts.svc.xml档案。

 

java调用WCF问题_第35张图片
 

开启MyProducts.svc.dml﹐并修改下的Type属性改为JKS。
修改后
   1: 
   2:   
   3:     
   4:       
   5:     

   6:   

   7: 

再重新回到之前的Web服务属性的编辑并进入信任库中﹐这时就已经可以选择别名了。指定别名后回头看MyProducts.svc.xml的设定就会发现已有修改。如果指令很熟悉的人就不需要这么麻烦了﹐直接编写设定档即可。
 
2.4. 建立 Servlet
在原始码套件上以鼠标右键﹐选择New/Servlet﹐然后输入ClassName和Package﹐按下[完成]。

 

java调用WCF问题_第36张图片
 

然后在所产生的wsService.java中输入以下程序代码

wsService.java

 

view source
print ?
01. @WebServlet(name = "wsService", urlPatterns = {"/wsService"})
02. public class wsService extends HttpServlet {
03. /**
04. * Processes requests for both HTTP
05. * GET and
06. * POST methods.
07. *
08. * @param request servlet request
09. * @param response servlet response
10. * @throws ServletException if a servlet-specific error occurs
11. * @throws IOException if an I/O error occurs
12. */
13. protected void processRequest(HttpServletRequest request, HttpServletResponse response)
14. throws ServletException, IOException {
15. response.setContentType("text/html;charset=UTF-8");
16. PrintWriter out = response.getWriter();
17. org.tempuri.ProductService client=null;
18. org.tempuri.IProductService port=null;
19. Product product;
20. try {
21. client=new org.tempuri.ProductService();
22. port=client.getWSHttpBindingIProductService();
23. BindingProvider bp=(BindingProvider)port;
24. bp.getRequestContext().put(BindingProvider.USERNAME_PROPERTY, "testman");
25. bp.getRequestContext().put(BindingProvider.PASSWORD_PROPERTY, "a0987");
26.  
27. out.println("");
28. out.println("");
29. out.println("Servlet wsService");           
30. out.println("");

你可能感兴趣的:(#,webservice)