在 Web 容器中使用 Spring + CXF 发布 WS(二) --SOAP 及其安全控制
2017-05-31 00:00
337 查看
摘要: 在对webservice的调用过程中对消息的加密和调用者的验证
WSDL 的全称是 Web Services Description Language(Web 服务描述语言),用于描述 WS 的具体内容。
当成功发布一个 WS 后,就能在浏览器中通过一个地址查看基于 WSDL 文档,它是一个基于 XML 的文档。一个典型的 WSDL 地址如下:
其中的?wsdl必须带上,才能返回一个基于xml的文档.
一个典型的wdsl文档如下:
其中,definitions 是 WSDL 的根节点,它包含两个重要的属性:
name:WS 名称,默认为“WS 实现类 + Service”,例如:HelloServiceImplService
targetNamespace:WS 目标命名空间,默认为“WS 实现类对应包名倒排后构成的地址”,例如:http://soap_spring_cxf.ws.demo/
*可以在 javax.jws.WebService 注解中配置以上两个属性值,但这个配置一定要在 WS 实现类上进行,WS 接口类只需标注一个 WebService 注解即可。
在 definitions 这个根节点下,有五种类型的子节点,它们分别是:
types:描述了 WS 中所涉及的数据类型
portType:定义了 WS 接口名称(endpointInterface)及其操作名称,以及每个操作的输入与输出消息
message:对相关消息进行了定义(供 types 与 portType 使用)
binding:提供了对 WS 的数据绑定方式
service:WS 名称及其端口名称(portName),以及对应的 WSDL 地址. 其中包括了两个重要信息:
portName:WS 的端口名称,默认为“WS 实现类 + Port”,例如:HelloServiceImplPort
endpointInterface:WS 的接口名称,默认为“WS 实现类所实现的接口+Service”,例如:MyServiceImplService.
如果说wsdl只是一个描述文档的话,那SOAP就是具体的调用内容了.
其实 SOAP 就是一个信封(Envelope),在这个信封里包括两个部分,一是头(Header),二是体(Body)。用于传输的数据都放在 Body 中了,一些特殊的属性需要放在 Header 中(下面会看到)。
一般情况下,将需要传输的数据放入 Body 中,而 Header 是没有任何内容的,看起来整个 SOAP 消息是这样的:
查阅WS的相关资料及可能的应用场景,在实际的应用中可能会有以下的需求:
WS 不应该让任何人都可以调用的,这样太不安全了,至少需要做一个身份认证
为了避免第三方恶意程序监控 WS 调用过程,能否对 SOAP Body 中的数据进行加密
SOAP Header 中是否也可存放某些东西用于数据传输
在 WS 领域有一个很强悍的解决方案,名为 WS-Security,它仅仅是一个规范,在 Java 业界里有一个很权威的实现,名为 WSS4J。
下面我将一步步让您学会,如何使用 Spring + CXF + WSS4J 实现一个安全可靠的 WS 调用框架。
将以上的需求抽象为以下步骤:
认证 WS 请求
加密 SOAP 消息
服务端的cxf-servlet.xml 配置文件
首先定义了一个基于 WSS4J 的拦截器(WSS4JInInterceptor),然后通过 <jaxws:inInterceptors> 将其配置到 testService上,最后使用了 CXF 提供的 Bus 特性,只需要在 Bus 上配置一个 logging feature,就可以监控每次 WS 请求与响应的日志了。
注意:这个 WSS4JInInterceptor 是一个 InInterceptor,表示对输入的消息进行拦截,同样还有 OutInterceptor,表示对输出的消息进行拦截。由于以上是服务器端的配置,因此我们只需要配置 InInterceptor 即可,对于客户端而言,我们可以配置 OutInterceptor(下面会看到)。
回调函数的实现类
客户端的cxf-servlet配置
与服务端的配置类似,注释可以在服务端中找.
客户端的密码设置回调函数.
测试:
使用spring提供的test以及Junit 单元测试来测试
如果指定的表标识符不存在即xml配置的user,在服务端的回调函数中userMap中不存在该键则抛出异常:
当密码和用户名都正确时,可以在控制台看到发送的SOAP信息
可见,在 SOAP Header 中提供了 UsernameToken 的相关信息,但 Username 与 Password 都是明文的,SOAP Body 也是明文的,这显然不是最好的解决方案。
如果您将 passwordType 由 PasswordText 改为 PasswordDigest(服务端与客户端都需要做同样的修改),那么就会看到一个加密过的密码,在此不做演示.
对于上面的这种方式,根据SOAP的信息可以看出,加密的过程实际上是在SOAP的Header头部加上了验证信息,我们也可以采用另外的一种方式直接在header头部加上验证信息
ReadSoapHeader类
客户端cxf-servlet配置与服务端大同小异
cxf-servlet.xml配置
AddSoapHeader.java
测试同样采用上面的单元测试.发送的SAOP信息
Header节点下即为添加的SAOP验证信息
在客户端与服务端上都有各自的“密钥库”,这个密钥库里存放了“密钥对”,而密钥对实际上是由“公钥”与“私钥”组成的。当客户端发送 SOAP 消息时,需要使用自己的私钥进行签名,当客户端接收 SOAP 消息时,需要使用客户端提供的公钥进行验签。
参考Apache CXF官网帮助文档上的介绍:(http://cxf.apache.org/docs/ws-security.html)
因为有请求就有相应,所以客户端与服务端的消息调用实际上是双向的,也就是说,客户端与服务端的密钥
库里所存放的信息是这样的:
客户端密钥库:客户端的私钥(用于签名)、服务端的公钥(用于验签)
服务端密钥库:服务端的私钥(用于签名)、客户端的公钥(用于验签)
总结成一句话:使用自己的私钥进行签名,使用对方的公钥进行验签。
新建keystore.bat,使用 JDK 提供的 keytool 命令行工具创建数字证书
运行该批处理程序,将生成两个文件:server_store.jks 与 client_store.jks,随后将 server_store.jks 放入服务端的 classpath 下,将 client_store.jks 放入客户端的 classpath 下.
服务端cxf配置
其中 action 为 Signature,client.properties 内容如下
客户端配置:
其中 action 为 Signature,client.properties 内容如下
通过单元测试结果如下:
服务端:
客户端:
其中的回调函数与第一章节中相同.可以看到,发送的消息也被加密了.
参考文献:https://my.oschina.net/huangyong/blog/287791
一 WSDL ,SOAP基本概念
通过上篇已经基本掌握了使用CXF开发基于SOAP的WS.在此基础上了解一下WSDL,SOAP等一些常见的术语.WSDL 的全称是 Web Services Description Language(Web 服务描述语言),用于描述 WS 的具体内容。
当成功发布一个 WS 后,就能在浏览器中通过一个地址查看基于 WSDL 文档,它是一个基于 XML 的文档。一个典型的 WSDL 地址如下:
http://localhost:8080/webservice/testService?wsdl
其中的?wsdl必须带上,才能返回一个基于xml的文档.
一个典型的wdsl文档如下:
<wsdl:definitions xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" xmlns:tns="http://webservices.chuyu.com/" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:ns1="http://schemas.xmlsoap.org/soap/http" name="MyServiceImplService" targetNamespace="http://webservices.chuyu.com/"> <wsdl:types>...</wsdl:types> <wsdl:message name="saygodby">...</wsdl:message> <wsdl:message name="saygodbyResponse">...</wsdl:message> <wsdl:message name="sayHelloResponse">...</wsdl:message> <wsdl:message name="sayHello">...</wsdl:message> <wsdl:portType name="Myservice">...</wsdl:portType> <wsdl:binding name="MyServiceImplServiceSoapBinding" type="tns:Myservice">...</wsdl:binding> <wsdl:service name="MyServiceImplService">...</wsdl:service> </wsdl:definitions>
其中,definitions 是 WSDL 的根节点,它包含两个重要的属性:
name:WS 名称,默认为“WS 实现类 + Service”,例如:HelloServiceImplService
targetNamespace:WS 目标命名空间,默认为“WS 实现类对应包名倒排后构成的地址”,例如:http://soap_spring_cxf.ws.demo/
*可以在 javax.jws.WebService 注解中配置以上两个属性值,但这个配置一定要在 WS 实现类上进行,WS 接口类只需标注一个 WebService 注解即可。
在 definitions 这个根节点下,有五种类型的子节点,它们分别是:
types:描述了 WS 中所涉及的数据类型
portType:定义了 WS 接口名称(endpointInterface)及其操作名称,以及每个操作的输入与输出消息
message:对相关消息进行了定义(供 types 与 portType 使用)
binding:提供了对 WS 的数据绑定方式
service:WS 名称及其端口名称(portName),以及对应的 WSDL 地址. 其中包括了两个重要信息:
<wsdl:service name="MyServiceImplService"> <wsdl:port binding="tns:MyServiceImplServiceSoapBinding" name="MyServiceImplPort"> <soap:address location="http://localhost:8080/webservice/testService"/> </wsdl:port> </wsdl:service>
portName:WS 的端口名称,默认为“WS 实现类 + Port”,例如:HelloServiceImplPort
endpointInterface:WS 的接口名称,默认为“WS 实现类所实现的接口+Service”,例如:MyServiceImplService.
如果说wsdl只是一个描述文档的话,那SOAP就是具体的调用内容了.
其实 SOAP 就是一个信封(Envelope),在这个信封里包括两个部分,一是头(Header),二是体(Body)。用于传输的数据都放在 Body 中了,一些特殊的属性需要放在 Header 中(下面会看到)。
一般情况下,将需要传输的数据放入 Body 中,而 Header 是没有任何内容的,看起来整个 SOAP 消息是这样的:
查阅WS的相关资料及可能的应用场景,在实际的应用中可能会有以下的需求:
WS 不应该让任何人都可以调用的,这样太不安全了,至少需要做一个身份认证
为了避免第三方恶意程序监控 WS 调用过程,能否对 SOAP Body 中的数据进行加密
SOAP Header 中是否也可存放某些东西用于数据传输
在 WS 领域有一个很强悍的解决方案,名为 WS-Security,它仅仅是一个规范,在 Java 业界里有一个很权威的实现,名为 WSS4J。
下面我将一步步让您学会,如何使用 Spring + CXF + WSS4J 实现一个安全可靠的 WS 调用框架。
将以上的需求抽象为以下步骤:
认证 WS 请求
加密 SOAP 消息
二 WS的请求身份认证
1.基于用户令牌的身份认证
添加Jar包依赖<dependency> <groupId>org.apache.cxf</groupId> <artifactId>cxf-rt-ws-security</artifactId> <version>${cxf.version}</version> </dependency>
服务端的cxf-servlet.xml 配置文件
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jaxws="http://cxf.apache.org/jaxws" xmlns:cxf="http://cxf.apache.org/core" 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"> <!--1. cxf-servlet.xml中import导入的文件不用自己创建,这是在依赖包中的。 --> <import resource="classpath:META-INF/cxf/cxf.xml"/> <import resource="classpath:META-INF/cxf/cxf-servlet.xml"/> <bean id="myService" class="com.chuyu.webservices.MyServiceImpl" /> <!-- 2. webservice发布配置中implementor可以直接写入实现类,如: <jaxws:endpoint id="testService" implementor="test.service.impl.MyServiceImpl" address="/testService"/> --> <bean id="serverPasswordCallback" class="com.chuyu.util.ServerPasswordCallback"/> <jaxws:endpoint id="testService" implementor="#myService" address="/testService"> <!--3.address参数是重点,这是webservice发布后其wsdl的相对路径,其绝对路径为应用访问路径/cxf拦截路径/address?wsdl--> <jaxws:inInterceptors> <bean class="org.apache.cxf.interceptor.LoggingInInterceptor"/> <bean class="org.apache.cxf.binding.soap.saaj.SAAJInInterceptor"/> <bean class="org.apache.cxf.ws.security.wss4j.WSS4JInInterceptor"> <constructor-arg> <map> <!-- action:UsernameToken使用基于“用户名令牌”的方式进行身份认证--> <entry key="action" value="UsernameToken"/> <!--密码加密策略.PasswordText:明文密码 ;PasswordDigest 密文密码--> <entry key="passwordType" value="PasswordDigest"/> <!--服务端别名 ,可不指定--> <entry key="user" value="cxfServer"/> <!--提供一个用于密码验证的回调处理器--> <entry key="passwordCallbackRef"> <ref bean="serverPasswordCallback"/> </entry> </map> </constructor-arg> </bean> </jaxws:inInterceptors> </jaxws:endpoint> <cxf:bus> <cxf:features> <cxf:logging/> </cxf:features> </cxf:bus> </beans>
首先定义了一个基于 WSS4J 的拦截器(WSS4JInInterceptor),然后通过 <jaxws:inInterceptors> 将其配置到 testService上,最后使用了 CXF 提供的 Bus 特性,只需要在 Bus 上配置一个 logging feature,就可以监控每次 WS 请求与响应的日志了。
注意:这个 WSS4JInInterceptor 是一个 InInterceptor,表示对输入的消息进行拦截,同样还有 OutInterceptor,表示对输出的消息进行拦截。由于以上是服务器端的配置,因此我们只需要配置 InInterceptor 即可,对于客户端而言,我们可以配置 OutInterceptor(下面会看到)。
回调函数的实现类
package com.chuyu.util; import org.apache.ws.security.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; import java.util.HashMap; import java.util.Map; @Component public class ServerPasswordCallback implements CallbackHandler { /** * 假定userMap为存放的客户端和服务端的名称和密码, * 在实际应用场景中可以使用数据库等存储机制来验证用户名和密码组合. */ private static final Map<String, String> userMap = new HashMap<String, String>(); static { userMap.put("client", "clientpass"); userMap.put("server", "serverpass"); } @Override public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { //将 JDK 提供的 javax.security.auth.callback.Callback 转型为 WSS4J 提供的 //org.apache.wss4j.common.ext.WSPasswordCallback WSPasswordCallback callback=(WSPasswordCallback) callbacks[0]; //客户端标识(用户名) String clientUsername= callback.getIdentifier(); //密码 String clientPassword = userMap.get(clientUsername); if (serverPassword != null) { callback.setPassword(serverPassword); } else { throw new SecurityException("验证失败"); } } }
客户端的cxf-servlet配置
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 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"> <import resource="classpath:META-INF/cxf/cxf.xml"/> <import resource="classpath:META-INF/cxf/cxf-servlet.xml"/> <!-- 客户端xml配置 --> <bean id="webTest" class="com.chuyu.client.Myservice" factory-bean="client" factory-method="create"/> <bean id="client" class="org.apache.cxf.jaxws.JaxWsProxyFactoryBean"> <property name="address" value="http://192.168.9.47:8080/ws-demo-server/webservice/testService?wsdl"> </property> <property name="serviceClass" value="com.chuyu.client.Myservice"></property> <property name="outInterceptors"> <list> <bean class="org.apache.cxf.interceptor.LoggingOutInterceptor" /> <bean class="org.apache.cxf.binding.soap.saaj.SAAJOutInterceptor" /> <bean class="org.apache.cxf.ws.security.wss4j.WSS4JOutInterceptor"> <constructor-arg> <map> <entry key="action" value="UsernameToken" /> <entry key="passwordType" value="PasswordDigest" /> <entry key="user" value="client" /> <entry key="passwordCallbackRef"> <ref bean="clientPasswordCallback" /> </entry> </map> </constructor-arg> </bean> </list> </property> </bean> <bean id="clientPasswordCallback" class="com.chuyu.webservice.ClientPasswordCallback"></bean> </beans>
与服务端的配置类似,注释可以在服务端中找.
客户端的密码设置回调函数.
package com.chuyu.webservice; import java.io.IOException; import javax.security.auth.callback.Callback; import javax.security.auth.callback.CallbackHandler; import javax.security.auth.callback.UnsupportedCallbackException; import org.apache.wss4j.common.ext.WSPasswordCallback; import org.springframework.stereotype.Component; @Component public class ClientPasswordCallback implements CallbackHandler{ @Override public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { WSPasswordCallback callback = (WSPasswordCallback) callbacks[0]; System.out.println("identifier: " + callback.getIdentifier()); //客户端标识 及xml中配置的User用户名 callback.setPassword("clientpass"); } }
测试:
使用spring提供的test以及Junit 单元测试来测试
package com.chuyu.webservice; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import com.chuyu.client.Myservice; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations={"classpath:cxf-servlet.xml","classpath:spring.xml"}) public class Clientclass{ @Autowired private Myservice webTest; @Test public void testSayhello(){ System.out.println(webTest.sayHello("张三")); } }
如果指定的表标识符不存在即xml配置的user,在服务端的回调函数中userMap中不存在该键则抛出异常:
当密码和用户名都正确时,可以在控制台看到发送的SOAP信息
<?xml version="1.0" encoding="utf-8"?> <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"> <SOAP-ENV:Header xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"> <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"> <wsse:UsernameToken wsu:Id="UsernameToken-c9246452-7027-4bc4-9775-b8e34fd4439c"> <wsse:Username>cxfClient</wsse:Username> <wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText">clientpass</wsse:Password> </wsse:UsernameToken> </wsse:Security> </SOAP-ENV:Header> <soap:Body> <ns2:sayHello xmlns:ns2="http://webservices.chuyu.com/"> <name>张三</name> </ns2:sayHello> </soap:Body> </soap:Envelope>
可见,在 SOAP Header 中提供了 UsernameToken 的相关信息,但 Username 与 Password 都是明文的,SOAP Body 也是明文的,这显然不是最好的解决方案。
如果您将 passwordType 由 PasswordText 改为 PasswordDigest(服务端与客户端都需要做同样的修改),那么就会看到一个加密过的密码,在此不做演示.
对于上面的这种方式,根据SOAP的信息可以看出,加密的过程实际上是在SOAP的Header头部加上了验证信息,我们也可以采用另外的一种方式直接在header头部加上验证信息
通过SoapHeader来增强Web Service的安全性
服务端的cxf-servlet.xml配置<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jaxws="http://cxf.apache.org/jaxws" xmlns:cxf="http://cxf.apache.org/core" 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"> <!--1. cxf-servlet.xml中import导入的文件不用自己创建,这是在依赖包中的。 --> <import resource="classpath:META-INF/cxf/cxf.xml"/> <import resource="classpath:META-INF/cxf/cxf-servlet.xml"/> <bean id="myService" class="com.chuyu.webservices.MyServiceImpl" /> <!-- 2. webservice发布配置中implementor可以直接写入实现类,如: <jaxws:endpoint id="testService" implementor="test.service.impl.MyServiceImpl" address="/testService"/> --> <!--<bean id="serverPasswordCallback" class="com.chuyu.util.ServerPasswordCallback"/>--> <jaxws:endpoint id="testService" implementor="#myService" address="/testService"> <!--3.address参数是重点,这是webservice发布后其wsdl的相对路径,其绝对路径为应用访问路径/cxf拦截路径/address?wsdl--> <jaxws:inInterceptors> <bean class="org.apache.cxf.interceptor.LoggingInInterceptor"/> <bean class="com.chuyu.util.ReadSoapHeader"></bean> <!--<bean class="org.apache.cxf.binding.soap.saaj.SAAJInInterceptor"/>--> <!--<bean class="org.apache.cxf.ws.security.wss4j.WSS4JInInterceptor">--> <!--<constructor-arg>--> <!--<map>--> <!--<!– action:UsernameToken使用基于“用户名令牌”的方式进行身份认证–>--> <!--<entry key="action" value="UsernameToken"/>--> <!--<!–密码加密策略.PasswordText:明文密码 ;PasswordDigest 密文密码–>--> <!--<entry key="passwordType" value="PasswordDigest"/>--> <!--<!–服务端别名 ,可不指定–>--> <!--<entry key="user" value="cxfServer"/>--> <!--<!–提供一个用于密码验证的回调处理器–>--> <!--<entry key="passwordCallbackRef">--> <!--<ref bean="serverPasswordCallback"/>--> <!--</entry>--> <!--</map>--> <!--</constructor-arg>--> <!--</bean>--> </jaxws:inInterceptors> </jaxws:endpoint> <cxf:bus> <cxf:features> <cxf:logging/> </cxf:features> </cxf:bus> </beans>
ReadSoapHeader类
package com.chuyu.util; import org.apache.cxf.binding.soap.SoapMessage; import org.apache.cxf.binding.soap.saaj.SAAJInInterceptor; import org.apache.cxf.interceptor.Fault; import org.apache.cxf.phase.AbstractPhaseInterceptor; import org.apache.cxf.phase.Phase; import org.w3c.dom.NodeList; import javax.xml.soap.SOAPException; import javax.xml.soap.SOAPHeader; import javax.xml.soap.SOAPMessage; /** * Created by caowenhui on 2017/6/6. */ public class ReadSoapHeader extends AbstractPhaseInterceptor<SoapMessage> { //消息输入拦截 private SAAJInInterceptor saa=new SAAJInInterceptor(); public ReadSoapHeader(){ //指定拦截阶段 super(Phase.PRE_PROTOCOL); getAfter().add(SAAJInInterceptor.class.getName()); } public void handleMessage(SoapMessage message) throws Fault { // 获取Soap信息的xml表示 SOAPMessage mess=message.getContent(SOAPMessage.class); if(mess==null){ saa.handleMessage(message); mess=message.getContent(SOAPMessage.class); } //获取SOAP xml的Hander头部信息 SOAPHeader head=null; try { head = mess.getSOAPHeader(); } catch (SOAPException e) { e.printStackTrace(); } if(head==null){ return; } //用户名和密码节点 NodeList nodes=head.getElementsByTagName("tns:spId"); NodeList nodepass=head.getElementsByTagName("tns:spPassword"); System.out.println(nodes.item(0).getTextContent()); System.out.println(nodepass.item(0).getTextContent()); if(nodes.item(0).getTextContent().indexOf("client")!=-1){ if(nodepass.item(0).getTextContent().equals("clientpass")){ System.out.println("认证成功"); } } else{ SOAPException soapExc=new SOAPException("认证错误"); throw new Fault(soapExc); } } }
客户端cxf-servlet配置与服务端大同小异
cxf-servlet.xml配置
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 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"> <import resource="classpath:META-INF/cxf/cxf.xml"/> <import resource="classpath:META-INF/cxf/cxf-servlet.xml"/> <!-- 客户端xml配置 --> <bean id="webTest" class="com.chuyu.client.Myservice" factory-bean="client" factory-method="create"/> <bean id="client" class="org.apache.cxf.jaxws.JaxWsProxyFactoryBean"> <property name="address" value="http://192.168.9.47:8080/ws-demo-server/webservice/testService?wsdl"></property> <property name="serviceClass" value="com.chuyu.client.Myservice"></property> <property name="outInterceptors"> <list> <bean class="org.apache.cxf.interceptor.LoggingOutInterceptor" /> <bean class="com.chuyu.webservice.AddSoapHeader"></bean> <!-- <bean class="org.apache.cxf.binding.soap.saaj.SAAJOutInterceptor" /> --> <!-- <bean class="org.apache.cxf.ws.security.wss4j.WSS4JOutInterceptor"> <constructor-arg> <map> <entry key="action" value="UsernameToken" /> <entry key="passwordType" value="PasswordDigest" /> <entry key="user" value="client" /> <entry key="passwordCallbackRef"> <ref bean="clientPasswordCallback" /> </entry> </map> </constructor-arg> </bean> --> </list> </property> </bean> <!-- <bean id="clientPasswordCallback" class="com.chuyu.webservice.ClientPasswordCallback"></bean> --> </beans>
AddSoapHeader.java
package com.chuyu.webservice; import java.text.SimpleDateFormat; import java.util.Date; import java.util.List; import javax.xml.namespace.QName; import org.apache.cxf.binding.soap.SoapHeader; import org.apache.cxf.binding.soap.SoapMessage; import org.apache.cxf.binding.soap.interceptor.AbstractSoapInterceptor; import org.apache.cxf.headers.Header; import org.apache.cxf.helpers.DOMUtils; import org.apache.cxf.interceptor.Fault; import org.apache.cxf.phase.Phase; import org.w3c.dom.Document; import org.w3c.dom.Element; public class AddSoapHeader extends AbstractSoapInterceptor{ private static String nameURI="http://www.WsAuthentication.com//authentication"; public AddSoapHeader(){ // 指定该拦截器在哪个阶段被激发 super(Phase.WRITE); } @Override public void handleMessage(SoapMessage message) throws Fault { SimpleDateFormat sd=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); Date date=new Date(); String time =sd.format(date); String spPassword="client"; String spName="clientpass"; QName qname=new QName("RequestSOAPHeader"); Document doc=DOMUtils.createDocument(); Element spId=doc.createElement("tns:spId"); spId.setTextContent(spName); Element spPass=doc.createElement("tns:spPassword"); spPass.setTextContent(spPassword); Element root=doc.createElementNS(nameURI, "tns:RequestSOAPHeader"); root.appendChild(spId); root.appendChild(spPass); SoapHeader head=new SoapHeader(qname,root); List<Header> headers=message.getHeaders(); headers.add(head); } }
测试同样采用上面的单元测试.发送的SAOP信息
Header节点下即为添加的SAOP验证信息
2.基于数字签名的身份认证
数字签名从字面上理解就是一种基于数字的签名方式。也就是说,当客户端发送 SOAP 消息时,需要对其进行“签名”,来证实自己的身份,当服务端接收 SOAP 消息时,需要对其签名进行验证(简称“验签”)。在客户端与服务端上都有各自的“密钥库”,这个密钥库里存放了“密钥对”,而密钥对实际上是由“公钥”与“私钥”组成的。当客户端发送 SOAP 消息时,需要使用自己的私钥进行签名,当客户端接收 SOAP 消息时,需要使用客户端提供的公钥进行验签。
参考Apache CXF官网帮助文档上的介绍:(http://cxf.apache.org/docs/ws-security.html)
因为有请求就有相应,所以客户端与服务端的消息调用实际上是双向的,也就是说,客户端与服务端的密钥
库里所存放的信息是这样的:
客户端密钥库:客户端的私钥(用于签名)、服务端的公钥(用于验签)
服务端密钥库:服务端的私钥(用于签名)、客户端的公钥(用于验签)
总结成一句话:使用自己的私钥进行签名,使用对方的公钥进行验签。
新建keystore.bat,使用 JDK 提供的 keytool 命令行工具创建数字证书
@echo off keytool -genkeypair -alias server -keyalg RSA -dname "cn=server" -keypass serverpass -keystore server_store.jks -storepass storepass keytool -exportcert -alias server -file server_key.rsa -keystore server_store.jks -storepass storepass keytool -importcert -alias server -file server_key.rsa -keystore client_store.jks -storepass storepass -noprompt del server_key.rsa keytool -genkeypair -alias client -dname "cn=client" -keyalg RSA -keypass clientpass -keystore client_store.jks -storepass storepass keytool -exportcert -alias client -file client_key.rsa -keystore client_store.jks -storepass storepass keytool -importcert -alias client -file client_key.rsa -keystore server_store.jks -storepass storepass -noprompt del client_key.rsa
运行该批处理程序,将生成两个文件:server_store.jks 与 client_store.jks,随后将 server_store.jks 放入服务端的 classpath 下,将 client_store.jks 放入客户端的 classpath 下.
服务端cxf配置
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 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"> <import resource="classpath:META-INF/cxf/cxf.xml"/> <import resource="classpath:META-INF/cxf/cxf-servlet.xml"/> <!-- 客户端xml配置 --> <bean id="webTest" class="com.chuyu.client.Myservice" factory-bean="client" factory-method="create"/> <bean id="client" class="org.apache.cxf.jaxws.JaxWsProxyFactoryBean"> <property name="address" value="http://192.168.9.47:8080/ws-demo-server/webservice/testService?wsdl"></property> <property name="serviceClass" value="com.chuyu.client.Myservice"></property> <property name="outInterceptors"> <list> <bean class="org.apache.cxf.interceptor.LoggingOutInterceptor" /> <!-- <bean class="com.chuyu.webservice.AddSoapHeader"></bean> --> <bean class="org.apache.cxf.binding.soap.saaj.SAAJOutInterceptor"/> <bean class="org.apache.cxf.ws.security.wss4j.WSS4JOutInterceptor"> <constructor-arg> <map> <!-- 签名(使用自己的私钥) --> <entry key="action" value="Signature" /> <entry key="signaturePropFile" value="client_sign.properties"/> <entry key="signatureUser" value="client"/> <entry key="passwordCallbackRef"> <ref bean="clientPasswordCallback" /> </entry> </map> </constructor-arg> </bean> </list> </property> </bean> <!-- <bean id="clientPasswordCallback" class="com.chuyu.webservice.ClientPasswordCallback"></bean> --> </beans>
其中 action 为 Signature,client.properties 内容如下
org.apache.ws.security.crypto.provider=org.apache.ws.security.components.crypto.Merlin org.apache.ws.security.crypto.merlin.file=server_store.jks org.apache.ws.security.crypto.merlin.keystore.type=jks org.apache.ws.security.crypto.merlin.keystore.password=storepass
客户端配置:
//.......相同省略 <bean id="wss4jOutInterceptor" class="org.apache.cxf.ws.security.wss4j.WSS4JOutInterceptor"> <constructor-arg> <map> <!-- 签名(使用自己的私钥) --> <entry key="action" value="Signature"/> <entry key="signaturePropFile" value="client.properties"/> <entry key="signatureUser" value="client"/> <entry key="passwordCallbackRef" value-ref="clientPasswordCallback"/> </map> </constructor-arg> </bean> //.....省略
其中 action 为 Signature,client.properties 内容如下
org.apache.ws.security.crypto.provider=org.apache.ws.security.components.crypto.Merlin org.apache.ws.security.crypto.merlin.file=client_store.jks org.apache.ws.security.crypto.merlin.keystore.type=jks org.apache.ws.security.crypto.merlin.keystore.password=storepass
通过单元测试结果如下:
3.SOAP消息的签名与加/解密
WSS4J 除了提供签名与验签(Signature)这个特性以外,还提供了加密与解密(Encrypt)功能.服务端:
客户端:
其中的回调函数与第一章节中相同.可以看到,发送的消息也被加密了.
参考文献:https://my.oschina.net/huangyong/blog/287791
相关文章推荐
- 在 Web 容器中使用 Spring + CXF 发布 WS(一)
- CXF系列之JAX-WS:基于SOAP的安全控制
- Spring 集成CXF框架发布Webservice服务 和 使用jdk生成Webservice clinet
- Web项目中使用Spring整合CXF发布Web Services
- jaxws soap webservice 使用apache cxf tool ——wsdl2java
- Web应用中使用CXF不基于Spring的WebService发布
- 使用spring安全容器管理web程序
- 使用CXF和Spring发布Soap服务
- Web应用中使用CXF不基于Spring的WebService发布
- webService总结(三)——使用CXF + Spring发布webService
- Spring Boot中使用Spring Security进行安全控制
- JAX-WS 学习二:基于WEB容器,发布WebService
- Web Services(2)-Spring+CXF 发布WS
- 使用CXF+spring+restful创建一个web的接口项目
- 使用Spring + CXF 发布WebService服务
- WebService学习之旅(二)JAX-WS基于Web容器发布WebService
- Web Service 那点事儿(3)—— SOAP 及其安全控制
- Spring Cloud Spring Boot mybatis分布式微服务云架构(十三)使用Spring Security安全控制
- 使用 Spring + CXF 发布 REST 服务
- Spring控制反转容器的使用