博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
在 Web 容器中使用 Spring + CXF 发布 WS(二) --SOAP 及其安全控制
阅读量:6004 次
发布时间:2019-06-20

本文共 13932 字,大约阅读时间需要 46 分钟。

hot3.png

一 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文档如下:

162842_tmAR_3406827.png

...
...
...
...
...
...
...
...

其中,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 地址.

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

  6.  

     portName:WS 的端口名称,默认为“WS 实现类 + Port”,例如:HelloServiceImplPort

    endpointInterface:WS 的接口名称,默认为“WS 实现类所实现的接口+Service”,例如:MyServiceImplService.

如果说wsdl只是一个描述文档的话,那SOAP就是具体的调用内容了.

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

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

 

查阅WS的相关资料及可能的应用场景,在实际的应用中可能会有以下的需求:

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

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

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

将以上的需求抽象为以下步骤:

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

 二 WS的请求身份认证

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

    添加Jar包依赖

org.apache.cxf
cxf-rt-ws-security
${cxf.version}

    服务端的cxf-servlet.xml 配置文件

首先定义了一个基于 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;@Componentpublic class ServerPasswordCallback implements CallbackHandler {    /**     * 假定userMap为存放的客户端和服务端的名称和密码,     * 在实际应用场景中可以使用数据库等存储机制来验证用户名和密码组合.     */    private static final Map
userMap = new HashMap
(); 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配置

与服务端的配置类似,注释可以在服务端中找.

客户端的密码设置回调函数.

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;@Componentpublic 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中不存在该键则抛出异常:

110012_tRYr_3406827.png

当密码和用户名都正确时,可以在控制台看到发送的SOAP信息

cxfClient
clientpass
张三

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

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

对于上面的这种方式,根据SOAP的信息可以看出,加密的过程实际上是在SOAP的Header头部加上了验证信息,我们也可以采用另外的一种方式直接在header头部加上验证信息

服务端的cxf-servlet.xml配置

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
{ //消息输入拦截 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配置

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
headers=message.getHeaders(); headers.add(head); }}

测试同样采用上面的单元测试.发送的SAOP信息

140134_OsWy_3406827.png

Header节点下即为添加的SAOP验证信息

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

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

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

参考Apache CXF官网帮助文档上的介绍:()

141501_rXTe_3406827.png

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

库里所存放的信息是这样的:

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

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

 

新建keystore.bat,使用 JDK 提供的 keytool 命令行工具创建数字证书

@echo off keytool -genkeypair -alias server -keyalg RSA -dname "cn=server" -keypass serverpass -keystore server_store.jks -storepass storepasskeytool -exportcert -alias server -file server_key.rsa -keystore server_store.jks -storepass storepasskeytool -importcert -alias server -file server_key.rsa -keystore client_store.jks -storepass storepass -nopromptdel server_key.rsa keytool -genkeypair -alias client -dname "cn=client" -keyalg RSA -keypass clientpass -keystore client_store.jks -storepass storepasskeytool -exportcert -alias client -file client_key.rsa -keystore client_store.jks -storepass storepasskeytool -importcert -alias client -file client_key.rsa -keystore server_store.jks -storepass storepass -nopromptdel client_key.rsa

运行该批处理程序,将生成两个文件:server_store.jks 与 client_store.jks,随后将 server_store.jks 放入服务端的 classpath 下,将 client_store.jks 放入客户端的 classpath 下.

服务端cxf配置

 

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

org.apache.ws.security.crypto.provider=org.apache.ws.security.components.crypto.Merlinorg.apache.ws.security.crypto.merlin.file=server_store.jksorg.apache.ws.security.crypto.merlin.keystore.type=jksorg.apache.ws.security.crypto.merlin.keystore.password=storepass

客户端配置:

//.......相同省略
//.....省略

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

org.apache.ws.security.crypto.provider=org.apache.ws.security.components.crypto.Merlinorg.apache.ws.security.crypto.merlin.file=client_store.jksorg.apache.ws.security.crypto.merlin.keystore.type=jksorg.apache.ws.security.crypto.merlin.keystore.password=storepass

通过单元测试结果如下:

154134_UvU7_3406827.png

3.SOAP消息的签名与加/解密

WSS4J 除了提供签名与验签(Signature)这个特性以外,还提供了加密与解密(Encrypt)功能.

服务端:

161414_dTsW_3406827.png

客户端:

161451_M1hQ_3406827.png

其中的回调函数与第一章节中相同.可以看到,发送的消息也被加密了.

161256_MCM9_3406827.png

参考文献:https://my.oschina.net/huangyong/blog/287791

 

转载于:https://my.oschina.net/u/3406827/blog/911982

你可能感兴趣的文章
python实现汉诺塔
查看>>
centos7安装 jupyter
查看>>
MySQL高可用架构之MHA
查看>>
在使用Windows时防止电脑死机的技巧
查看>>
Druid Monitor监控JavaSE和JavaWeb
查看>>
voip工程项目介绍
查看>>
我的友情链接
查看>>
DataTable创建行和列,DataReader读取
查看>>
我的友情链接
查看>>
bat记录远程桌面连接登录信息
查看>>
英勇行动海豹突击队 感
查看>>
我的友情链接
查看>>
ActiveMQ集群应用
查看>>
Exchange Server 2010系列之十七:运维中的故障诊断小工具的使用简介
查看>>
centos7安装指定版本docker且使用本地docker 仓库
查看>>
JAVA学习笔记之常用列表(备查询)
查看>>
使用Storyboard进行界面跳转及传值
查看>>
MVC 3.0 搞笑网站
查看>>
webstorm启动vue项目配置
查看>>
Spring Bood 配置OAuth2 ClientDetailsServiceConfigurer
查看>>