經過上一篇文章,相信您已經學會了如何使用 CXF 開發基於 SOAP 的 WS 了。或許您目前對於底層原理性的東西還不太理解,心中不免會有些疑問:html
什麼是 WSDL?java
什麼是 SOAP?spring
如何能讓 SOAP 更加安全?shell
我將努力經過本文,針對以上問題,讓您獲得一個滿意的答案。apache
還等什麼呢?就從 WSDL 開始吧!瀏覽器
WSDL 的全稱是 Web Services Description Language(Web 服務描述語言),用於描述 WS 的具體內容。安全
當您成功發佈一個 WS 後,就能在瀏覽器中經過一個地址查看基於 WSDL 文檔,它是一個基於 XML 的文檔。一個典型的 WSDL 地址以下:服務器
http://localhost:8080/ws/soap/hello?wsdloracle
注意:WSDL 地址必須帶有一個 wsdl
參數。框架
在瀏覽器中,您會看到一個標準的 XML 文檔:
其中,definitions
是 WSDL 的根節點,它包括兩個重要的屬性:
提示:能夠在 javax.jws.WebService
註解中配置以上兩個屬性值,但這個配置必定要在 WS 實現類上進行,WS 接口類只需標註一個 WebService 註解便可。
在 definitions 這個根節點下,有五種類型的子節點,它們分別是:
其中包括了兩個重要信息:
提示:可在 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 消息的結構有着殊途同歸之妙!
看到這裏,您或許會有不少疑問:
沒錯!這就是咱們今天要展開討論的話題 —— 基於 SOAP 的安全控制。
在 WS 領域有一個很強悍的解決方案,名爲 WS-Security
,它僅僅是一個規範,在 Java 業界裏有一個很權威的實現,名爲 WSS4J。
下面我將一步步讓您學會,如何使用 Spring
+ CXF
+ WSS4J
實現一個安全可靠的 WS 調用框架。
其實您須要作也就是兩件事情:
怎樣對 WS 進行身份認證呢?可以使用以下解決方案:
第一步:添加 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> <ref bean="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 的構造器參數作一個說明。
如下即是 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:client id="helloService" serviceClass="demo.ws.soap_spring_cxf_wss4j.HelloService" address="http://localhost:8080/ws/soap/hello"> <jaxws:outInterceptors> <ref bean="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(服務端與客戶端都須要作一樣的修改),那麼就會看到一個加密過的密碼:
除了這種基於用戶名與密碼的身份認證之外,還有一種更安全的身份認證方式,名爲「數字簽名」。
數字簽名從字面上理解就是一種基於數字的簽名方式。也就是說,當客戶端發送 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 消息中的數據呢?
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 響應進行簽名與加密呢?相信您必定有辦法作到,不妨親自動手試一試吧!
本文的內容有些多,確實須要稍微總結一下:
關於「SOAP 安全控制」也就這點事兒了,但關於「WS 那點事兒」還並無結束,由於 RESTful Web Services 在等着您。如何發佈 REST 服務?如何對 REST 服務進行安全控制?咱們下次再見!