使用 Spring4 + CXF3 + WS-Security 开发 WebService

来源:互联网 发布:golang test用法 编辑:程序博客网 时间:2024/05/20 02:54

说明

目前运维的平台需要和其他类型相似的平台进行数据对接,由于数据量不大,基本上不存在对接时的性能问题,所以选择使用 WebService 进行数据对接。综合来看,Spring + CXF 的技术实现方式是现在的主流,有较多的技术支持,因此选择这种方式来开发 WebService。

(一)开发环境

选用 JDK1.7 + Maven3.3.9 + Spring4.3.10 + CXF3.1.12 + Tomcat7.0 来进行开发。

(二) WebService 服务端开发

1. 新建 Maven 项目,添加 Maven 依赖

<dependency>  <groupId>junit</groupId>  <artifactId>junit</artifactId>  <version>4.12</version></dependency><!--cxf--><dependency>  <groupId>org.apache.cxf</groupId>  <artifactId>cxf-rt-frontend-jaxws</artifactId>  <version>3.1.12</version></dependency><dependency>  <groupId>org.apache.cxf</groupId>  <artifactId>cxf-rt-transports-http</artifactId>  <version>3.1.12</version></dependency><dependency>  <groupId>org.apache.cxf</groupId>  <artifactId>cxf-rt-ws-security</artifactId>  <version>3.1.12</version></dependency><!--spring--><dependency>  <groupId>org.springframework</groupId>  <artifactId>spring-context</artifactId>  <version>4.3.10.RELEASE</version></dependency><dependency>  <groupId>org.springframework</groupId>  <artifactId>spring-web</artifactId>  <version>4.3.10.RELEASE</version></dependency>

2. 创建一个 JAX-WS 注解的 Service类
定义一个接口及其实现类

package demo.ws.soap_spring_cxf;import javax.jws.WebParam;import javax.jws.WebService;@WebServicepublic interface HelloService {    String say(@WebParam(name = "name") String name);}
package demo.ws.soap_spring_cxf;import org.springframework.stereotype.Component;import javax.jws.WebService;@WebService@Componentpublic class HelloServiceImpl implements HelloService {    public String say(String name) {        return "hello " + name;    }}

定义好 WebService 服务类,再进行适当的配置,就可以准备发布服务了。

3. 配置 CXF 与 Spring
在发布服务之前,我们需要在 web.xml 中配置监听器,加载 Spring 配置文件;添加 CXF Servlet,处理请求

<?xml version="1.0" encoding="UTF-8"?><web-app xmlns="http://java.sun.com/xml/ns/javaee"          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"          xsi:schemaLocation="http://java.sun.com/xml/ns/javaee           http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" version="3.0">  <display-name>webserviceDemo</display-name>  <!--spring-->  <context-param>    <param-name>contextConfigLocation</param-name>    <param-value>classpath:spring.xml</param-value>  </context-param>  <listener>    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>   <!--cxf-->  <servlet>    <servlet-name>cxf</servlet-name>    <servlet-class>org.apache.cxf.transport.servlet.CXFServlet</servlet-class>  </servlet>  <servlet-mapping>    <servlet-name>cxf</servlet-name>    <url-pattern>/ws/*</url-pattern>  </servlet-mapping></web-app>

spring.xml 中<context:component-scan>配置需要扫描的包,同时使用 <import> 标签引入其他配置文件

<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans"       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"       xmlns:context="http://www.springframework.org/schema/context"       xsi:schemaLocation="http://www.springframework.org/schema/beans       http://www.springframework.org/schema/beans/spring-beans.xsd        http://www.springframework.org/schema/context        http://www.springframework.org/schema/context/spring-context.xsd">    <context:component-scan base-package="demo.ws"/>    <import resource="spring-cxf.xml"/>    <import resource="spring-beans.xml"/></beans>

spring-cxf.xml 用于发布服务,spring-beans.xml 用于配置验签信息

4. 使用 CXF’s XML 发布服务
使用 XML 发布服务,在 spring-cxf.xml 中添加代码

