使用 Apache CXF


使用 Apache CXF 0 摘要 使用 Web Service 在两个独立系统中交换信息是 J2EE 开发的常见任务。本文将讲述如何使用 Apache CXF 框架来开发 Web Service,并且利用 Tomcat 的 SSL 支持增进 Web Service 的安全性 1 概述 1.1 Web Service 概念及 CXF 基本开发步骤。 Web Service 本质上是 RPC 调用。以往的 RPC 采用二进制格式进行定义和调用,适合要求效率 的同质系统。新的 Web Service 采用 XML 这种人机友好的格式进行定义与调用,在效率与兼容性之 间取得了比较好的平衡。 Web Service 的系统结构与二进制 RPC 类似,由四部分组成 在动态语言中,调用桩与服务桩将由语言本身产生,而在静态语言中,两者一般由工具生成1。 众所周知,Java 是一种静态语言。而 CXF 与 Axis 就是 Java 中常用的两种 Web Service 代码生成工具。 RPC 除处理服务调用之外,还应该处理服务的定义,使客户程序知道调用服务的方式并且提交 1 CXF 自动生成服务桩 客 户 程 序 调 用 桩 ( 或 称 桩 过 程 ) 服 务 桩 服 务 调用 返回 足够的参数。在 Web Service 负责此项工作的是 WSDL(Web 服务定义语言),它用 XML 文档描述 Web Service 的各种信息。 CXF 紧密围绕着这两个概念进行开发。它提供了方便生成服务桩与调用桩的工具,也提供了从 服务生成 WSDL 文档的工具。但是,由于开发顺序的不同,CXF 开发有两种方法。第一,使用适当的 工具书写 WSDL 文件,使其兼容标准,然后从 WSDL 文件生成服务桩、调用桩与部分服务代码(主 要的类与方法的声明)。另外,也可以使用第二种方法,首先实现服务并抽象出服务的远程调用接口 然后利用 CXF 生成相应的 WSDL 文件与服务桩、调用桩。这两种方法最终实现的代码与效果是类似的。 但是大多数 Web Service 资深专家都会推荐前者,因为前者生成的 WSDL 文件符合标准,不止可以 在 JAVA 平台使用,也可以在其它如.NET,PHP 之类的异构系统中使用,而后者,由于是从 JAVA 代码 生成 WSDL 文件,往往与标准或者与其它异构系统有细微的差异,最终导致无法互操作。本文主要描 述 JAVA 平台下的 Web Service 开发,因此将采用后一种方法,即 Code-First。本文的概念与方法同 样适用于 WSDL-First 开发。 1.2 SSL 概念 安全套接层(SSL)及其继任者传输层安全(TLS)是在互联网上提供保密安全通道的加密协 议,为诸如网站、电子邮件、网上传真等等数据传输进行保密。TLS 利用密钥算法在互联网上提供端点 身份认证与通讯保密。在典型例子中,只有服务器被可靠身份认证(即其验证被确保),客户端踪迹不 一定经可靠认证;相互间的身份认证需要公钥基础设施(PKI)设置于客户端中。协议的设计在某种 程度上能够使客户端/服务器应用程序通讯本身预防窃听、干扰(Tampering)、和消息伪造。 2 CXF 开发实践 2.1 安装 CXF 与其它 Java 类库一样,安装方法是将它的包文件复制到目标系统的${CLASSPATH}文件 夹中。因为 CXF 依赖于其它许多 Java 类库,比如 Spring、WSDL4J 等,所以需要安装的类库很多, 都可以在 CXF 的二进制安装包的 lib 文件夹中找到。如果知道系统的规模,可以对依赖的类库进行删 减,详情见 lib/WHICH_JARS。 在典型的配置下,要将 CXF 集成到已有的 Web 应用程序时,只需要把包文件复制到 WEB- INF/lib 下。 注意,CXF 不支持 JDK1.4! 2.2 开发服务 我们知道,利用 Java 开发一个业务系统或者 Web 应用系统,通常使用利用 MVC 模式将系统分 解为四个层次:表示层、控制层、业务逻辑层和数据访问与存储(持久)层。表示层与控制层通常对应 Struts 或者 Swing 等一些 MVC 框架,而业务逻辑层和数据访问与存储层对应 EJB 或者 Spring+Hibernate 等框架。Web Service 属于控制层,它向外部系统暴露业务逻辑层的访问。在向外 部系统提供 Web Service 前应该先设计好业务逻辑层。 以下示例是一个典型的过程化的业务逻辑层。 UserService.java: package demo.cxf; public interface UserService { public void changeUserState(User user, int state); public boolean checkUserExists(String username); public void createUser(User user); public void cusumePremiumPoint(String username, int points); public void deleteUser(String username) throws NotFoundException; public void increasePremiumPoint(String username, int points); public void updateUserInformation(User user); } UserServiceImpl.java: package demo.cxf; public class UserServiceImpl implements UserService { public void changeUserState(User user, int state) { print("changeUserState"); } public boolean checkUserExists(String username) { print("checkUserExists"); return false; } public void createUser(User user) { print("createUser"); print(user.getUsername()); } public void cusumePremiumPoint(String username, int points) { print("consumePremiumPoint"); } public void deleteUser(String username) throws NotFoundException { throw new NotFoundException("我故意没找到"); } public void increasePremiumPoint(String username, int points) { print("increasePremiumPoint"); } public void updateUserInformation(User user) { print("updateUserInformation"); } private void print(Object o) { System.out.println(o); } } User.java: package demo.cxf; import java.util.Date; public class User { private Date birthday; private int height; private String password; private String sex; private String sign; private String username; public Date getBirthday() { return birthday; } public void setBirthday(Date birthday) { this.birthday = birthday; } public int getHeight() { return height; } public void setHeight(int height) { this.height = height; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getSex() { return sex; } public void setSex(String sex) { this.sex = sex; } public String getSign() { return sign; } public void setSign(String sign) { this.sign = sign; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } } NotFoundException.java package demo.cxf; public class NotFoundException extends Exception { public NotFoundException() { super(); } public NotFoundException(String message, Throwable cause) { super(message, cause); } public NotFoundException(String message) { super(message); } public NotFoundException(Throwable cause) { super(cause); } } 几个小的注意事项:  接口最好不包含内嵌类或者枚举类型,CXF 不能识别它们  接口抛出的异常也将被 CXF 识别并导出 2.3 添加 JAX-WS 标注到 Java 代码中 在设计好逻辑层之后,就可以动手为逻辑层添加 JAX-WS 标注,以便使用 CXF 的代码生成工具 将 Java 代码导出为 WSDL 定义。 TODO 介绍 JAX-WS JAX-WS 定义了很多的标注,可以对导出的服务接口进行定义。其中较常用的是  @WebService,必选的标注。用于导出的服务接口及其实现类 name 定义导出的服务接口的名字,对应于 WSDL 文档中 wsdl:portType。 默认是服务接口的 Java 类名加 PortType targetNamespace 定义导出的服务接口的名域(namespace),默认是倒置的服务接口 Java 包名。如 demo.cxf.UserService 的名域将会是 http://cxf.demo/ serviceName 定义服务名,与名域一起唯一标识一个服务。默认是其 Java 类名 wsdlLocation 其 WSDL 文档 URL。可由服务器容器自动产生 endpointInterface 指定服务接口的 Java 类。通常用于服务实现类的标注。应当指定类的 全名,如 demo.cxf.UserService portName 对应 WSDL 文档中的 wsdl:port 元素。默认是 Java 类名加 Port  @WebMethod,可选的标注,用于服务接口的方法 operationName 指定方法在 WSDL 文档中的名字,客房端用此名字调用方法 action TODO 翻译此说明 Specifies the value of the soapAction attribute of the soap:operation element generated for the method. The default value is an empty string. exclude 生成 WSDL 文档时将该方法排除在外  @SOAPBinding,可选的标注,用于指定生成的 SOAP 定义文档风格。关于此标注再详细的 信息请查阅 SOAP 标准等参考资料 style Style.DOCUMENT (默认) Style.RPC SOAP 消息风格 use Use.LITERAL (默认) Use.ENCODED SOAP 数据编码方式 parameterSt yle ParameterStyle.BARE ParameterStyle.WRAPPED (默认) TODO 翻译此说明 Specifies how the method parameters, which correspond to message parts in a WSDL contract, are placed into the SOAP message body. A parameter style of BARE means that each parameter is placed into the message body as a child element of the message root. A parameter style of WRAPPED means that all of the input parameters are wrapped into a single element on a request message and that all of the output parameters are wrapped into a single element in the response message. If you set the style to RPC you must use the WRAPPED parameter style.  @RequestWrapper,可选的标注,用于指定如何包装客户端调用服务方法使用的参数  @ResponseWrapper,可选的标注,用于指定如何包装客户端调用服务方法的返回值  @WebFault,可选的标注,用于注解服务接口方法抛出的异常 name 异常的名字 targetNamespace 对应的名域,默认是服务接口的名域 faultName 实现该异类的类名  @WebParam,可选的标注,用于指定方法参数的使用方式 name 在 WSDL 文档中的名字,默认是 arg0,arg1… targetNamespace 对应的名域。默认是服务接口的名域 mode Mode.IN (默认)、Mode.OUT、Mode.INOUT 对于 Java 程序没有意义 header true 或者 false(默认),指定该参数是否在 SOAP 消息头部发送 partName TODO 翻译此说明 Specifies the value of the name attribute of the wsdl:part element for the parameter when the binding is document.  @WebResult,可选的标注,用于指定返回值的使用方式 name 返回值在 WSDL 文件中的名字。默认是 return targetNamespace 对应的名域。默认是服务接口的名域 header true 或者 false(默认),指定该参数是否在 SOAP 消息头部发送 partName TODO 翻译此说明 Specifies the value of the name attribute of the wsdl:part element for the parameter when the binding is document. 一个使用了所有标注的示例: package org.apache.cxf; import javax.jws.*; import javax.xml.ws.*; import javax.jws.soap.*; import javax.jws.soap.SOAPBinding.*; import javax.jws.WebParam.*; @WebService(name="quoteReporter") @SOAPBinding(style=Style.RPC, use=Use.LITERAL) public interface quoteReporter { @WebMethod(operationName="getStockQuote") @RequestWrapper(targetNamespace="http://demo.iona.com/types",className="java.lang.String") @ResponseWrapper(targetNamespace="http://demo.iona.com/types", className="org.eric.demo.Quote") @WebResult(targetNamespace="http://demo.iona.com/types",name="updatedQuote") public Quote getQuote( @WebParam(targetNamespace="http://demo.iona.com/types",name="stockTicker",mode=Mode.IN) String ticker ); } 对于我们的例子,应该在服务接口与服务实现类上添加@WebService 标注,如下: UserService.java: @WebService public interface UserService { UserServiceImpl.java: @WebService(endpointInterface = "demo.cxf.UserService", serviceName = "/UserService") public class UserServiceImpl implements UserService { 2.4 使用 java2wsdl 工具生成 WSDL 文件 一旦为服务接口与服务实现类添加好适当的标注后,有两种方法可以得到对应的 WSDL 文件。 一是使用 CXF 提供的 java2wsdl 命令行。二是把服务发布到 Web 容器,由 Web 容器自动生成 WSDL 文件。后者比较简单。比如本文的例子发布到 http://server/cxf/UserService,那么可以使用 http://server/cxf/UserService?wsdl 这个 URL 得到对应的 WSDL 文档。下面是一个使用 java2wsdl 工具的例子: WEB-INF\classes> path D:\ develop\resource\apache-cxf-2.0.1-incubator\bin;%PATH% WEB-INF\classes> set CLASSPATH=.: D:\ develop\resource\apache-cxf-2.0.1-incubator\lib WEB-INF\classes> java2wsdl demo.cxf.UserService WEB-INF\classes> copy UserServiceSerivce.wsdl d:\ws 运行完毕,可以看到生成名为 UserServiceService.wsdl 的 WSDL 文件。 2.5 使用 wsdl2java 工具生成调用桩代码 用命令行 java2wsdl 生成的 WSDL 文件并不一定是正确的,因为它可能没有包含正确的 WebService 地址,但是没关系,我们先生成调用桩代码。 D:\ws> path D:\ develop\resource\apache-cxf-2.0.1-incubator\bin;%PATH% D:\ws> set CLASSPATH=.: D:\ develop\resource\apache-cxf-2.0.1-incubator\lib D:\ws> wsdl2java -client -compile UserServiceService.wsdl 此时 ws 将有一个文件夹 demo 包含调用桩代码及编译后的 class 文件 wsdl2java 还有其它命令行选项: -server 生成服务接口 -client 生成调用桩代码 -impl 生成服务实现代码 -compile 生成代码后编译 -ant 生成 ant 自动构建脚本 -quiet 静默模式,不输出警告与错误信息 2.6 配置服务运行环境 Web Service 一般使用 SOAP 协议将服务暴露为 Web 服务器2。CXF 对此提供了广泛的支持,可 以使用 TOMCAT、这样的 Web 服务器,也可以使用 CXF 集成的 Jetty Web 服务器。两者的配置过程是 类似的。因为 Web 应用系统比较常见,所以这里介绍前一种方式。 将代码编译成功并发布到 Web 容器上,然后定义一个 Spring 的配置文件,假如名字为 beans.xml,内容是: 这个配置文件首先定导入 CXF 预定义的配置文件,然后利用定义一个 Web Service。它有如下属性: implementor 服务实现类 address Web Service 地址,实际地址还要加上 Web 应用程序的地址。如本例 中 Web 应用程序配置在/cxf 下,那么 UserService 的地址是 http://server:8080/cxf/UserService 接着在 Web 应用程序的 web.xml 配置文件中加入下列配置: contextConfigLocation WEB-INF/beans.xml org.springframework.web.context.ContextLoaderListener CXFServlet org.apache.cxf.transport.servlet.CXFServlet 1 CXFServlet /* 可以看到,CXF 使用了 Spring 来发布服务,因此需要在 WEB-INF/lib 中加入 Spring 的类库。 重启 Web 应用程序后可以用 http://localhost:8080/<应用程序名>/UserService?wsdl 得到服务接 口的 WSDL 文档。之前用工具生成的 WSDL 文档可能包含不正确的服务地址,现在把它更新一下。在 WSDL 文档中查找标签。更改为: 2.7 编写 Web Service 客户端 至此,我们已经可以写一个 UserService 的客户端了。代码如下: UserServiceTest.java: import java.net.MalformedURLException; import java.net.URL; import javax.xml.namespace.QName; import demo.cxf.NotFoundException_Exception; import demo.cxf.User; import demo.cxf.UserService; import demo.cxf.UserServiceService; public class UserServiceTest { public static void main(String[] args) throws MalformedURLException { UserServiceService uss=new UserServiceService(new URL("file:d:\\ws\\UserServiceService.wsdl"), new QName("http://cxf.demo/","UserServiceService")); UserService myus=uss.getUserServicePort(); User u=new User(); u.setUsername("fish"); myus.createUser(u); } } 客户端程序首先根据 WSDL 文档(file:d\ws\UserServiceService.wsdl)和一个 WSDL 服务全名 (http://cxf.demo/UserServiceService)生成代理,然后取得 UserService 的代理。利用这个代理,我 们就可以像本地代码一样调用服务器的业务逻辑层了。这个代码没有什么稀奇之处,通常可以完全满 足使用。如果需要深入桩代码内部,CXF 提供了很多辅助类。 值得注意的是,代码中出现的“UserService”、“UserServiceService”和“UserServicePort” 三个类、方法名。规则是服务接口名对应第一个3,而后两个由服务接口名分别加上“-Service”“-Port” 构成。 3 为 CXF 启用 SSL 双向认证 3.1 SSL 概述 安全套接层(SSL)及其新继任者传输层安全(TLS)是在互联网上提供保密安全通道的加密 协议,为诸如网站、电子邮件、网上传真等等数据传输进行保密。TLS 利用密钥算法在互联网上提供端 点身份认证与通讯保密。在典型例子中,只有服务器被可靠身份认证(即其验证被确保),客户端踪迹 不一定经可靠认证;相互间的身份认证需要公钥基础设施(PKI)设置于客户端中。协议的设计在某 种程度上能够使客户端/服务器应用程序通讯本身预防窃听、干扰(Tampering)、和消息伪造。 TLS 包含三个基本阶段: 1. 对等协商密钥算法支持 2. 基于公钥密码的密钥交换和基于证书的身份认证 3. 基于对称密钥的数据传输保密 在第一阶段,客户端与服务器协商所用密码算法。 当前广泛实现的算法选择如下:  公钥密码系统:RSA、Diffie-Hellman、DSA 及 Fortezza;  对称密钥系统:RC2、RC4、IDEA、DES、Triple DES 及 AES;  单向散列函数:MD5 及 SHA。 TLS 的记录层(Record layer)用于封装更高层的 HTTP 等协议。记录层数据可以被随意压缩、 加密,与消息验证码(MAC)打包在一起。每个记录层包都有一个 content_type 段用以记录更上层 3 实际上它就是服务接口 用的协议。 当一个连接被发起时,从客户端的角度看,要收发几个握手信号: 1. 发送一个 ClientHello 消息,说明它支持的密码算法列表、压缩方法及最高协议版本,也发 送稍后将被使用的随机字节。 2. 然后收到一个 ServerHello 消息,包含服务器选择的连接参数,源自客户端初期所提供的 ClientHello。 3. 当双方知道了连接参数,客户端与服务器交换证书(依靠被选择的公钥系统)。这些证书通 常基于 X.509,不过已有草案支持以 OpenPGP 为基础的证书。 4. 服务器能够请求得到来自客户端的证书,所以连接可以是相互的身份认证。 5. 客户端与服务器通过加密通道协商一个共同的“主密钥”,这通过精心谨慎设计的伪随机 数函数实现。结果可能使用 Diffie-Hellman 交换,或简单的公钥加密,双方各自用私钥解密。 所有其他关键数据的加密均使用这个“主密钥”。 TLS/SSL 有多样的安全保护措施。所有的记录层数据均被编号,序号用在消息验证码(MAC)中。 3.2 公开密钥加密、证书与 CA 的概念 公开密钥加密也称为非对称密钥加密,该加密算法使用两个不同的密钥:公开密钥和私有密钥。 这两个密钥是数学相关的,用某用户私钥加密后所得的信息只能用该用户的公钥才能解密,反之亦 然。RSA 算法(由发明者 Rivest,Shmir 和 Adleman 姓氏首字母缩写而来)是最著名的公开密钥加 密算法。非对称密钥加密的另一用途是身份验证:用私钥加密的信息,只能用公钥对其解密,接收者 由此可知这条信息确实来自于拥有私钥的某人。 顾名思义,公开密钥是处于公共域的,它通常被发布出去。公钥发布的形式就是证书,其中除了 包含公钥之外,还包含了身份信息,以及 CA 的签名。证书的传输格式标准一般使用 X509 证书格式。 反之,私钥是被保护存储的,可以使用硬件或者普通的文件来存储私钥。一般使用 PKCS#12 格式将 它和公钥加密存储在普通的文件中。在 Java 平台,一般也使用 JKS 格式。 因为证书中包含了身份信息,因为在商务应用中,交易双方会对证书进行验证。方法是查看 CA 签名。CA 是指身份认证机构,是被交易双方都信任的第三方实体。任何人可以将自己的公钥和身份信 息提交给 CA 进行认证,由 CA 对身份信息进行确实,然后签名发证。CA 实际上也是使用非对称加密, 同样拥有私钥和公钥。CA 的公钥通常已经被内置到 IE 或者 Netscape 这样的软件,它使用自己的私 钥对其它人的证书签名。理论上任何人都可以成为 CA,为其它人的证书进行签名认证。但是,因为要 求 CA 要被交易双方所信任,双方都必须包含有 CA 的信任记录(即证书),所以在 Internet 范围内 的应用通常会选择 Verisign 等内置于 IE 或者 Netscape 的 CA 机构,当然,要交一笔不菲的美金。证 书与 CA 的关系可以参照公民与公安部的关系。公民之间通过身份证相互认证,而公安部作为公民都 信任的第三方签发携带公民身份信息的身份证。在前文 SSL 的概述中,服务器与客户端双方就是通过 交换被 CA 签名的证书来验证对方的身份。 3.3 实践数字证书生成 Java 5 集成了对 SSL 的支持,而且提供了一个名为 keytool.exe 的命令行工具来管理证书与 CA 签名。keytool.exe 位于 JRE 的 bin 文件夹下。本文使用%JAVA_HOME%\bin\keytool 来指代它。Java 5 内置了一些信任的 CA 证书,它们位于%JAVA_HOME% \lib\security\cacerts4文件内,可以使用 keytool 进行管理。下面是 keytool 的一些命令行参数: -genkey 生成新的密钥和公钥并保存(以下是它的参数) -alias 在 keystore 中的名字,一个 keystore 可以存储多个密钥,每个密 钥都有不同的名字,可以使用-alias 引用 -keyalg 密钥的生成算法,可以是 RSA 或者 DSA -keysize 密钥长度,512 位长的 RSA 密钥已经被破解,所以推荐个人使用 1024 位,CA 使用 2048 位 -sigalg 签名算法,可以使用 SHA1 或者 MD5 -dname 身份信息,X500 格式 -validity 有效时间,以天为单位 -keypass 当前操作的密钥的密码,以防止未授权访问。注意,这个密码和 storepass 是不一样的,后者是保护整个存储文件的密码。由于很多 客户端认为两个密码是一样的,如果设置了不同的密码可能会发生 错误 -keystore 密钥的存储文件,如前所述,该存储文件将保存用户的公钥和私钥, 也可以保存用户信任的证书(包括 CA 证书和普通证书)。前者称 为 keyEntry,后者称为 trustedCertEntry。此外,可以使用两种格 式存储该文件,分别是 JKS 和 PKCS#125。该文件受密码保护。 -storepass keystore 的密码。参见-keypass -storetype 存储文件的格式,可以是 jks 或者 pkcs12。参见-keystore -certreq 生成 CA 签名请求(以下是它的参数) -file 签名请求存储文件名 -alias -sigalg -keypass -keystore -storepass -storetype 参见-genkey -delete 从存储文件中删除一个记录(以下是它的参数) -alias -keystore -storepass -storetype 参见-genkey -export 将密钥导出(以下是它的参数) -rfc 以 RFC1421 标准规定的格式导出密钥 -alias -file -keystore 参见-genkey 4 如果安装的是 JDK,则对应%JAVA_HOME%\jre\lib\security\cacerts 5 支持的格式实际上取决于 JDK 或者当前已安装的支持软件包 -storepass -storetype -import 将密钥导入存储文件(以下是它的参数) -noprompt 不提示是否信任证书 -trustcacerts 导入证书时考虑其它证书,包括前文所述的 JDK 内置的包含信任 CA 的证书存储文件 -alias -file -keypass -keystore -storepass -storetype 参见-genkey -list 列出密钥存储文件中的密钥(以下是它的参数) -rfc -alias -keystore -storepass -storetype 参见-genkey -printcert 打印证书文件信息(以下是它的参数) -file 参见-delete -selfcert 自签名(以下是它的参数) -alias -dname -validity -keypass -sigalg -keystore -storepass -storetype 参见-genkey keytool 本身虽然可以管理密钥和证书,也能够完全自签名,可以满足普通的应用,但是如果需 要根据 PKI 规范的要求建立证书链,就需要用到另一个工具 OpenSSL。OpenSSL 的命令行比较复杂。 首先从 OpenSSL 的官方网站下载 OpenSSL 的编译版本6。下载安装后可以看到一个名为 openssl.exe 的可执行文件,它不需要其它动态链接库,可以将其拷贝到%SystemRoot%,即 (C:\Windows\system32\)下。以下是它常用的几条子命令: genrsa 使用 RSA 算法生成密钥(以下是它的参数) -in 密钥输入的文件名 -out 密钥输出的文件名 req 管理 CA 签名请求(以下是它的参数) -new 生成 CA 签名请求 -out 证书文件输出文件名 -key 输入的密钥文件 x509 管理 x509 证书文件(以下是它的参数) -req 对 CA 签名请求进行签名 -in 输入 CA 签名请求文件 -out 签名后的证书文件 -signkey 自签名使用的密钥文件 TODO 在 CA 签名中的作用待了 解 -days 有效期 -CAserial 保存 CA 签名序列号文件 -CAcreateserial 如果没有,创建 CA 签名序列号 -CAkey CA 私钥文件 -CACA 证书 -sha1 使用 SHA1 散列算法 -trustout TODO 待了解 pkcs12 使用 PKCS#12 格式管理存储文件(以下是它的参数) -export 导出 PKCS#12 格式的存储文件 -in 证书文件或者存储文件 -clcerts 仅导出客户端证书 -inkey 私钥文件 -password 保护存储文件的密码 -out 存储文件或者证书、密钥 -nodes 不加密私钥 -nokeys 不包含私钥 rsa 管理 RSA 密钥 根据前文的论述,应当首先生成 CA 证书,然后分别生成服务器与客户端的证书,最后用 CA 证 书对它们进行签名。CA 证书可以被导入服务器与客户端的信任列表中。 3.3.1 生成 CA 证书 首先利用 RSA 算法生成密钥(包含公钥和私钥)。命令如下: D:\Goldfish\workon>openssl genrsa -out __zt_cakey.pem 2048 Openssl 生成密钥的子命令是 genrsa,利用-out 选项指定输出为__zt_cakey.pem 文件。注意,要 指定密钥强度为 2048 位。目前 512 位的 RSA 算法已经可以暴力破解,普通用户应该使用 1024 位的 密钥,而 CA 则最好使用 2048 位强度的密钥 接着我们将对 CA 密钥进行自签名。自签名的 CA 证书称为根 CA 证书。根 CA 证书被布署到支持 SSH 的节点,以便互相验证身份。下面这条命令将生成 CA 签名请求: D:\Goldfish\workon>openssl req -new -out __zt_careq.csr -key __zt_cakey.pem -subj 6 大多数 Linux 一般已经预安装 OpenSSL,试着运行输入 openssl 命令看看 /C=CN/CN=ca/L=Xiamen/O=ffcs/ST=Fujian/EMAILADDRESS=nobody@nowhere.com/OU=DD openssl 的子命令 req 专门用于生成 CA 签名请求。使用-out 选项指定 CA 签名请求保存到 __zt_careq.csr 文件;-key 选项指定密钥文件;-subj 用于将身份信息附加到 CA 签名请求上。注意参 数的格式。这种格式称为 X500 格式,它是一些键值对,关键字是一些有特殊意义的字符,如: C Country,国家 CN Common Name,名字 L Locality Name,城市名 O Organization Name,公司名 OU Organizational Unit Name,部门 ST State Name,省份 EMAILADDRESS Email Address,电子邮箱 此外 openssl 要求将键值对用“/”连接起来 接着使用 CA 密钥对请求进行自签名 D:\Goldfish\workon>openssl x509 -req -in __zt_careq.csr -out __zt_cacert.pem -signkey __zt_cakey.pem -days 1095 x509 是签名后证书存储的标准格式,因此 openssl 的 x509 子命令相当于一个 MiniCA,可以完 成 CA 的大多数功能。-req 选项指对 CA 签名请求进行回复。分别使用-in 和-out 选项指定输入和输出文 件。注意,输入应当是一个 CA 签名请求,而输出是一个 X509 格式的证书存储文件。-signkey 指定 CA 私钥的存储文件。-days 指定有效期,这里是三年,最好还是多一些,不然三年后 CA 证书失效将 需要布署新的 CA 证书及所有客户端证书。 最后一步是将 X509 格式的自签名证书及 CA 密钥导入 PKCS#12 格式的证书存储文件中。 D:\Goldfish\workon>openssl pkcs12 -export -clcerts -in __zt_cacert.pem -inkey __zt_cakey.pem -out ca.pfx -passout pass:123456 pkcs12 是 openssl 专门用于管理 PKCS#12 文件的子命令。-export 选项说明了将从其它文件导 出生成 PKCS#12 文件;-clcerts 选项指示只导出客户端证书;-in 和-out 选项分别指定输入和输出文 件;-inkey 指定 CA 的私钥;-passout 选项指定输出文件的密码。注意密码格式,是一个“pass:”加 上真正的密码。其它格式请参见 openssl 的使用手册。 现在,一个名为 ca.pfx 的 CA 证书已经生成完毕。为了保证安全性,将 ca.pfx 文件复制到一个安 全的地方,并将产生的形如“__zt_*.*”的临时文件全部删除7。在本例中采用了一个很简单的密码 “123456”,在生产环境中,CA 的密码一定要慎重选择。 3.3.2 生成服务器证书并对其签名 生成服务器证书相对麻烦些,需要使用 openssl 和 keytool 两种工具。 首先要从 CA 密钥的存储文件 ca.pfx 中生成 CA 的私钥及自签名证书。命令如下: D:\Goldfish\workon>openssl pkcs12 -in ca.pfx -clcerts -nodes -nokeys -out __zt_cacert.pem.1 -passin pass:123456 D:\Goldfish\workon>openssl pkcs12 -in ca.pfx -clcerts -nodes -out __zt_cafile.pem -passin pass:123456 D:\Goldfish\workon>openssl rsa -in __zt_cafile.pem -out __zt_cakey.pem 前两条命令与上面生成 CA 证书时类似,不同的是使用了-nodes 和-nokeys 两个选项,分别指示 7 接下来的例子产生的__zt_*.*文件都是临时文件 了不对输出的信息进行加密以及不输出私钥。-passin 选项与-passout 选项类似,用于指定输入文件 的密码。第三条命令用于生成密钥8。 使用 openssl 生成的 CA 自签名证书文件__zt_cacert.pem.1 虽然也是 rfc 格式,但是头部包含四 行身份信息。接下来的操作需要的 keytool.exe 不能识别这种格式。所以我们需要手动将那四行身份信 息删除。比如 删除头部四行身份信息之后: 将删除四行身份信息的文件保存为__zt_cacert.pem 接着使用 keytool.exe 生成服务器证书存储文件。 D:\Goldfish\workon>keytool -genkey -alias mykey -keyalg rsa -keysize 1024 -validity 8 TODO,欢迎注解,不大明白这些文件格式之间的转换关系 1095 -storepass 123456 -keystore server.store 这条命令将显示一个向导,在按提示输入身份信息后生成 server.store 文件。其中包含了一个密 钥,名为“mykey”。如前所述,这里使用了-keysize 选项指定了密钥强度是 1024 位。 注意你填写的身份信息。“名字与姓氏”实际上是 X500 中的 Common Name。对于一个网站, 就是它的域名。如果不正确填写它的名字,在某些应用中将会失败。 接着生成一个 CA 签名请求: D:\Goldfish\workon>keytool -certreq -alias mykey -sigalg MD5withRSA -file __zt_myreq.csr -keystore server.store -storepass 123456 子命令-certreq 用于生成 CA 签名请求。-alias 指定在证书存储文件中的密钥的名字;-sigalg 指 定了签名算法,一般指定为 MD5withRSA,这是 openssl 的默认值;-file 指定生成的签名请求的文 件名。 接下来就是使用 openssl 来回复 CA 签名请求了。命令如下: D:\Goldfish\workon>openssl x509 -req -in __zt_myreq.csr -out __zt_mycert.pem -CA __zt_cacert.pem -CAkey __zt_cakey.pem -days 1095 -CAserial ca-cert.srl -sha1 – trustout -CAcreateserial 与前面自签名不同,这里分别用-CA 及-CAkey 指定了 CA 的证书文件以及 CA 私钥。这里实际上 是创建了一条证书链,这条证书链包含了两个证书——使用 CA 私钥加密过的服务器证书以及未加密 的 CA 证书。-CAserial 指定了一个保存 CA 签名的序列文件,由于还指定了-CAcreateserial,当没有 这个文件的时候将会自动创建它。 接下来将 CA 证书导入到 server.store 作为信任的 CA 证书: D:\Goldfish\workon>keytool -import -alias __zt_caroot -keystore server.store – storepass 123456 -file __zt_cacert.pem 最后,将签名回复安装到 server.store 中。 D:\Goldfish\workon>keytool -import -alias mykey -keystore server.store -storepass 123456 -file __zt_mycert.pem 如果喜欢尝试,可以发现如果没有前一个步骤,将不能安装 CA 签名后的服务器证书。因为 keytool 不能在证书链中找到信任的 CA 证书。 至此,服务器证书生成并使用 CA 签名成功。 3.3.3 生成客户端证书并对其签名 与生成服务器证书完全类似,唯一不同的是身份信息与存储文件名字——client.store。不再重 复。 现在,当前工作目录下可以找到 ca.pfx、client.store 和 server.store 三个文件。其中 ca.pfx 是 PKCS#12 格式的 CA 密钥存储文件,client.store 和 server.store 是经过 CA 签名的 JKS 格式的密钥 存储文件,密钥在两者中都名为 mykey。 为了简化操作过程,本文提供了一个 Linux 下的脚本程序,可以方便地完成以上过程。 3.4 配置 Tomcat 的 SSL 支持 使 Tomcat 是很简单的事,只需要对%CATALINA_HOME%\conf\server.xml 进行简单的配置。 在元素里添加一个子元素,内容如下: 其中几个属性与 SSL 支持有关,它们的作用描述如下: scheme 指定为 https secure 指定为 true clientAuth 是否认证客户端证书,如果为 true 会要求客户端提交它的证书,本 例中是双向认证,所以设为 true sslProtocol 指定为 TLS keystoreFile 存储服务器密钥的存储文件 keystorePass 存储服务器密钥的存储文件密码 keystoreType 存储服务器密钥的存储文件的文件格式,可以为 jks,也可以为 pkcs12 keyAlias 服务器密钥在存储文件中的名字。使用 keymgr.py 工具生成的密钥存 储文件默认是 mykey truststoreFile 存储信任的证书文件的存储文件 truststorePass 存储信任的证书文件的存储文件密码 truststoreType 存储信任的证书文件的存储文件的文件格式。jks 和 pkcs12 之一 port 服务端口,默认的 https 端口号是 443 重启 Tomcat 之后可以 Web Service 即可生效。可以使用浏览器进行测试。先把 clientAuth 改为 false,在浏览器的地址栏里输入 https://server/cxf/UserService?wsdl,如果可以显示 WSDL 文档, 说明服务器的证书可以工作。再将 clientAuth 改成 true。再次打开 https://server/cxf/UserService? wsdl,如果浏览器提示选择一个证书以继续浏览,说明 SSL 支持已经配置成功。 在实际的生产环境中还应去除原有的不安全连接方法。方法是在该 Web 应用程序的 web.xml 文 件中增加以下配置: Protected Context /* CONFIDENTIAL 3.5 为 Web Service 客户端启用 SSL 支持 虽然 CXF 声称可以通过简单的配置支持 SSL 客户端,但是根据其用户手册操作时却碰到问题。 本文利用 CXF 提供的辅助类,在客户端向服务器发起 SSL 连接之前,将客户端证书与 CA 证书的存 储文件配置设置到 CXF 内部。代码如下: UserServiceFactory.java: import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.UnrecoverableKeyException; import java.security.cert.CertificateException; import javax.net.ssl.KeyManager; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; import javax.xml.namespace.QName; import org.apache.cxf.configuration.jsse.TLSClientParameters; import org.apache.cxf.endpoint.Client; import org.apache.cxf.frontend.ClientProxy; import org.apache.cxf.transport.http.HTTPConduit; import org.apache.cxf.transports.http.configuration.HTTPClientPolicy; import demo.cxf.User; import demo.cxf.UserService; import demo.cxf.UserServiceService; public class UserServiceFactory { private final static String keyStore = "client.store"; private final static String trustStore = "client.store"; private final static String trustStorePass = "123456"; private final static String keyStorePass = "123456"; private final static QName SERVICE = new QName("http://cxf.demo/", "UserServiceService"); private static UserService us; /** * 取得信任证书管理器 * @return * @throws IOException */ private static TrustManager[] getTrustManagers() throws IOException { try { String alg = TrustManagerFactory.getDefaultAlgorithm(); TrustManagerFactory factory = TrustManagerFactory.getInstance(alg); InputStream fp = UserServiceFactory.class.getResourceAsStream(trustStore); KeyStore ks = KeyStore.getInstance("JKS"); ks.load(fp, trustStorePass.toCharArray()); fp.close(); factory.init(ks); TrustManager[] tms = factory.getTrustManagers(); return tms; } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } catch (KeyStoreException e) { e.printStackTrace(); } catch (CertificateException e) { e.printStackTrace(); } return null; } /** * 取得个人证书管理器 * @return * @throws IOException */ private static KeyManager[] getKeyManagers() throws IOException { try { String alg = KeyManagerFactory.getDefaultAlgorithm(); KeyManagerFactory factory = KeyManagerFactory.getInstance(alg); InputStream fp =UserServiceFactory.class.getResourceAsStream(keyStore); KeyStore ks = KeyStore.getInstance("JKS"); ks.load(fp, keyStorePass.toCharArray()); fp.close(); factory.init(ks, keyStorePass.toCharArray()); KeyManager[] keyms = factory.getKeyManagers(); return keyms; } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } catch (KeyStoreException e) { e.printStackTrace(); } catch (CertificateException e) { e.printStackTrace(); } catch (UnrecoverableKeyException e) { e.printStackTrace(); } return null; } static { UserServiceService service = null; try { service = new UserServiceService(new URL("file:D:\\ws\\UserServiceService.wsdl"), SERVICE); } catch (MalformedURLException e) { e.printStackTrace(); } us = service.getUserServicePort(); Client client = ClientProxy.getClient(us); HTTPConduit httpConduit = (HTTPConduit) client.getConduit(); TLSClientParameters tlsParams = httpConduit.getTlsClientParameters(); if (tlsParams == null) tlsParams = new TLSClientParameters(); tlsParams.setSecureSocketProtocol("SSL"); try { tlsParams.setKeyManagers(getKeyManagers()); tlsParams.setTrustManagers(getTrustManagers()); } catch (IOException e) { e.printStackTrace(); } httpConduit.setTlsClientParameters(tlsParams); } public static UserService getInstance(){ return us; } } 这些代码一目了解,不再进行深入的解释。在实际使用中只需酌情修改存储文件的位置、密码、服 务名、与 WSDL 文件地址及由 WSDL 文档生成的接口或者类名。作为示例,要把 client.store 放到$ {CLASSPATH}下,最简单的就是放到与 UserServiceFactory 相同的位置。 使用了上述工厂模式之后,可以写出这样的测试代码: UserService us=UserServiceFactory.getInstance(); User u=new User(); u.setUsername("fish"); us.createUser(u); try { us.deleteUser("fish"); } catch (NotFoundException_Exception e) { e.printStackTrace(); } 上述代码将会在服务器端打印出 createUser fish 并且在客户端得到一个 NotFoundException_Exception 异常 3.6 吊销客户端证书 在生产环境中,不可避免地要碰到与客户中止合作,并取消其访问权限的情况。这通常表现为服 务器端显式拒绝客户端的连接请求。基本思路是设置一个包含所有已注销客户的黑名单。但 J2EE 与 CXF 并没有提供对证书黑名单的支持。这时,我们需要使用 CXF 的插件功能,为 CXF 开发过滤插件。 在在之前,首先了解一下 CXF 的系统架构。简单地说,CXF 使用流水线型(或者说总线型)处理 机制,它的核心是一个 Bus。一个客户端的请求或者一个对客户端桩代码的调用被组织成为一个 Message。同时,所有的 CXF 功能都组织成 Interceptor 挂接在 Bus 上,分阶段依次处理 Message。Message 本质上是一个 Map 数据结构,既包含系统公共的也包含 Interceptor 自定义的数 据。 要提供证书黑名单功能,首先要取得 CXFServlet 提供的 HttpServletRequest 对象,从中取得 证书包含的身份信息,对其进行验证。代码如下: package demo.cxf; import java.security.cert.X509Certificate; import javax.security.auth.x500.X500Principal; import javax.servlet.http.HttpServletRequest; import org.apache.cxf.bus.CXFBusImpl; import org.apache.cxf.interceptor.Fault; import org.apache.cxf.message.Message; import org.apache.cxf.phase.AbstractPhaseInterceptor; import org.apache.cxf.phase.Phase; import org.apache.cxf.transport.http.AbstractHTTPDestination; public class SSLFilter extends AbstractPhaseInterceptor { public SSLFilter() { super(Phase.RECEIVE); } private CXFBusImpl bus; public CXFBusImpl getBus() { return bus; } public void setBus(CXFBusImpl bus) { this.bus = bus; } public void handleMessage(Message msg) throws Fault { HttpServletRequest request = (HttpServletRequest) msg.get(AbstractHTTPDestination.HTTP_REQUEST); if (request == null) return; String certSubject = null; X509Certificate[] certChain = (X509Certificate[]) request.getAttribute("javax.servlet.request.X509Certificate"); if (certChain == null) { System.out.println("no javax.servlet.request.X509Certificate instance"); } else { int len = certChain.length; if (len > 0) { X509Certificate cert = (X509Certificate) certChain[0]; X500Principal pSubject = cert.getSubjectX500Principal(); certSubject = pSubject.getName(); // 判断客户的名字是否出现在吊销列表中,如果是的话,抛出异常 if (certSubject.indexOf("client2") != -1) throw new Fault(new RuntimeException("fish is here!")); } System.out.println(certSubject); } } } 写完代码之后,要将这个 Interceptor 插接到 CXF,方法是修改 CXF 的配置文件 WEB- INF/beans.xml,如下(注意红色修改部分): 4 关于 CXF 的其它问题 4.1 基于 SSL 双向认证的系统否支持 ASP? 预计是可以。方法是首先将生成的证书及其 CA 导入客户端的系统。然后利用 Microsoft 提供的 ServerXMLRequest 组件向服务器发起加密的连接(参见 Microsoft MSDN ServerXMLHTTP 组件 文档的描述)。由于 Web Service 可以通过普通的 HTTP POST 访问,只要发送的 HTTP 请求中包含 调用请求的 SOAP 文档,就可以完成一个 Web Service 调用。代码如下(VBScript): Option Explicit Dim xmlhttp,soapbody soapdoby=GetSomeCallSoap() //利用 GetSomeCallSoap()取得一个调用的 SOAP 消息 set xmlhttp=CreateObject("MSXML2.ServerXMLHTTP.4.0") xmlhttp.setOption 3,"client2" //客户端证书名字 xmlhttp.open "get","https://server/cxf/UserService?wsdl" xmlhttp.send soapboxy MsgBox xmlhttp.responseText 值得注意的是,这种方案目前还没有见于文档的记载,是否真正可行目前还无法用实例判断。而 且使用裸 HTTP 协议访问 Web Service 是一件比较繁重的任务,需要对 WSDL 以及 SOAP 协议进行 广泛的调研。 4.2 keymgr.py 脚本 下载安装 Python2.5(http://www.python.org/ftp/python/2.5.1/python-2.5.1.msi),然后下载 OpenSSL,将 openssl.exe 与 keymgr.py 复制到 c:\windows\system32,同时保证 keytool.exe 能被 访问到。 4.3 在 Eclipse 中使用 CXF Eclipse 对 CXF 的支持是通过 SOA Tools Platform(STP)项目来实现的,所以首先安装 STP 的 插件包。它的主页是 http://www.eclipse.org/stp/。在下载的时候最好选择它的 All in One 包,其中包 含了它所依赖的其它插件。下载后安装到 eclipse 的安装目录。然后,我们还需要下载 CXF 的插件包: http://people.apache.org/~blin/incubator-cxf-2.0-M1-v4/repository/org/apache/cxf/cxf-eclipse- plugin/2.0-incubator-M1/cxf-eclipse-plugin-2.0-incubator-M1.zip。也可以从 CXF 的源码中构建, 参见 CXF 源码的说明文件。 安装完毕之后,运行 Eclipse 可以发现多了一个 SOA 菜单。现在还要做一个简单的配置。在 Preferences 对话框找到 SOA Tools->Installed Runtimes,点击 Add 按钮,设置好 CXF 的安装位置。 要使用 STP 来开发 Web Service 工程,首先新建一个 JAX-WS Java First Project,如下图: 接下来的步骤与 2 CXF 开发实践类似,首先开发服务。仍然以 demo.cxf.UserService 为例。在服 务代码编写完毕之后,为服务接口与服务实现类添加 JAX-WS 标注。STP 插件可以自动生成详细标注, 方法是在 Outline 视图上选取类或接口,然后在右键菜单上选择“JAX-WS Tools->Create Web Service”。因为这里只要添加“@WebService”,而且其向导功能清晰,不对它进行深入探讨。保存完 Java 文件,可以发现 STP 自动生成了 wsdl 文件。这个文件与前文所用 java2wsdl 文件是一样的。 STP 还提供了从 wsdl 文件生成 Java 代码的工具。方法是在 Package Explorer 视图中选择 wsdl 文件,并在右键菜单中选择“JAX-WS Tools->Generate Code”。此时弹出向导: 本例中已经有了服务接口与实现类,只需要生成客户端的调用桩代码,将“Generation Options”中的“Implementation”和“Server”两个选项去掉。点击“Finish”生成代码。这里有个 BUG,它的输出文件夹只能选 src,在有些工程的配置里,会覆盖原有的代码,所以,小心使用! 4.4 把 WSDL 文件嵌入到 Jar 文件中 客户端桩代码需要一个 URL 作为参数,Jar 文件中资源的 URL 可以通过以下方法取得(需要 Spring): ClassPathResource r; r=new ClassPathResource("META-INF/cxf/UserServiceService.wsdl"); System.out.println(r.getURL()); 4.5 与 Axis 的互操作——Axis 客户端代码 与 CXF 类似,开发基于 Axis 的 Web Service 客户端分为两个步骤。 4.5.1 使用 wsdl2java 生成客户端桩代码 Axis 的 wsdl2java 是一个名为 org.apache.axis.wsdl.WSDL2Java 的工具类,为了运行它,需 要设置${CLASSPATH}与${PATH}变量。命令行示例(脚本文件 axisclient.bat): java -classpath D:\Goldfish\develop\resource\axis-1_4\lib\axis.jar;D:\Goldfish\develop\resource\axis-1_4\lib\commons-logging- 1.0.4.jar;D:\Goldfish\develop\resource\axis-1_4\lib\commons-discovery-0.2.jar;D:\Goldfish\develop\resource\axis- 1_4\lib\jaxrpc.jar;D:\Goldfish\develop\resource\axis-1_4\lib\log4j-1.2.8.jar;D:\Goldfish\develop\resource\axis- 1_4\lib\saaj.jar;D:\Goldfish\develop\resource\axis-1_4\lib\wsdl4j-1.5.1.jar;D:\Goldfish\develop\resource\axis- 1_4\lib;D:\Goldfish\develop\resource\apache-cxf-2.0.1-incubator\lib\geronimo-activation_1.1_spec-1.0- M1.jar;D:\Goldfish\develop\resource\apache-cxf-2.0.1-incubator\lib\geronimo-annotation_1.0_spec- 1.1.jar;D:\Goldfish\develop\resource\apache-cxf-2.0.1-incubator\lib\geronimo-javamail_1.4_spec-1.0-M1.jar org.apache.axis.wsdl.WSDL2Java %1 先更改 UserServiceService.wsdl 内包含的服务地址并且备份 CXF 的文件。然后运行命令: axisclient.bat UserServiceService.wsdl 就会生成一个 demo 文件夹。新建一个 Java 工程,并把 demo 复制到源代码文件夹里。 4.5.2 创建 UserServiceFactory 工厂类 由于使用了 SSL 双向认证,需要在客户端配置 SSL 证书与信任证书。代码如下: import javax.xml.rpc.ServiceException; import demo.cxf.UserService; import demo.cxf.UserServiceService; import demo.cxf.UserServiceServiceLocator; public class UserServiceFactory { private static UserService us = null; public static UserService getInstance() { if (us == null) { System.setProperty("javax.net.ssl.keyStore", "D:\\Goldfish\\develop\\projects\\AxisClient\\bin\\client.store"); System.setProperty("javax.net.ssl.keyStorePassword", "123456"); System.setProperty("javax.net.ssl.trustStore", "D:\\Goldfish\\develop\\projects\\AxisClient\\bin\\client.store"); System.setProperty("javax.net.ssl.trustStorePassword", "123456"); UserServiceService uss = new UserServiceServiceLocator(); try { us = uss.getUserServicePort(); } catch (ServiceException e) { e.printStackTrace(); } } return us; } } 需要注意的是,Java 在创建 SSL Socket 连接时会自动在系统属性里查找密钥与信任证书存储 文件。与前文 CXF 的繁复配置相比,Axis 这个配置似乎就比较简单了。但是它有两个不足,一则不能 将证书存储文件打包到 jar 包内,二则影响系统的其它模块,可能会导致使用 SSL 的其它模块了连接 失败。 5 参考文档 使用 openssl 和 keytool 生成证书: http://debian.kanxue.net/2007/03/19/tomcat-ssl/ SSL 概念: http://zh.wikipedia.org/wiki/%E4%BC%A0%E8%BE%93%E5%B1%82%E5%AE %89%E5%85%A8 Apache-CXF 主页: http://incubator.apache.org/cxf/ Eclipse 与 CXF 集成的介绍: http://bldmickey.blog.sohu.com/61111724.html
还剩30页未读

继续阅读

下载pdf到电脑,查找使用更方便

pdf的实际排版效果,会与网站的显示效果略有不同!!

需要 10 金币 [ 分享pdf获得金币 ] 1 人已下载

下载pdf

pdf贡献者

mousefat

贡献于2012-11-07

下载需要 10 金币 [金币充值 ]
亲,您也可以通过 分享原创pdf 来获得金币奖励!
下载pdf