过去微软.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时所制作的凭证先汇出。
将汇出的凭证档档名命名为 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所有的凭证档。
如果有删除凭证的需要时同样使用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 加入链接库。
在链接库管理器中﹐按下[加入JAR/数据夹]指向Metor 2.2.1的位置﹐之后在NetBeans 中就可以直接选择了。
1.3. 建立项目
开启NetBeans新增项目﹐左侧选择Java﹐右侧选择Java Application﹐[下一步]继续。
输入项目名称﹑项目位置﹑Class文件等各项信息﹐按下[完成]。
专案名称:wsHttpClient
Class名称:JavaClient
1.4. 加入 Web Service Client
项目建立后﹐在该项目的名称上以鼠标右键选择 New/Web Service Client。
完成后回到NetBeans IDE接口等一下﹐NetBeans正在产生WSDL档及自动产生一些相关的设定档和程序代码。NetBeans跑完后大约如下图。
1.5. 档案转换为 UT-8 格式
检视项目之下有个Generated Source﹐将其展开后会如下﹐在Generated Source是NetBeans依WSDL所自动产生的class檔。
点选开启 GetProduct.java﹐画面会出现警告﹐这是因为NetBeans自动Generated的档案为ANSI格式﹐但这个档案中有中文﹐而NetBeans要读取的档案为UTF-8﹐故这里NetBeans跳出了警告。
因为档案带有中文字﹐因此必须要先将档案改存为UTF-8才行﹐不然后续做 Builde Project 时是会出错的。可以用记事本开启档案﹐再以另存新档方式变更格式再回存。不过因为档案不只一个﹐这样改比较慢﹐可以去网络找一个老牌的工具 ConverZ 做批次转换。 www.it165.net
1.6. 编辑 Web 服务属性
接着在Web服务参照下的MyProducts按下鼠标右键﹐选择[编辑Web服务属性]。
开出的是Web服务属性的画面﹐这里我们只要设定Quality Of Service页签下关于安全的部分。
在编辑Web服务属性的设定画面上有一个[使用开发默认值]的复选框﹐点击一下﹐画面会跳出一些讯息。
这个讯息是询问我们,是否使用METRO Library﹐如果要使用则会移除JAX-WS library﹐因为JAX-WS已被包含于METRO之中。按下Yes则NetBeans会帮我们项目加上METRO。如下图﹐回到Project中展开链接库﹐就可以看到METRO 2.0已被加入。
再回到编辑Web服务属性设定画面﹐刚刚所点击的[使用开发默认值]的复选框如果已经有被勾选了﹐请将勾选取消。然后先离开编辑Web服务属性设定画面。
1.7. 加入CallbackHandler 档案
这里需要加入一个继承CallbackHandler的档案
TrustStoreCallbackHandler.java
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服务属性
将[认证凭证]选择动态。变更完后画面会改变。
在[使用者名称回呼处理程序]及[密码回呼处理程序]后方的浏览键按下后选择刚刚所建立的CallbackHandler档案TrustStoreCallbackHandler.java。
接下来点选画面上的[信任库]按键。
画面将出现信任库配置的画面。
位置请选择凭证文件所在的keystor﹐同时在信任库密码输入keystore的密码。这时候如果要去选择别名是选不到的﹐请先按下「OK」键﹐并离开编辑Web服务属性的画面回到NetBeans IDE画面。
观察Project之下在原始码套件/META-INF之下多了两个档案﹐MyProducts.svc.xml及wsit-client.xml﹐这是刚刚的设定之后产生的。
现在必须先开启MyProducts.svc.xml做些修改。修改
MyProducts.svc.xml 取需要修改的部分
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"/> 前面的设定已完成了工作中的大部分﹐剩下client程序。 wcfClient.java 1.10. 测试程序 执行Build Project﹐如果没有任何错误﹐那么就直接执行程序。结果﹐失败~~ 接着在[链接库]上按鼠标右键选择[Add Library…]﹐点选之前所加入的Metro 2.2.1按下[加入链接库]就可以了。 重新再Build一次程序后﹐再一次执行。结果﹐再次失败~~~ 错误的讯息 2012/12/22 上午 10:44:09 [com.sun.xml.ws.policy.parser.PolicyConfigParser] parse 修改前 1: 网络上有许多人询问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 下一步之后这里要选择所使用的容器﹐这里选择的是GlassFish Server 3.1.2。 按下[完成]后﹐可以看到所产生的项目架构。 2.2. 加入 Web Service Client 在项目名称上鼠标右键选择New/Web Service Client。 选择WSDL URL并输入具有认证的WCF URL。 WSDL URL:http://10.0.100.101:85/wsHttpUserAuth.webhost/MyProducts.svc?wsdl [完成]之后在项目中可以看到同样产生了Generated Sources还有服务参照。请记得Generated出来的class档带有中文﹐要先将档案格式转换成UTF-8﹐不然在最后做builder时会失败。 2.3. 编辑 Web 服务属性 在Web服务参照之下的MyProducts按下鼠标右键选择[编辑Web服务属性]。 到此和之前的Java Application的做法都相同﹐但这次是搭配GlassFish做container﹐因此不需要METRO﹐故接下来直接点[信任库]设定keystore就可以。 将位置改选择放置凭证的keystore档案﹐并输入keystore的密码。同样的现在是选不到别名的。请按下[OK]回到前一个画面后也按下[OK]离开Web服务属性的编辑。 重新检视项目﹐在原始码套件/META-INF之下多了MyProducts.svc.xml档案。 开启MyProducts.svc.dml﹐并修改 然后在所产生的wsService.java中输入以下程序代码 wsService.java
1.9. 撰写 Client 程序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.
}
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)
信息: 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 设定
2:
3:
4:
5:
6:
7:
8:
9:
修改后
1:
2:
3:
4:
5:
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﹐那么就直接执行程序。结果﹐这次总算成功~~
修改后
1:
2:
3:
4:
5:
6:
7:
再重新回到之前的Web服务属性的编辑并进入信任库中﹐这时就已经可以选择别名了。指定别名后回头看MyProducts.svc.xml的设定就会发现已有修改。如果指令很熟悉的人就不需要这么麻烦了﹐直接编写设定档即可。
2.4. 建立 Servlet
在原始码套件上以鼠标右键﹐选择New/Servlet﹐然后输入ClassName和Package﹐按下[完成]。01.
@WebServlet(name =
"wsService"
, urlPatterns = {
"/wsService"
})
02.
public
class
wsService extends HttpServlet {
03.
/**
04.
* Processes requests for both HTTP
05.
*
GET
and06.
*
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(
"
);
30.
out
.println(
""
);