<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans"       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"       xmlns:cxf="http://cxf.apache.org/core"       xmlns:jaxws="http://cxf.apache.org/jaxws"       xsi:schemaLocation="http://www.springframework.org/schema/beans       http://www.springframework.org/schema/beans/spring-beans.xsd        http://cxf.apache.org/jaxws http://cxf.apache.org/schemas/jaxws.xsd        http://cxf.apache.org/core http://cxf.apache.org/schemas/core.xsd">    <!--发布服务-->    <jaxws:endpoint id="helloService" implementor="demo.ws.soap_spring_cxf.HelloServiceImpl" address="/soap/hello">    </jaxws:endpoint>    <cxf:bus>        <cxf:features>            <cxf:logging/>        </cxf:features>    </cxf:bus></beans>

<cxf:logging> 标签用于开启服务日志。启动服务后,访问 http://localhost:8080/ws/soap/hello?wsdl ,显示如下信息即表示服务发布成功。

<wsdl:definitions xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:tns="http://soap_spring_cxf.ws.demo/" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:ns1="http://schemas.xmlsoap.org/soap/http" name="HelloServiceImplService" targetNamespace="http://soap_spring_cxf.ws.demo/">    ...  <wsdl:message name="say">     <wsdl:part element="tns:say" name="parameters"/>   </wsdl:message>    <wsdl:message name="sayResponse">     <wsdl:part element="tns:sayResponse" name="parameters"/>   </wsdl:message>    <wsdl:portType name="HelloService">     <wsdl:operation name="say">       <wsdl:input message="tns:say" name="say"/>        <wsdl:output message="tns:sayResponse" name="sayResponse"/>     </wsdl:operation>   </wsdl:portType>    <wsdl:binding name="HelloServiceImplServiceSoapBinding" type="tns:HelloService">     <soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>      <wsdl:operation name="say">       <soap:operation soapAction="" style="document"/>        <wsdl:input name="say">         <soap:body use="literal"/>       </wsdl:input>        <wsdl:output name="sayResponse">         <soap:body use="literal"/>       </wsdl:output>     </wsdl:operation>   </wsdl:binding>    <wsdl:service name="HelloServiceImplService">     <wsdl:port binding="tns:HelloServiceImplServiceSoapBinding" name="HelloServiceImplPort">       <soap:address location="http://localhost:8080/ws/soap/hello"/>     </wsdl:port>   </wsdl:service> </wsdl:definitions>

(三)完善 WebService ,使用签名和加密

在数据传输过程中,往往会有加密的需求,这里使用 WS-Security 来实现签名和加密,它提供了 WSS4J interceptors 。而在 CXF2.2 之后可以使用 WS-SecurityPolicy,这种方式更简单也更标准。

1. 生成公钥和私钥
在配置拦截器之前,需要生成公钥和私钥,用于签名加密以及验签解密。CXF 官方文档中使用 X.509 证书来生成公钥和私钥,但是官方文档提示说这种方式不适合生产环境。

keytool -genkey -alias ciecc -keypass cieccPassword -keyalg RSA -keysize 1024 -validity 3650 -keystore privatestore.jks -storepass keyStorePassword -dname "cn=ciecc" keytool -selfcert -alias ciecc -keystore privatestore.jks -storepass keyStorePassword -keypass cieccPasswordkeytool -importkeystore -alias ciecc -deststorepass keyStorePassword -destkeypass cieccPassword -destkeystore publicstore.jks -srckeystore privatestore.jks -srcstorepass keyStorePassword

keytool 是 JDK 自带的命令,ciecc 是私钥的别名,cieccPassword 是私钥的密码,keyStorePassword 是秘钥库的密码。keytool 具体使用方法可以自行查找。使用以上命令会在当前目录下生成 publicstore.jks 和 privatestore.jks 两个文件,公钥和私钥分别保存在这两个文件中。

2. 配置拦截器(Interceptor)
对于服务端来说,需要在 spring-cxf.xml 中配置 WSS4JInInterceptor,对接收到的信息进行验签并解密。对于客户端的话,配置 WSS4JOutInterceptor,对要发送的请求进行签名和加密。这里是从服务端的角度来配置的,客户端的配置会在后面讲到。

