通过上一篇文章,相信您已经学会了如何使用 CXF 开发基于 SOAP 的 WS 了。或许您目前对于底层原理性的东西还不太理解,心中难免会有些疑问:

什么是 WSDL?

什么是 SOAP?

如何能让 SOAP 更加安全?

我将努力通过本文,针对以上问题,让您得到一个满意的答案。

还等什么呢?就从 WSDL 开始吧!

WSDL 的全称是 Web Services Description Language(Web 服务描述语言),用于描述 WS 的具体内容。

当您成功发布一个 WS 后,就能在浏览器中通过一个地址查看基于 WSDL 文档,它是一个基于 XML 的文档。一个典型的 WSDL 地址如下:

http://localhost:8080/ws/soap/hello?wsdl

注意:WSDL 地址必须带有一个 wsdl 参数。

在浏览器中,您会看到一个标准的 XML 文档:

其中,definitions 是 WSDL 的根节点,它包括两个重要的属性:

  1. name:WS 名称,默认为“WS 实现类 + Service”,例如:HelloServiceImplService
  2. targetNamespace:WS 目标命名空间,默认为“WS 实现类对应包名倒排后构成的地址”,例如:http://soap_spring_cxf.ws.demo/

提示:可以在 javax.jws.WebService 注解中配置以上两个属性值,但这个配置一定要在 WS 实现类上进行,WS 接口类只需标注一个 WebService 注解即可。

在 definitions 这个根节点下,有五种类型的子节点,它们分别是:

  1. types:描述了 WS 中所涉及的数据类型
  2. portType:定义了 WS 接口名称(endpointInterface)及其操作名称,以及每个操作的输入与输出消息
  3. message:对相关消息进行了定义(供 types 与 portType 使用)
  4. binding:提供了对 WS 的数据绑定方式
  5. service:WS 名称及其端口名称(portName),以及对应的 WSDL 地址

其中包括了两个重要信息:

  1. portName:WS 的端口名称,默认为“WS 实现类 + Port”,例如:HelloServiceImplPort
  2. endpointInterface:WS 的接口名称,默认为“WS 实现类所实现的接口”,例如:HelloService

提示:可在 javax.jws.WebService 注解中配置 portName 与 endpointInterface,同样必须在 WS 实现类上配置。

如果说 WSDL 是用于描述 WS 是什么,那么 SOAP 就用来表示 WS 里有什么。

其实 SOAP 就是一个信封(Envelope),在这个信封里包括两个部分,一是头(Header),二是体(Body)。用于传输的数据都放在 Body 中了,一些特殊的属性需要放在 Header 中(下面会看到)。

一般情况下,将需要传输的数据放入 Body 中,而 Header 是没有任何内容的,看起来整个 SOAP 消息是这样的:

可见,HTTP 请求的 Request Header 与 Request Body,这正好与 SOAP 消息的结构有着异曲同工之妙!

看到这里,您或许会有很多疑问:

  1. WS 不应该让任何人都可以调用的,这样太不安全了,至少需要做一个身份认证吧?
  2. 为了避免第三方恶意程序监控 WS 调用过程,能否对 SOAP Body 中的数据进行加密呢?
  3. SOAP Header 中究竟可存放什么东西呢?

没错!这就是我们今天要展开讨论的话题 —— 基于 SOAP 的安全控制。

在 WS 领域有一个很强悍的解决方案,名为 WS-Security,它仅仅是一个规范,在 Java 业界里有一个很权威的实现,名为 WSS4J

下面我将一步步让您学会,如何使用 Spring + CXF + WSS4J 实现一个安全可靠的 WS 调用框架。

其实您需要做也就是两件事情:

  1. 认证 WS 请求
  2. 加密 SOAP 消息

怎样对 WS 进行身份认证呢?可使用如下解决方案:

1. 基于用户令牌的身份认证

第一步:添加 CXF 提供的 WS-Security 的 Maven 依赖

<!-- lang: xml -->
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-rt-ws-security</artifactId>
<version>${cxf.version}</version>
</dependency>

