目前运维的平台需要和其他类型相似的平台进行数据对接,由于数据量不大,基本上不存在对接时的性能问题,所以选择使用 WebService 进行数据对接。综合来看,Spring + CXF 的技术实现方式是现在的主流,有较多的技术支持,因此选择这种方式来开发 WebService。
选用 JDK1.7 + Maven3.3.9 + Spring4.3.10 + CXF3.1.12 + Tomcat7.0 来进行开发。
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>
<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>
在数据传输过程中,往往会有加密的需求,这里使用 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