<bean id="wss4jInInterceptor" class="org.apache.cxf.ws.security.wss4j.WSS4JInInterceptor">    <constructor-arg>        <map>            <entry key="action" value="Signature Encrypt"/>            <!-- 提供公钥的密码 -->            <entry key="passwordCallbackRef" value-ref="serverPasswordCallback"/>            <entry key="signatureVerificationPropFile" value="server.properties"/>            <entry key="decryptionPropFile" value="server.properties"/>        </map>    </constructor-arg></bean>

其中 signatureVerificationPropFile 和 decryptionPropFile 属性的配置信息都来自 server.properties 文件,这个配置文件中的内容如下:

org.apache.wss4j.crypto.provider=org.apache.wss4j.common.crypto.Merlinorg.apache.wss4j.crypto.merlin.keystore.type=jksorg.apache.wss4j.crypto.merlin.keystore.password=keyStorePasswordorg.apache.wss4j.crypto.merlin.keystore.file=publicstore.jks

而 passwordCallbackRef 这个 bean 根据签名用户提供对应的密钥密码,代码如下:

package demo.ws.soap_spring_cxf_wss4j;import org.apache.wss4j.common.ext.WSPasswordCallback;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Component;import javax.security.auth.callback.Callback;import javax.security.auth.callback.CallbackHandler;import javax.security.auth.callback.UnsupportedCallbackException;import java.io.IOException;@Componentpublic class ServerPasswordCallback implements CallbackHandler {    @Autowired    private SignatureUser user;    public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {        WSPasswordCallback callback = (WSPasswordCallback) callbacks[0];        String clientUsername = callback.getIdentifier();        String serverPassword = user.getUserMap().get(clientUsername);        if (serverPassword != null) {            callback.setPassword(serverPassword);        }    }}

SignatureUser 类提供了用户及密码信息

package demo.ws.soap_spring_cxf_wss4j;import org.springframework.stereotype.Component;import java.util.Map;@Componentpublic class SignatureUser {    private Map<String, String> userMap;    public Map<String, String> getUserMap() {        return userMap;    }    public void setUserMap(Map<String, String> userMap) {        this.userMap = userMap;    }}

密码信息通过 Spring 注入,即 spring-beans.xml 文件中的 signatureUser bean

<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans"       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"       xsi:schemaLocation="http://www.springframework.org/schema/beans       http://www.springframework.org/schema/beans/spring-beans.xsd">    <bean id="signatureUser" class="demo.ws.soap_spring_cxf_wss4j.SignatureUser">        <property name="userMap">            <map>                <entry key="ciecc" value="cieccPassword"/>            </map>        </property>    </bean></beans>

3. 签名和加密
完成以上工作后,对数据进行验签只需要在发布服务的时候添加 inInterceptors 即可

<!--发布服务--><jaxws:endpoint id="helloService" implementor="demo.ws.soap_spring_cxf.HelloServiceImpl" address="/soap/hello">    <jaxws:inInterceptors>        <ref bean="wss4jInInterceptor"/>    </jaxws:inInterceptors></jaxws:endpoint>

(四)生成客户端进行测试

1. 生成客户端
使用 Java 自带的 wsimport 命令可以很方便地根据 wsdl 文档生成客户端。

wsimport -s . -encoding utf-8 http://localhost:8080/ws/soap/hello?wsdl

执行命令,会在当前目录下生成客户端源码。下一步是配置客户端调用服务

2. 调用服务
新建 Maven 项目,添加和服务端相同的 Maven 依赖,将客户端源码导入到 src 目录下,配置 spring-client.xml

