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

说明

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

(一)开发环境

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

(二) WebService 服务端开发

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

<dependency>
  <groupId>junitgroupId>
  <artifactId>junitartifactId>
  <version>4.12version>
dependency>

<dependency>
  <groupId>org.apache.cxfgroupId>
  <artifactId>cxf-rt-frontend-jaxwsartifactId>
  <version>3.1.12version>
dependency>
<dependency>
  <groupId>org.apache.cxfgroupId>
  <artifactId>cxf-rt-transports-httpartifactId>
  <version>3.1.12version>
dependency>
<dependency>
  <groupId>org.apache.cxfgroupId>
  <artifactId>cxf-rt-ws-securityartifactId>
  <version>3.1.12version>
dependency>

<dependency>
  <groupId>org.springframeworkgroupId>
  <artifactId>spring-contextartifactId>
  <version>4.3.10.RELEASEversion>
dependency>
<dependency>
  <groupId>org.springframeworkgroupId>
  <artifactId>spring-webartifactId>
  <version>4.3.10.RELEASEversion>
dependency>

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

package demo.ws.soap_spring_cxf;

import javax.jws.WebParam;
import javax.jws.WebService;

@WebService
public 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
@Component
public class HelloServiceImpl implements HelloService {

    public String say(String name) {
        return "hello " + name;
    }
}

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

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


<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>webserviceDemodisplay-name>
  
  <context-param>
    <param-name>contextConfigLocationparam-name>
    <param-value>classpath:spring.xmlparam-value>
  context-param>
  <listener>
    <listener-class>org.springframework.web.context.ContextLoaderListenerlistener-class> 

  
  <servlet>
    <servlet-name>cxfservlet-name>
    <servlet-class>org.apache.cxf.transport.servlet.CXFServletservlet-class>
  servlet>
  <servlet-mapping>
    <servlet-name>cxfservlet-name>
    <url-pattern>/ws/*url-pattern>
  servlet-mapping>

web-app>

spring.xml 中配置需要扫描的包,同时使用 标签引入其他配置文件


<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 中添加代码


<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>

标签用于开启服务日志。启动服务后,访问 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 cieccPassword
keytool -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.Merlin
org.apache.wss4j.crypto.merlin.keystore.type=jks
org.apache.wss4j.crypto.merlin.keystore.password=keyStorePassword
org.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;

@Component
public 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;

@Component
public class SignatureUser {

    private Map userMap;

    public Map getUserMap() {
        return userMap;
    }

    public void setUserMap(Map userMap) {
        this.userMap = userMap;
    }
}

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


<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


<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;

@Component
public 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.Merlin
org.apache.wss4j.crypto.merlin.keystore.type=jks
org.apache.wss4j.crypto.merlin.keystore.password=keyStorePassword
org.apache.wss4j.crypto.merlin.keystore.alias=ciecc
org.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=cieccds:X509IssuerName>
                <ds:X509SerialNumber>67874909ds: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=cieccds:X509IssuerName>
                <ds:X509SerialNumber>67874909ds: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

你可能感兴趣的:(数据对接)