其实底层实现还是 WSS4J,CXF 只是对其做了一个封装而已。

第二步:完成服务端 CXF 相关配置

<!-- lang: 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-4.0.xsd
http://cxf.apache.org/core
http://cxf.apache.org/schemas/core.xsd
http://cxf.apache.org/jaxws
http://cxf.apache.org/schemas/jaxws.xsd"> <bean id="wss4jInInterceptor" class="org.apache.cxf.ws.security.wss4j.WSS4JInInterceptor">
<constructor-arg>
<map>
<!-- 用户认证(明文密码) -->
<entry key="action" value="UsernameToken"/>
<entry key="passwordType" value="PasswordText"/>
<entry key="passwordCallbackRef" value-ref="serverPasswordCallback"/>
</map>
</constructor-arg>
</bean> <jaxws:endpoint id="helloService" implementor="#helloServiceImpl" address="/soap/hello">
<jaxws:inInterceptors>
<refbean="wss4jInInterceptor"/>
</jaxws:inInterceptors>
</jaxws:endpoint> <cxf:bus>
<cxf:features>
<cxf:logging/>
</cxf:features>
</cxf:bus> </beans>

首先定义了一个基于 WSS4J 的拦截器(WSS4JInInterceptor),然后通过 <jaxws:inInterceptors> 将其配置到 helloService 上,最后使用了 CXF 提供的 Bus 特性,只需要在 Bus 上配置一个 logging feature,就可以监控每次 WS 请求与响应的日志了。

注意:这个 WSS4JInInterceptor 是一个 InInterceptor,表示对输入的消息进行拦截,同样还有 OutInterceptor,表示对输出的消息进行拦截。由于以上是服务器端的配置,因此我们只需要配置 InInterceptor 即可,对于客户端而言,我们可以配置 OutInterceptor(下面会看到)。

有必要对以上配置中,关于 WSS4JInInterceptor 的构造器参数做一个说明。

  • action = UsernameToken:表示使用基于“用户名令牌”的方式进行身份认证
  • passwordType = PasswordText:表示密码以明文方式出现
  • passwordCallbackRef = serverPasswordCallback:需要提供一个用于密码验证的回调处理器(CallbackHandler)

以下便是 ServerPasswordCallback 的具体实现:

<!-- lang: java -->
package demo.ws.soap_spring_cxf_wss4j; import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
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 ServerPasswordCallback implements CallbackHandler { 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 {
WSPasswordCallback callback = (WSPasswordCallback) callbacks[0]; String clientUsername = callback.getIdentifier();
String serverPassword = userMap.get(clientUsername); if (serverPassword != null) {
callback.setPassword(serverPassword);
}
}
}

可见,它实现了 javax.security.auth.callback.CallbackHandler 接口,这是 JDK 提供的用于安全认证的回调处理器接口。在代码中提供了两个用户,分别是 client 与 server,用户名与密码存放在 userMap 中。这里需要将 JDK 提供的 javax.security.auth.callback.Callback 转型为 WSS4J 提供的 org.apache.wss4j.common.ext.WSPasswordCallback,在 handle 方法中实现对客户端密码的验证,最终需要将密码放入 callback 对象中。

第三步:完成客户端 CXF 相关配置

<!-- lang: 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-4.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://cxf.apache.org/jaxws
http://cxf.apache.org/schemas/jaxws.xsd"> <context:component-scan base-package="demo.ws"/> <bean id="wss4jOutInterceptor" class="org.apache.cxf.ws.security.wss4j.WSS4JOutInterceptor">
<constructor-arg>
<map>
<!-- 用户认证(明文密码) -->
<entry key="action" value="UsernameToken"/>
<entry key="user" value="client"/>
<entry key="passwordType" value="PasswordText"/>
<entry key="passwordCallbackRef" value-ref="clientPasswordCallback"/>
</map>
</constructor-arg>
</bean> <jaxws:clientid="helloService"serviceClass="demo.ws.soap_spring_cxf_wss4j.HelloService"address="http://localhost:8080/ws/soap/hello">
<jaxws:outInterceptors>
<refbean="wss4jOutInterceptor"/>
</jaxws:outInterceptors>
</jaxws:client> </beans>

