http://www.ibm.com/developerworks/cn/webservices/tutorials/ws-gsoap/index.html#listing6
本文通过一系列的实验步骤描述,向您展示如何使用 gSOAP 编写的客户端,通过双向证书认证,访问由 tomcat7 和 Axis2 搭建的 web 服务。全文循序渐进,分为几个阶段逐步展开,每个阶段相关的注意事项和遇到过的一些问题也一并记录下来供读者参考。
先简单介绍一下需要准备的工具。
此外,为叙述方便,后文的用词上也作一些约定:
回页首
这里编写一个非常简单的 sayHello 功能的 java 代码,传入参数是一个字符串,返回值则是“Hello”加上传入的字符串。稍后用其发布 web 服务已经足够了,如 清单 1 所示:
public class SimpleService{ public String sayHello(String name){ return "Hello, "+name; } }
使用 eclipse 或者 jdk 自带的 javac 命令行工具进行编译,生成 SimpleService.class 文件。
利用 SimpleService.class 文件,快速的发布一个基于 http 的 web 服务,步骤如下:
这样,一个简单的 web 服务发布就完成了。
验证的方式也很简单,打开发布服务电脑上的浏览器,查看网址 http://127.0.0.1:8080/axis2/services/SimpleService?wsdl,如果能够显示 XML 结构的内容,就说明发布成功。
当然,也可以通过局域网内其它电脑访问该服务,如果访问不了,需要查看,IP 地址和端口是否正确,防火墙是否配置得当或关闭等。
后文我们将这个基于 http 的 web 服务网址记为 <service_http>。
值得一提的是,这里我们发布的 web 服务是通过 axis2 提供的热部署 (hotdeployment)功能进行的。打开<tomcat_home>\webapps\axis2\WEB-INF\conf 下的 axis2.xml 文件,我们可以看到 axis2 默认是打开了热部署开关 hotdeployment 的,如 图 1所示。
此外,pojo 部署目录也可以进行修改,如 图 2 所示:
回页首
编写客户端,首先要使用 gSOAP 提供的工具生成一些代码。
gSOAP 的压缩包可以从其网站下载,例如 gsoap_2.8.9.zip,然后解压即可。后文我们将 gSOAP 压缩包的解压路径记为:<gsoap_home>。
在 <gsoap_home>\gsoap\bin\ 下,已经为我们准备好了三种不同平台的代码生成工具,使用时根据自己的操作系统进行选择。如果没有合适的版本,可以根据工具源码,以及 INSTALL.txt 文件的描述,自行编译。
下面描述工具的使用步骤。
在命令行中输入生成 C++ 头文件的命令:
wsdl2h -o gSOAP_http.h <service_http>
如果要生成 C 头文件,则需要加上 -c 选项:
wsdl2h -c -o gSOAP_http.h <service_http>
此外,我们也可以将 <service_http> 从浏览器中另存为本地的 XML 文件,用于上述的命令中来生成头文件。
wsdl2h 命令选项比较多,除了常用的 -c – o 等选项之外,可以敲入 -h 进行详细查看。
注意! 在生成代码的过程中,可能会遇到“Cannot open file ‘ typemap.dat ’”的提示,这里 typemap.dat 文件主要的作用是从 xml 格式数据转换为 C/C++ 代码时,可以自定义或优化绑定的类型。该文件可以从 <service_http>\gsoap\ 下找到。本文的实验中,不使用该文件也可以,wsdl2h 会使用内置类型进行转换。
接下来,我们利用 gSOAP_http.h 生成相关的源码。
在命令行中输入生成 C++ 客户端源码的命令:
soapcpp2 -C gSOAP_http.h -I <gsoap_home>\gsoap\import
如果要生成 C 源码,则需要加上 -c 选项:
soapcpp2 -c -C gSOAP_http.h -I <gsoap_home>\gsoap\import
这里 -C 表示只生成客户端源码,-c 表示只生成 C 语言源码,-I 指明了生成源码时需要提供的头文件路径。soapcpp2 命令还有更多的选项,可以通过敲入 -h 进行详细查看。
如果生成的是 C++ 源码,则需要的文件包括以下 8 个:
如果生成的是 C 源码,则需要的文件包括以下 8 个:
注意! 并非针对所有的 web 服务生成的文件名和数量都是固定的,例如这里会生成 SimpleServiceSoap11Binding.nsmap 和 SimpleServiceSoap12Binding.nsmap 两个文件,但有些 web 服务可能只生成一个 .nsmap 文件。编写客户端代码时要根据具体情况,进行头文件包含。
根据挑选好的 8 个文件,建立一个 C++ 或者 C 的工程,然后再自行建立一个包含 main() 函数的源文件:
#include "soapH.h" #include "SimpleServiceSoap11Binding.nsmap" using namespace std; int main() { struct soap tClientSoap; _ns1__sayHello tSayHelloSender; _ns1__sayHelloResponse tSayHelloResponse; int iResult; soap_init(&tClientSoap); tSayHelloSender.args0 = new string("the beautiful world!\n"); iResult=soap_call___ns2__sayHello(&tClientSoap, NULL, NULL, &tSayHelloSender, &tSayHelloResponse); if(iResult==SOAP_OK) { cout<<*(tSayHelloResponse.return_)<<endl; } else { cout<<"Error code "<<iResult<<endl; } delete tSayHelloSender.args0; soap_destroy(&tClientSoap); soap_end(&tClientSoap); soap_done(&tClientSoap); return 0; }
#include "soapH.h" #include "SimpleServiceSoap11Binding.nsmap" int main() { struct soap tClientSoap; struct _ns1__sayHello tSayHelloSender; struct _ns1__sayHelloResponse tSayHelloResponse; char achName[256]="the beautiful world!\n"; int iResult; soap_init(&tClientSoap); tSayHelloSender.args0=achName; iResult=soap_call___ns2__sayHello(&tClientSoap, NULL, NULL, &tSayHelloSender, &tSayHelloResponse); if(iResult==SOAP_OK) { printf("%s\n",tSayHelloResponse.return_); } else { printf("Error code %d\n",iResult); } soap_destroy(&tClientSoap); soap_end(&tClientSoap); soap_done(&tClientSoap); return 0; }
除了前文所述的,需要根据具体情况调整包含的 .nsmap 文件名之外。
使用 gSOAP 工具生成的头文件和源码中包含的结构体、类和函数名也是根据情况的变化而不同的(本例中是 _ns1_sayHello、_ns1_sayHelloResponse 以及 soap_call___ns2__sayHello() 函数)。通常情况下,查阅 wsdl2h 工具生成的头文件(本例中是 gSOAP_http.h 文件),就可以找到对应的声明。
如果出现调用 soap 函数返回结果不是 SOAP_OK 的情况,可以根据错误码打印,在 stdsoap2.h 文件中找到对应的宏定义名。
Windows 平台下:
以 C++ 代码为例,可以使用 Visual Studio2010 或者 CodeBlocks+MinGW 进行编译和运行。
其中,使用 CodeBlocks+MinGW 可能会遇到一些问题,例如编译的时候可能会报错“undefined reference to sendto@24'”或者“undefined reference to __imp_send”。此时需要链接库文件 libws2_32.a。
Linux 平台下:
以 C 代码为例,可以在源码目录下简单键入命令 gcc – o Hello *.c 生成可执行文件。
如果上述步骤均无错,编译成功后运行,可以看到输出结果“Hello, the beautiful world!”
回页首
前面描述了一个基于 http 方式 web 服务的搭建和访问。接下来,为了将其改造为基于 https 的通信方式,我们需要先准备好相关的证书。
由于只是用于实验目的,就不向专门的 CA 认证机构申请证书了,使用 openssl 工具进行证书的制作。
Windows 平台
在 Windows 平台下编译 openssl 需要确保安装有 Visual C++2005 以上版本,如果没有 perl 工具还需要安装 activeperl。
编译步骤如下:
上述步骤完毕后,生成的内容都输出到 D:\ssl 了(实际上先是生成到解压目录下的 out32dll 子目录中的)。后文我们就将 openssl 的安装目录记为 <openssl_home>。
其中,<openssl_home>\lib\ 下的 libeay32.lib 和 ssleay32.lib 就是后文进行程序的编译链接时需要的库文件。
注意! 如果不严格按照上述步骤来做,容易出现一些问题。
例如:使用 Visual C++6.0 版本的话,很可能会在执行 nmake -f ms\ntdll.mak 时遇到错误 NMAKE : fatal error U1077: 'ml' : return code '0x1',微软提供了一个 MASMsetup.EXE 文件 的下载用来修正这个问题,但实际上问题还会存在。仔细阅读微软的下载页面就会发现,这个文件是为 Visual C++2005 准备的。所以还是建议使用 Visual C++2005 以上的版本。
再如:如果不是在命令行中,运行上文所述的 vcvarsall.bat 文件,就会出现 namke.exe、link.exe 等工具找不到的错误提示。即使手动将相关路径加入 PATH 环境变量,还是会出现其它文件找不到引发的错误提示。
此外,设定 <openssl_home> 的时候,如果指定到根目录下,可能会提示错误,无法创建相关文件夹。
如果要清理掉重新编译,先执行 nmake -f ms\ntdll.mak clean 命令。
Linux 平台
通常情况下 Linux 系统已经自带 openssl。可以在命令行下键入 openssl 命令进行尝试,如果提示找不到相关命令,再使用压缩包解压后进行编译和安装,过程比较简单,这里不赘述。
注意!Windows 下 libeay32.lib 和 ssleay32.lib 文件,在 Linux 下对应的文件名是 libcrypto.a 和 libssl.a
从命令行进入 <openssl_home>\bin 目录,输入命令
openssl req -new -x509 -keyout ca.key -out ca.crt -config ..\ssl\openssl.cnf
这里会要求输入 key 文件保护口令,以及证书的详细信息,生成的文件包括:
注意! 接下来制作服务端和客户端证书的时候,填写的证书详细信息,不要和 CA 证书的详细信息完全一样,否则,使用 CA 私钥进行签名时会报错:“openssl TXT_DB error number 2 failed to update database”。
先使用 jdk 自带的 keytool 工具,为服务端生成一个 keystore 文件。打开命令行,进入某个存放服务端证书的目录,然后输入命令:
keytool -genkey -alias server -keyalg RSA -keystore server.keystore
这里 keytool 也会交互的要求用户输入 keystore 文件保护口令,以及相应的证书详细信息。
生成的 server.keystore 文件中,包含了服务端的一对密钥以及一个自签名证书。
注意! 服务端证书详细信息中的 CommonName(名字与姓氏)要和客户端访问的 URL 域名或 IP 地址一致,否则 gSOAP 程序内部会检验不通过导致访问失败。具体情况下文会提及。
由于我们要让服务端和客户端信任同一个 CA,因此这里生成的自签名证书需要替换成由 CA 私钥 签名的证书。具体操作如下:
首先,生成服务端的证书签名请求文件,输入命令
keytool -certreq -alias server -sigalg MD5withRSA -file server.csr -keystore server.keystore
按照提示,输入 server.keystore 的保护口令后,生成了 server.csr
然后将 server.csr 拷贝到 CA 所在电脑的 <openssl_home>\bin 目录下,输入命令:
openssl ca -in server.csr -out server.crt -cert ca.crt -keyfile ca.key -notext -config ..\ssl\openssl.cnf
注意! 这里可能会提示缺少 ./demoCA 等目录和文件。要解决这个问题,可以修改 <openssl_home>\ssl\openssl.cnf 中的配置,也可以在当前的 <openssl_home>\bin 下手动建立如下内容:
注意 ! 这里还可能会遇到诸如“The stateOrProvinceName field needed to be the same in the CA certificate (xx) and the request (xx)”的错误。有可能 CA 证书的这个字段和服务端证书请求的这个字段确实不同,那么重新生成字段相同的文件即可。也有可能明明已经相同了,还报这个错误,这是 openssl 某些版本的 bug,可以查阅 openssl 的 bug report 页面以及 相关参考 页面。为解决这个问题,需要将<openssl_home>\ssl\openssl.cnf 中 [ policy_match ] 下相关字段值由 match 修改为 optional。
重新执行一下上述命令 , 如果顺利,就会生成服务端证书文件 server.crt。
接下来,我们要将 CA 签名后的证书导入 server.keystore。方法是将 ca.crt 和 server.crt 拷贝回 server.keystore 所在电脑的目录下,输入命令:
keytool -import -v -trustcacerts -alias ca -file ca.crt -keystore server.keystore
keytool -import -v -alias server -file server.crt -keystore server.keystore
上述第一步,是导入 CA 根证书作为服务端的受信任 CA。第二部是导入由 CA 签名的服务端证书。
注意! 这里必须先将 CA 根证书导入服务端证书所在的同一个 keystore 文件中,然后才能导入新生成的服务端证书,否则 keytool 会报错“keytool 错误: java.lang.Exception: 无法从回复中建立链接”。
先生成客户端私钥,在命令行中输入:
openssl genrsa -des3 -out client.key 1024
接着生成客户端证书签名请求文件,输入命令:
openssl req -new -key client.key -out client.csr -config ..\ssl\openssl.cnf
最后进行 CA 签名:
openssl ca -in client.csr -out client.crt -cert ca.crt -keyfile ca.key -notext -config ..\ssl\openssl.cnf
顺利的话,客户端证书文件 client.crt 就生成完毕了。
注意! 我们还需要制作一个客户端证书与 key 的拼接文件,后文会使用到。简单来说就是将 client.crt 和 client.key 的内容,按顺序合成一个文件。具体合成的方法 Linux 下可以使用命令“cat client.crt client.key > client.pem”,Windows 下可以使用命令“type client.crt client.key > client.pem”,或者用文本编辑器,直接手动拼接也可以。
生成的和证书相关的文件。包括:
后文中需要用到的包括:
回页首
打开 <tomcat_home>\conf\server.xml 文件,找到如 图 3 所示的片段:
将这一段注释符去掉,使得 https 端口 8443 生效(如果找不到这一段,也可以自行添加),并添加上参数 keystoreFile 和 keystorePass,如 图 4。
这里假定,先前生成的 server.keystore 文件 的保护口令是“server”。
其中 clientAuth=”false”表示服务端不需要检查客户端证书。
修改完毕后,重启 tomcat7 服务,然后可以打开网页浏览器,访问 https://127.0.0.1:8443 进行尝试,如果提示“不受信任的链接”之类的安全警报,可以点击接受信任,并继续访问,就可以看到显示有 Apache Tomcat 标志的页面了。
此时还没有结束,因为我们还不能访问 axis2 提供的 web 服务。
打开 <tomcat_home>\webapps\axis2\WEB-INF\conf\axis2.xml 文件,应该可以找到 图 5 所示的配置片段:
我们在这段配置下面增加一概述段 https 的配置,如 图 6 所示:
上述 tomcat7 和 axis2 的配置文件都修改完毕并保存后,重启 tomcat7 服务,然后访问地址:https://127.0.0.1:8443/axis2/services/SimpleService?wsdl,如果能够显示 XML 结构的内容,就说明发布成功。
同样的,也可以通过局域网内其它电脑访问该服务,如果访问不了,需要查看,IP 地址和端口号是否正确,防火墙是否配置得当或关闭等因素。
后文我们将这个基于 https 的 web 服务网址记为 <service_https>。
回页首
相关的头文件的生成,以及源文件的生成方法,同前文所述。
注意! 使用 wsdl2h 时,如果直接使用 <service_https>,会遇到 图 7 所示的问题,提示 wsdl2h 程序“no SSL support, please rebuild wsdl2h with SSL or download the files and rerun wsdl2h”:
按照 图 7 的提示,要么重新编译支持 SSL 的 wsdl2h 可执行文件,要么将相关文件下载下来使用。
重新编译 wsdl2h 可以按照 gSOAP 相关文档进行,这里我们采用简单一点的方式,从浏览器打开 <service_https>,并另存为 SimpleService.xml 文件。
注意! 保存在本地的 SimpleService.xml 文件里包含了 URL 信息,如果将其拷贝给局域网其它电脑访问 web 服务,那么另存的 URL 地址也应该有相应修改,而不能再是 127.0.0.1 了。例如 图 8 所示的是局域网的 Linux 环境下使用 wget 程序获取 <service_https> 文件的例子,使用 --no-check-certificate 选项,忽略对服务端证书的检查:
这和在本机使用浏览器,打开 非 127.0.0.1 地址的 URL 后,另存为 XML 文件,再拷贝到局域网机器上,效果是一样的。
生成 C++ 相关的客户端文件命令:
wsdl2h -o gSOAP_https.h SimpleService.xml
soapcpp2 -C gSOAP_https.h -I <gsoap_home>\gsoap\import
生成 C 相关的客户端文件命令:
wsdl2h -c -o gSOAP_https.h SimpleService.xml
soapcpp2 -c -C gSOAP_https.h -I <gsoap_home>\gsoap\import
同前文所述,挑选好所需的 8 个文件,然后建立一个 C++ 或者 C 的工程,再自行建立一个包含 main() 函数的源文件,进行测试。
和 http 客户端的源码不同之处在于,https 客户端源码,需要在调用 soap_init() 后,调用 soap_ssl_client_context() 函数,这里我们暂且使用 SOAP_SSL_NO_AUTHENTICATION 参数,表示客户端不检查服务端证书。
#include "soapH.h" #include "SimpleServiceSoap11Binding.nsmap" using namespace std; int main() { struct soap tClientSoap; _ns1__sayHello tSayHelloSender; _ns1__sayHelloResponse tSayHelloResponse; int iResult; soap_init(&tClientSoap); if(soap_ssl_client_context(&tClientSoap, SOAP_SSL_NO_AUTHENTICATION, NULL, NULL, NULL, NULL, NULL)) { soap_print_fault(&tClientSoap, stderr); return -1; } else { tSayHelloSender.args0 = new string("the beautiful world!\n"); iResult=soap_call___ns2__sayHello(&tClientSoap, NULL, NULL, &tSayHelloSender, &tSayHelloResponse); if(iResult==SOAP_OK) { cout<<*(tSayHelloResponse.return_)<<endl; } else { cout<<"Error code "<<iResult<<endl; } delete tSayHelloSender.args0; } soap_destroy(&tClientSoap); soap_end(&tClientSoap); soap_done(&tClientSoap); return 0; }
#include "soapH.h" #include "SimpleServiceSoap11Binding.nsmap" int main() { struct soap tClientSoap; struct _ns1__sayHello tSayHelloSender; struct _ns1__sayHelloResponse tSayHelloResponse; char achName[256]="the beautiful world!\n"; int iResult; soap_init(&tClientSoap); if(soap_ssl_client_context(&tClientSoap, SOAP_SSL_NO_AUTHENTICATION, NULL, NULL, NULL, NULL, NULL)) { soap_print_fault(&tClientSoap, stderr); return -1; } else { tSayHelloSender.args0=achName; iResult=soap_call___ns2__sayHello(&tClientSoap, NULL, NULL, &tSayHelloSender, &tSayHelloResponse); if(iResult==SOAP_OK) { printf("%s\n",tSayHelloResponse.return_); } else { printf("Error code %d\n",iResult); } } soap_destroy(&tClientSoap); soap_end(&tClientSoap); soap_done(&tClientSoap); return 0; }
Windows 平台下以 清单 4 的 C++ 代码的编译为例。
使用 CodeBlocks+MinGW 编译的步骤为:
使用 Visual Studio2010 编译的步骤为:
注意! 上述的步骤完毕后,编译并运行,可能会有如 图 9 所示的错误
引发这个问题的原因是生成的 soapClient.cpp 文件中定义的用于调用 web 服务的函数里,默认的 URL 开头多了一个空格,如 图 10 所示:
解决的方法,要么手动删除这个空格,要么在 清单 4 中直接把 <service_https> 以字符串形式作为调用 web 服务的函数(本文中为 soap_call___ns2__sayHello() 函数)的第二个参数传入。
上述问题解决后,再次编译和运行就应该能够看到输出结果“Hello, the beautiful world!”
Linux 平台下以 清单 5 的 C 代码为例。
输入编译命令:gcc -o Hellos -DWITH_OPENSSL -lcrypto -lssl *.c
即可生成可执行文件。
这里同样要注意 图 10 所说到的 URL 头部多了一个空格的问题。
编译和运行没有其它问题的话,就应该能够看到输出结果“Hello, the beautiful world!”
回页首
前面描述了搭建支持 https 的 web 服务及访问的方法,但是客户端和服务端之间的通信并没有涉及到证书的认证,接下来我们将要描述如何进行基于证书认证的通信。
在 清单 4 和 清单 5 中,我们都使用到了 soap_ssl_client_context() 函数,这个函数的定义位于 stdsoap2.cpp 或 stdsoap2.c 中,函数原型为:
int SOAP_FMAC2 soap_ssl_client_context(struct soap *soap,
unsigned short flags,
const char *keyfile,
const char *password,
const char *cafile,
const char *capath,
const char *randfile)
在 gSOAP 网站的 User Guide 页面可以找到若干处对该函数各个入参注释的片段,大致的含义如下:
struct soap *soap ——用于 soap 通信的结构指针
unsigned short flags ——是否进行证书认证的标志
const char *keyfile ——如果服务端要认证客户端,填写客户端的“证书 +key”拼接文件名
const char *password ——如果服务端要认证客户端,填写客户端的 key 文件的密码
const char *cafile ——如果客户端要认证服务端,填写 CA 证书文件名(要包含路径)
const char *capath ——如果客户端要认证服务端,填写 CA 路径或 NULL(本文都用 NULL)
const char *randfile ——如果认证过程需要随机文件,填写随机文件名,否则填写 NULL
其中第二个表示认证标志的参数,常用的宏定义位于 stdsoap2.h 中,如 图 11 所示:
之前我们用的 SOAP_SSL_NO_AUTHENTICATION 只是供测试使用,不会进行证书的认证,下文中我们将使用 SOAP_SSL_DEFAULT 参数。
服务端 tomcat7 保持 图 4 所述的配置不变。
简单的修改 soap_ssl_client_context() 中的参数即可,修改后的代码片段,如 清单 6 所示,其中,ca.crt 文件就是前文所述的 CA 根证书。
if(soap_ssl_client_context(&tClientSoap, SOAP_SSL_DEFAULT, NULL, NULL, "D:\\certs\\ca.crt", NULL, NULL)) { soap_print_fault(&tClientSoap, stderr); return -1; }
注意!前文曾说过,如果服务端证书详细信息中的 CommonName(名字与姓氏)与客户端访问的 URL 的域名或 IP 地址不一致(例如,证书中填写的是“localhost”,客户端访问时使用的 <service_https> 中是“127.0.0.1”),会导致访问失败。具体报错的代码片段,可以在 stdsoap2.cpp 或 stdsoap2.c 中的 tcp_connect() 函数里找到,如 图 12 所示:
由于 图 12 所示的错误提示并不打印在标准输出(通常是屏幕)上,所以一旦出错,客户端程序最多在具体调用 web 服务的函数(本文中为 soap_call___ns2__sayHello() 函数)处返回错误码为 30 的打印(SOAP_SSL_ERROR),难以直接看出原因,所以要特别注意。
编译方法同前文。如果没什么问题,运行应该能够看到输出结果“Hello, the beautiful world!”
服务端 tomcat 的配置文件需要在 图 4 的配置基础上修改,修改后的配置片段见 图 13:
这里我们加上了 truststoreFile 和 truststorePass 字段,并把 clientAuth 的值改为了 true。修改后记得重启 tomcat7 服务。
注意! 本例中 trustoreFile 和 keystoreFile 配置成同一个 keystore 文件了,这是因为 keystore 可以根据别名(alias)存放多份密钥与证书。完全可以将 CA 证书导入另一个单独的 keystore 文件里,作为 truststoreFile 的参数。
客户端的代码需要在 清单 6 的基础上继续修改,见 清单 7。这里,假定 client.pem 文件中包含的 key 文件的保护口令是“client”。
if(soap_ssl_client_context(&tClientSoap, SOAP_SSL_DEFAULT, "D:\\certs\\client.pem", "client", "D:\\certs\\ca.crt", NULL, NULL)) { soap_print_fault(&tClientSoap, stderr); return -1; }
编译方法同前文。如果没什么问题,运行应该能够看到输出结果“Hello, the beautiful world!”