<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans"       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"       xmlns:context="http://www.springframework.org/schema/context"       xmlns:jaxws="http://cxf.apache.org/jaxws"       xsi:schemaLocation="http://www.springframework.org/schema/beans       http://www.springframework.org/schema/beans/spring-beans.xsd        http://cxf.apache.org/jaxws http://cxf.apache.org/schemas/jaxws.xsd        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">    <context:component-scan base-package="ws"/>    <jaxws:client id="helloService" serviceClass="demo.ws.soap_spring_cxf.HelloServiceImplService"                  address="http://localhost:8080/ws/soap/hello?wsdl">        <jaxws:outInterceptors>            <ref bean="wss4jOutInterceptor"/>        </jaxws:outInterceptors>    </jaxws:client>    <!--客户端发送请求拦截器-->    <bean id="wss4jOutInterceptor" class="org.apache.cxf.ws.security.wss4j.WSS4JOutInterceptor">        <constructor-arg>            <map>                <!--签名(使用自己的私钥)-->                <entry key="action" value="Signature Encrypt"/>                <entry key="user" value="ciecc"/>                <entry key="passwordCallbackClass" value="ws.ClientPasswordCallback"/>                <entry key="signaturePropFile" value="client.properties"/>                <!-- 加密 -->                <entry key="encryptionPropFile" value="client.properties"/>            </map>        </constructor-arg>    </bean></beans>

上面的配置方式基本上和服务端一致,只不过用到的是 WSS4JOutInterceptor,其中 user 的值必须是私钥的别名,即 ciecc,passwordCallbackClass 的类使用回调方法提供私钥密码来使用私钥进行签名。