注意:这里使用的是 WSS4JOutInterceptor,它是一个 OutInterceptor,使客户端对输出的消息进行拦截。

WSS4JOutInterceptor 的配置基本上与 WSS4JInInterceptor 大同小异,这里需要提供客户端的用户名(user = client),还需要提供一个客户端密码回调处理器(passwordCallbackRef = clientPasswordCallback),代码如下:

<!-- lang: java -->
package demo.ws.soap_spring_cxf_wss4j; 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];
callback.setPassword("clientpass");
}
}

在 ClientPasswordCallback 无非设置客户端用户的密码,其它的什么也不用做了。客户端密码只能通过回调处理器的方式来提供,而不能在 Spring 中配置。

第四步:调用 WS 并观察控制台日志

部署应用并启动 Tomcat,再次调用 WS,此时会在 Tomcat 控制台里的 Inbound Message 中看到如下 Payload:

可见,在 SOAP Header 中提供了 UsernameToken 的相关信息,但 Username 与 Password 都是明文的,SOAP Body 也是明文的,这显然不是最好的解决方案。

如果您将 passwordType 由 PasswordText 改为 PasswordDigest(服务端与客户端都需要做同样的修改),那么就会看到一个加密过的密码:

除了这种基于用户名与密码的身份认证以外,还有一种更安全的身份认证方式,名为“数字签名”。

2. 基于数字签名的身份认证

数字签名从字面上理解就是一种基于数字的签名方式。也就是说,当客户端发送 SOAP 消息时,需要对其进行“签名”,来证实自己的身份,当服务端接收 SOAP 消息时,需要对其签名进行验证(简称“验签”)。

在客户端与服务端上都有各自的“密钥库”,这个密钥库里存放了“密钥对”,而密钥对实际上是由“公钥”与“私钥”组成的。当客户端发送 SOAP 消息时,需要使用自己的私钥进行签名,当客户端接收 SOAP 消息时,需要使用客户端提供的公钥进行验签。

因为有请求就有相应,所以客户端与服务端的消息调用实际上是双向的,也就是说,客户端与服务端的密钥库里所存放的信息是这样的:

  • 客户端密钥库:客户端的私钥(用于签名)、服务端的公钥(用于验签)
  • 服务端密钥库:服务端的私钥(用于签名)、客户端的公钥(用于验签)

记住一句话:使用自己的私钥进行签名,使用对方的公钥进行验签。

可见生成密钥库是我们要做的第一件事情。

第一步:生成密钥库

现在您需要创建一个名为 keystore.bat 的批处理文件,其内容如下:

<!-- lang: shell -->
@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

在以上这些命令中,使用了 JDK 提供的 keytool 命令行工具,关于该命令的使用方法,可点击以下链接:

http://docs.oracle.com/javase/6/docs/technotes/tools/solaris/keytool.html

运行该批处理程序,将生成两个文件:server_store.jks 与 client_store.jks,随后将 server_store.jks 放入服务端的 classpath 下,将 client_store.jks 放入客户端的 classpath 下。如果您在本机运行,那么本机既是客户端又是服务端。

第二步:完成服务端 CXF 相关配置

<!-- lang: xml -->
...
<bean id="wss4jInInterceptor" class="org.apache.cxf.ws.security.wss4j.WSS4JInInterceptor">
<constructor-arg>
<map>
<!-- 验签(使用对方的公钥) -->
<entry key="action" value="Signature"/>
<entry key="signaturePropFile" value="server.properties"/>
</map>
</constructor-arg>
</bean>
...

其中 action 为 Signature,server.properties 内容如下:

<!-- lang: java -->
org.apache.ws.security.crypto.provider=org.apache.wss4j.common.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

第三步:完成客户端 CXF 相关配置

<!-- lang: xml -->
...
<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 内容如下:

<!-- lang: java -->
org.apache.ws.security.crypto.provider=org.apache.wss4j.common.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

此外,客户端同样需要提供签名用户(signatureUser)与密码回调处理器(passwordCallbackRef)。

