使用 Apache CXF

sdflysun 贡献于2013-04-14

作者 金色其泽  创建于2007-09-23 14:17:00   修改者Lenovo  修改于2011-03-08 09:58:00字数31212

文档摘要: 使用WebService在两个独立系统中交换信息是J2EE开发的常见任务。本文将讲述如何使用Apache CXF框架来开发WebService,并且利用Tomcat的SSL支持增进WebService的安全性1概述1.1WebService概念及CXF基本开发步骤。WebService本质上是RPC调用。以往的RPC采用二进制格式进行定义和调用,适合要求效率的同质系统。
关键词:

使用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类似,由四部分组成 客户程序 调用桩(或称桩过程) 服务桩 服务 调用 返回 在动态语言中,调用桩与服务桩将由语言本身产生,而在静态语言中,两者一般由工具生成 CXF自动生成服务桩 。众所周知,Java是一种静态语言。而CXF与Axis就是Java中常用的两种Web Service代码生成工具。 RPC除处理服务调用之外,还应该处理服务的定义,使客户程序知道调用服务的方式并且提交足够的参数。在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); } } 几个小的注意事项: l 接口最好不包含内嵌类或者枚举类型,CXF不能识别它们 l 接口抛出的异常也将被CXF识别并导出 2.3添加JAX-WS标注到Java代码中 在设计好逻辑层之后,就可以动手为逻辑层添加JAX-WS标注,以便使用CXF的代码生成工具将Java代码导出为WSDL定义。 TODO 介绍JAX-WS JAX-WS定义了很多的标注,可以对导出的服务接口进行定义。其中较常用的是 l @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 l @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文档时将该方法排除在外 l @SOAPBinding,可选的标注,用于指定生成的SOAP定义文档风格。关于此标注再详细的信息请查阅SOAP标准等参考资料 style Style.DOCUMENT (默认) Style.RPC SOAP消息风格 use Use.LITERAL (默认) Use.ENCODED SOAP数据编码方式 parameterStyle 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. l @RequestWrapper,可选的标注,用于指定如何包装客户端调用服务方法使用的参数 l @ResponseWrapper,可选的标注,用于指定如何包装客户端调用服务方法的返回值 l @WebFault,可选的标注,用于注解服务接口方法抛出的异常 name 异常的名字 targetNamespace 对应的名域,默认是服务接口的名域 faultName 实现该异类的类名 l @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. l @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服务器 还支持JMS和同一JVM内的通信 。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”三个类、方法名。规则是服务接口名对应第一个 实际上它就是服务接口 ,而后两个由服务接口名分别加上“-Service”“-Port”构成。 3 为CXF启用SSL双向认证 3.1 SSL概述 安全套接层(SSL)及其新继任者传输层安全(TLS)是在互联网上提供保密安全通道的加密协议,为诸如网站、电子邮件、网上传真等等数据传输进行保密。TLS利用密钥算法在互联网上提供端点身份认证与通讯保密。在典型例子中,只有服务器被可靠身份认证(即其验证被确保),客户端踪迹不一定经可靠认证;相互间的身份认证需要公钥基础设施(PKI)设置于客户端中。协议的设计在某种程度上能够使客户端/服务器应用程序通讯本身预防窃听、干扰(Tampering)、和消息伪造。 TLS包含三个基本阶段: 1. 对等协商密钥算法支持 2. 基于公钥密码的密钥交换和基于证书的身份认证 3. 基于对称密钥的数据传输保密 在第一阶段,客户端与服务器协商所用密码算法。 当前广泛实现的算法选择如下: l 公钥密码系统:RSA、Diffie-Hellman、DSA及Fortezza; l 对称密钥系统:RC2、RC4、IDEA、DES、Triple DES及AES; l 单向散列函数:MD5及SHA。 TLS的记录层(Record layer)用于封装更高层的HTTP等协议。记录层数据可以被随意压缩、加密,与消息验证码(MAC)打包在一起。每个记录层包都有一个content_type段用以记录更上层用的协议。 当一个连接被发起时,从客户端的角度看,要收发几个握手信号: 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\cacerts 如果安装的是JDK,则对应%JAVA_HOME%\jre\lib\security\cacerts 文件内,可以使用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#12 支持的格式实际上取决于JDK或者当前已安装的支持软件包 。该文件受密码保护。 -storepass keystore的密码。参见-keypass -storetype 存储文件的格式,可以是jks或者pkcs12。参见-keystore -certreq 生成CA签名请求(以下是它的参数) -file 签名请求存储文件名 -alias -sigalg -keypass -keystore -storepass -storetype 参见-genkey -delete 从存储文件中删除一个记录(以下是它的参数) -alias -keystore 参见-genkey -storepass -storetype -export 将密钥导出(以下是它的参数) -rfc 以RFC1421标准规定的格式导出密钥 -alias -file -keystore -storepass -storetype 参见-genkey -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 参见-genkey -storetype keytool本身虽然可以管理密钥和证书,也能够完全自签名,可以满足普通的应用,但是如果需要根据PKI规范的要求建立证书链,就需要用到另一个工具OpenSSL。OpenSSL的命令行比较复杂。首先从OpenSSL的官方网站下载OpenSSL的编译版本 大多数Linux一般已经预安装OpenSSL,试着运行输入openssl命令看看 。下载安装后可以看到一个名为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私钥文件 -CA CA证书 -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 /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_*.*”的临时文件全部删除 接下来的例子产生的__zt_*.*文件都是临时文件 。在本例中采用了一个很简单的密码“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两个选项,分别指示了不对输出的信息进行加密以及不输出私钥。-passin选项与-passout选项类似,用于指定输入文件的密码。第三条命令用于生成密钥 TODO,欢迎注解,不大明白这些文件格式之间的转换关系 。 使用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 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

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

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

需要 8 金币 [ 分享文档获得金币 ] 1 人已下载

下载文档