package ws;import org.apache.wss4j.common.ext.WSPasswordCallback;import org.springframework.stereotype.Component;import javax.security.auth.callback.Callback;import javax.security.auth.callback.CallbackHandler;import javax.security.auth.callback.UnsupportedCallbackException;import java.io.IOException;@Componentpublic class ClientPasswordCallback implements CallbackHandler {    public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {        WSPasswordCallback callback = (WSPasswordCallback) callbacks[0];        callback.setPassword("cieccPassword"); //key 的密码    }}

signaturePropFile 和 encryptionPropFile 所需要的签名属性文件和加密属性文件都是 client.properties,其配置如下:

org.apache.wss4j.crypto.provider=org.apache.wss4j.common.crypto.Merlinorg.apache.wss4j.crypto.merlin.keystore.type=jksorg.apache.wss4j.crypto.merlin.keystore.password=keyStorePasswordorg.apache.wss4j.crypto.merlin.keystore.alias=cieccorg.apache.wss4j.crypto.merlin.keystore.file=privatestore.jks

3. 测试
使用 Junit 测试客户端签名加密及服务端验签解密

package ws;import demo.ws.soap_spring_cxf.HelloService;import org.junit.Test;import org.springframework.context.ApplicationContext;import org.springframework.context.support.ClassPathXmlApplicationContext;public class Client {    @Test    public void testHello() {        ApplicationContext context = new ClassPathXmlApplicationContext("spring-client.xml");        HelloService service = context.getBean("helloService", HelloService.class);        String result = service.say("world");        System.out.println(result);    }}

客户端调用服务,发送 “word”,服务端接收到如下信息:

<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">  <soap:Header>    <wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" soap:mustUnderstand="1">      <xenc:EncryptedKey xmlns:xenc="http://www.w3.org/2001/04/xmlenc#" Id="EK-d18f673f-6ecc-4a2c-a21d-570bb1effcd6">        <xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p"/>        <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">          <wsse:SecurityTokenReference>            <ds:X509Data>              <ds:X509IssuerSerial>                <ds:X509IssuerName>CN=ciecc</ds:X509IssuerName>                <ds:X509SerialNumber>67874909</ds:X509SerialNumber>              </ds:X509IssuerSerial>            </ds:X509Data>          </wsse:SecurityTokenReference>        </ds:KeyInfo>        <xenc:CipherData>          <xenc:CipherValue>xE9E5Z6CwRfcovG8RI8qUVtdUe/jSJwsUORC8Q3EkOmBVSXt1/+YZI7XuXcTYcFfREKyKynxSSrC0lCk7O0/0yQU56SS+E7tWfxYzxRyJANE7w9EMEksFm4J7REbYo0kpvfjFk4guudYn8T3VHO372BR9etAEanD1CK8jABUxrk=</xenc:CipherValue>        </xenc:CipherData>        <xenc:ReferenceList>          <xenc:DataReference URI="#ED-3a49d5cb-149d-4424-beaa-1170c25ee7b9"/>        </xenc:ReferenceList>      </xenc:EncryptedKey>      <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#" Id="SIG-21b77015-5a76-4bac-87c2-51a424195467">        <ds:SignedInfo>          <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#">            <ec:InclusiveNamespaces xmlns:ec="http://www.w3.org/2001/10/xml-exc-c14n#" PrefixList="soap"></ec:InclusiveNamespaces>          </ds:CanonicalizationMethod>          <ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>          <ds:Reference URI="#id-4e904297-6fbd-4218-813b-1c15756111f9">            <ds:Transforms>              <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>            </ds:Transforms>            <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>            <ds:DigestValue>2IVsqcAWoiKks6D7+SgN5dE+oi0=</ds:DigestValue>          </ds:Reference>        </ds:SignedInfo>        <ds:SignatureValue>zKeU6h2BPznK/YokHZrLafToI8A5cp2Nzgx89ZfgdBYmeGQtF/7Dy7BridVS00ftNPHp5OdrWRQ9504J//qOUzYAam0V+5CjyMBwldbRL1gG/57jlOxg+prsIotEmHg4Zo+KcK0vHO5Q+zrK3k/kDehHJ3XML6w/MbEEsOWnq2U=</ds:SignatureValue>        <ds:KeyInfo Id="KI-678fba15-f90f-4215-98c4-e33c2d06c93c">          <wsse:SecurityTokenReference wsu:Id="STR-ca6c19eb-7eed-476c-9867-f8a27c05241a">            <ds:X509Data>              <ds:X509IssuerSerial>                <ds:X509IssuerName>CN=ciecc</ds:X509IssuerName>                <ds:X509SerialNumber>67874909</ds:X509SerialNumber>              </ds:X509IssuerSerial>            </ds:X509Data>          </wsse:SecurityTokenReference>        </ds:KeyInfo>      </ds:Signature>    </wsse:Security>  </soap:Header>  <soap:Body xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" wsu:Id="id-4e904297-6fbd-4218-813b-1c15756111f9">    <xenc:EncryptedData xmlns:xenc="http://www.w3.org/2001/04/xmlenc#" Id="ED-3a49d5cb-149d-4424-beaa-1170c25ee7b9" Type="http://www.w3.org/2001/04/xmlenc#Content">      <xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes128-cbc"/>      <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">        <wsse:SecurityTokenReference xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" xmlns:wsse11="http://docs.oasis-open.org/wss/oasis-wss-wssecurity-secext-1.1.xsd" wsse11:TokenType="http://docs.oasis-open.org/wss/oasis-wss-soap-message-security-1.1#EncryptedKey">          <wsse:Reference URI="#EK-d18f673f-6ecc-4a2c-a21d-570bb1effcd6"/>        </wsse:SecurityTokenReference>      </ds:KeyInfo>      <xenc:CipherData>        <xenc:CipherValue>1by/ysWb1wtB4i+HdqzvAJalQwZNeSn50+Z1SH+s/wfAe14TpJANAqukPFPn3gP3hi2K85nhCNuS+BMtJGawpm/LgtkxGaXQAenqMTCygzMhv58yvQ0jxnCoI9yxJn0AALHt3C0vfVaBWURoBNoX3Q==</xenc:CipherValue>      </xenc:CipherData>    </xenc:EncryptedData>  </soap:Body></soap:Envelope>

可见客户端发送的信息都经过了加密。服务端接收信息后,能成功返回“hello world”则表明服务端成功验签解密并提供了服务。

参考文档:
CXF User’s Guide:http://cxf.apache.org/docs/index.html
CXF WS-Security:http://cxf.apache.org/docs/ws-security.html
《架构探险 从零开始写javaweb框架》:https://my.oschina.net/huangyong/blog

原创粉丝点击