第四步:调用 WS 并观察控制台日志

可见,数字签名确实是一种更为安全的身份认证方式,但无法对 SOAP Body 中的数据进行加密,仍然是“world”。

究竟怎样才能加密并解密 SOAP 消息中的数据呢?

3. SOAP 消息的加密与解密

WSS4J 除了提供签名与验签(Signature)这个特性以外,还提供了加密与解密(Encrypt)功能,您只需要在服务端与客户端的配置中稍作修改即可。

服务端:

<!-- lang: xml -->
...
<bean id="wss4jInInterceptor" class="org.apache.cxf.ws.security.wss4j.WSS4JInInterceptor">
<constructor-arg>
<map>
<!-- 验签 与 解密 -->
<entry key="action" value="Signature Encrypt"/>
<!-- 验签(使用对方的公钥) -->
<entry key="signaturePropFile" value="server.properties"/>
<!-- 解密(使用自己的私钥) -->
<entry key="decryptionPropFile" value="server.properties"/>
<entry key="passwordCallbackRef" value-ref="serverPasswordCallback"/>
</map>
</constructor-arg>
</bean>
...

客户端:

<!-- lang: xml -->
...
<bean id="wss4jOutInterceptor" class="org.apache.cxf.ws.security.wss4j.WSS4JOutInterceptor">
<constructor-arg>
<map>
<!-- 签名 与 加密 -->
<entry key="action" value="Signature Encrypt"/>
<!-- 签名(使用自己的私钥) -->
<entry key="signaturePropFile" value="client.properties"/>
<entry key="signatureUser" value="client"/>
<entry key="passwordCallbackRef" value-ref="clientPasswordCallback"/>
<!-- 加密(使用对方的公钥) -->
<entry key="encryptionPropFile" value="client.properties"/>
<entry key="encryptionUser" value="server"/>
</map>
</constructor-arg>
</bean>
...

可见,客户端发送 SOAP 消息时进行签名(使用自己的私钥)与加密(使用对方的公钥),服务端接收 SOAP 消息时进行验签(使用对方的公钥)与解密(使用自己的私钥)。

现在您看到的 SOAP 消息应该是这样的:

可见,SOAP 请求不仅签名了,而且还加密了,这样的通讯更加安全可靠。

但是还存在一个问题,虽然 SOAP 请求已经很安全了,但 SOAP 响应却没有做任何安全控制,看看下面的 SOAP 响应吧:

如何才能对 SOAP 响应进行签名与加密呢?相信您一定有办法做到,不妨亲自动手试一试吧!

4. 总结

本文的内容有些多,确实需要稍微总结一下:

  1. WSDL 是用于描述 WS 的具体内容的
  2. SOAP 是用于封装 WS 请求与响应的
  3. 可使用“用户令牌”方式对 WS 进行身份认证(支持明文密码与密文密码)
  4. 可使用“数字签名”方式对 WS 进行身份认证
  5. 可对 SOAP 消息进行加密与解密

