在 Web 容器中使用 Spring + CXF 發佈 WS(二) --SOAP 及其安全控制

一 WSDL ,SOAP基本概念

經過上篇已經基本掌握了使用CXF開發基於SOAP的WS.在此基礎上了解一下WSDL,SOAP等一些常見的術語.html

WSDL 的全稱是 Web Services Description Language(Web 服務描述語言),用於描述 WS 的具體內容。java

當成功發佈一個 WS 後,就能在瀏覽器中經過一個地址查看基於 WSDL 文檔,它是一個基於 XML 的文檔。一個典型的 WSDL 地址以下:node

http://localhost:8080/webservice/testService?wsdl

其中的?wsdl必須帶上,才能返回一個基於xml的文檔.web

一個典型的wdsl文檔以下:spring

<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 的根節點,它包含兩個重要的屬性:數據庫

  1. name:WS 名稱,默認爲「WS 實現類 + Service」,例如:HelloServiceImplService
  2. targetNamespace:WS 目標命名空間,默認爲「WS 實現類對應包名倒排後構成的地址」,例如:http://soap_spring_cxf.ws.demo/
    *能夠在 javax.jws.WebService 註解中配置以上兩個屬性值,但這個配置必定要在 WS 實現類上進行,WS 接口類只需標註一個 WebService 註解便可。

在 definitions 這個根節點下,有五種類型的子節點,它們分別是:apache

  1. types:描述了 WS 中所涉及的數據類型
  2. portType:定義了 WS 接口名稱(endpointInterface)及其操做名稱,以及每一個操做的輸入與輸出消息
  3. message:對相關消息進行了定義(供 types 與 portType 使用)
  4. binding:提供了對 WS 的數據綁定方式
  5. service:WS 名稱及其端口名稱(portName),以及對應的 WSDL 地址.

    其中包括了兩個重要信息:瀏覽器

  6. <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的相關資料及可能的應用場景,在實際的應用中可能會有如下的需求:

  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包依賴

<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>-->
                            <!--&lt;!&ndash; action:UsernameToken使用基於「用戶名令牌」的方式進行身份認證&ndash;&gt;-->
                            <!--<entry key="action" value="UsernameToken"/>-->
                            <!--&lt;!&ndash;密碼加密策略.PasswordText:明文密碼 ;PasswordDigest 密文密碼&ndash;&gt;-->
                            <!--<entry key="passwordType" value="PasswordDigest"/>-->
                            <!--&lt;!&ndash;服務端別名 ,可不指定&ndash;&gt;-->
                            <!--<entry key="user" value="cxfServer"/>-->
                            <!--&lt;!&ndash;提供一個用於密碼驗證的回調處理器&ndash;&gt;-->
                            <!--<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

相關文章
相關標籤/搜索