CXF SOAP 及其安全控制的更多相关文章

  1. SOAP 及其安全控制--转载

    原文地址:http://my.oschina.net/huangyong/blog/287791 目录[-] 1. 基于用户令牌的身份认证 2. 基于数字签名的身份认证 3. SOAP 消息的加密与解 ...

  2. cxf soap rest webservice spring

    1. 导入 jar 包 2. 编写接口 3. 编写实现 4. 配置spring 配置文件 5. 配置web.xml servlet 6. 访问 package com.diancai.test; im ...

  3. Cxf soap协议改成1.2

    在和.net做联调的时候,报错: A SOAP 1.2 message is not valid when sent to a SOAP 1.1 only endpoint. 看来是soap协议不匹配 ...

  4. 使用Spring和Tomcat发布CXF SOAP WebService

    上一节中使用代理工厂JaxWsProxyFactoryBean来发布WebService, 这种方式必须指定运行的端口,如果端口被占用,就会发布失败. cxf的WebService也可利用Tomcat ...

  5. cxf的soap风格+spirng4+maven 客户端

    上篇博客介绍了,cxf的soap风格的服务端,现在我们写客户端来调用 1.pom.xml <project xmlns="http://maven.apache.org/POM/4.0 ...

  6. 使用CXF实现基于Soap协议的WebService

    本文介绍使用CXF实现基于Soap协议的WebService(CXF的版本是3.0.0) 一. 前言 Java有三种WebService规范:Jax-WS,Jax-RS,Jaxm 1. Jax-WS( ...

  7. CXF、Spring整合的SOAP Web Service服务端

    1.建工程,导入CXFjar包 2.服务接口 package com.cxf.soap; import java.util.List; import javax.jws.WebService; @We ...

  8. 读《架构探险——从零开始写Java Web框架》

    内容提要 <架构探险--从零开始写Java Web框架>首先从一个简单的 Web 应用开始,让读者学会如何使用 IDEA.Maven.Git 等开发工具搭建 Java Web 应用:接着通 ...

  9. Resin文档阅读笔记

    阅读文档对应的版本为Resin4.0,且基本只关注Standard版本的功能. 1.Resin可以注册为服务: To install the service, use C:/> resin-3. ...

随机推荐

  1. 标准库bind函数中使用占位符placeholders

    placeholders ,占位符.表示新的函数对象中参数的位置.当调用新的函数对象时,新函数对象会调用被调用函数,并且其参数会传递到被调用函数参数列表中持有与新函数对象中位置对应的占位符. 举个例子 ...

  2. 【前端】用百度BAE和express部署自己的node后台

    转载请注明出处:http://www.cnblogs.com/shamoyuu/p/node_bae.html 百度有一个应用引擎,价格非常便宜,Java的tomcat每天4毛钱,node每天2毛钱, ...

  3. Xamarin.Android 调用Web Api(通过ListView展示远程获取的数据)

    xamarin.android如何调用sqlserver 数据库呢(或者其他的),很多新手都会有这个疑问.xamarin.android调用远程数据主要有两种方式: 在Android中保存数据或调用数 ...

  4. Android HTTP请求用HttpUrlConnection与HttpClient比较

    在安卓和JAVA应用开发中需要访问网络,少不了要提交HTTP请求,而基本上目前有两个实现方式:HttpUrlConnection(即URL.openConnection)和HttpClient. 网上 ...

  5. win8.1中安装rabbitmq

    项目测试的时候,用的是项目组linux测试机上的rabbitmq,为了方便自己随时使用,便在自己的电脑win8.1上也安装了一套,安装过程如下: 一.准备erlang和rabbitmq的安装程序:   ...

  6. 致IT同仁 — IT人士常犯的17个职场错误

    记得刚参加工作时(那是97年),中国的IT刚刚兴起,那时,作为一个IT人士是一件很光荣的事,而那时的我正在做电气和电子相关的工作.99年第一次跳槽,进入了IT行业做软件开发.至今,中国的IT已经走过了 ...

  7. mac下安装ELK

    本文主要为自己所走弯路而做的补充,对小白(比如我)来讲某些博客讲的还是高深了,特地来此补充说明一些东西. 主要步骤参考http://blog.csdn.net/ywheel1989/article/d ...

  8. WPF基础篇之命名空间

    WPF中XAML与C#一样,也有自己独立的编译器.XAML会被解析和编译,最终形成微软的中间语言存储在程序集中.在解析和编译XAML的语言过程中,我们经常需要告诉编译器一些重要的信息,比如XAML代码 ...

  9. C#中(int)、Conver.Toint32()、int.Parse()三种类型转换方式的区别与联系--C#基础知识

    自己刚学习C#,总结了一些知识,想分享给大家.毕竟刚学习这门语言,学得不深,如果哪里有错误,请帮忙指出一下哈,谢谢! 1.(int)可用于单精度.双精度等其他数值类型的转换(到整型int),不能用于转 ...

  10. I/HwPointEventFilter: do not support AFT because of no config

    I/HwPointEventFilter: do not support AFT because of no config 这是华为对系统做了修改,默认不打印日志,要改配置 在拨号界面输入:以下进入工 ...