CXF で WS-Security を試してみる
前回の続き。WS-Security(まだよくわかってない)でクライアントとサービスプロバイダの間で認証情報をやりとりしてみる。前回のTODOではログインしたユーザの情報を渡す予定だったけど、固定のユーザ名・パスワードを渡すところまでしかできなかった。
サービス実装側の修正
最初に作った ws-server プロジェクトで、WS-Security関係のクラスを使うために、まず以下の依存関係をpom.xmlに追加。
<dependency> <groupId>org.apache.cxf</groupId> <artifactId>cxf-rt-ws-security</artifactId> <version>${cxf.version}</version> <exclusions> <exclusion> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> </exclusion> </exclusions> </dependency>
さらに、src/main/resources/applicationContext.xml ファイルの jaxws:endpoint 要素に jaxws:inInterceptors を追加して以下のようにする。
<jaxws:endpoint id="helloWorld" implementor="example.cxf.HelloWorldServiceImpl" address="/HelloWorld"> <jaxws:inInterceptors> <bean class="org.apache.cxf.ws.security.wss4j.WSS4JInInterceptor"> <constructor-arg> <map> <entry key="action" value="UsernameToken" /> <entry key="passwordType" value="PasswordDigest" /> <entry key="passwordCallbackClass" value="example.cxf.ServerPasswordCallback" /> </map> </constructor-arg> </bean> </jaxws:inInterceptors> </jaxws:endpoint>
- WS-Security関係の処理は全てWSS4JInInterceptorというインターセプタが行ってくれるらしい。
- actionは、SOAPメッセージを受け取って何をするか。UsernameTokenは、単純なユーザ名・パスワードによる確認?
- passwordTypeは、actionがUsernameTokenの場合のパスワードのエンコーディング方法。
passwordCallbackClass は説明が難しい。とりあえず src/main/java/example/cxf/ServerPasswordCallback.java に以下のように作ると、
package example.cxf; 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.ws.security.WSPasswordCallback; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class ServerPasswordCallback implements CallbackHandler { public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { WSPasswordCallback pc = (WSPasswordCallback) callbacks[0]; pc.setPassword("foobarbaz"); } }
"foobarbaz" というパスワードがクライアントから送られてくればOKということになるらしい。pc.getIdentifier() で送られてきたユーザ名も取得できたりする。
クライアント側の修正
前回作った ws-webclient プロジェクトで、WS-Security関係のクラスを使うために、さっきと同じく以下の依存関係をpom.xmlに追加。
<dependency> <groupId>org.apache.cxf</groupId> <artifactId>cxf-rt-ws-security</artifactId> <version>${cxf.version}</version> <exclusions> <exclusion> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> </exclusion> </exclusions> </dependency>
さらに、src/main/resources/applicationContext.xml ファイルの jaxws:client 要素の outInterceptors にWSS4JOutInterceptorを追加して以下のようにする。
<jaxws:client id="helloWorldService" address="http://localhost:8080/ws-server/HelloWorld" serviceClass="example.cxf.HelloWorldService"> <jaxws:features> <wsa:addressing xmlns:wsa="http://cxf.apache.org/ws/addressing" /> </jaxws:features> <jaxws:inInterceptors> <ref local="cxfLogInbound" /> </jaxws:inInterceptors> <jaxws:inFaultInterceptors> <ref local="cxfLogInbound" /> </jaxws:inFaultInterceptors> <jaxws:outInterceptors> <!-- ここから追加 --> <bean class="org.apache.cxf.ws.security.wss4j.WSS4JOutInterceptor"> <constructor-arg> <map> <entry key="action" value="UsernameToken" /> <entry key="user" value="ws-client" /> <entry key="passwordType" value="PasswordDigest" /> <entry key="passwordCallbackClass" value="example.cxf.ClientPasswordCallback" /> </map> </constructor-arg> </bean> <!-- ここまで追加 --> <ref local="cxfLogOutbound" /> </jaxws:outInterceptors> <jaxws:outFaultInterceptors> <ref local="cxfLogOutbound" /> </jaxws:outFaultInterceptors> </jaxws:client>
- こちらも、WS-Security関係の処理は全てWSS4JOutInterceptorというインターセプタが行ってくれるらしい。さっきはInでこっちはOut。認証情報を受け取る側と送り出す側。
- action, passwordType はサービス側とあわせる。
- user は、ユーザ名として指定する値(ここを可変にする方法が不明。JaxWsProxyFactoryBean からJavaコードで構築するしかない?)。
passwordCallbackClass もサービス側と対になっている。とりあえず src/main/java/example/cxf/ClientPasswordCallback.java に以下のように作ると、
package example.cxf; 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.ws.security.WSPasswordCallback; public class ClientPasswordCallback implements CallbackHandler { public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { WSPasswordCallback pc = (WSPasswordCallback) callbacks[0]; pc.setPassword("foobarbaz"); } }
"foobarbaz" というパスワードを、さっきの user で指定したユーザ名と一緒に、SOAPメッセージに付加してくれる。
実行
あとは、前回と同じくAjaxアプリを動かすだけ。ログを見るとサービス側で以下のようなSOAPメッセージを受け取っているのが確認できる。
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"> <soap:Header> <wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" soap:mustUnderstand="1"> <wsse:UsernameToken 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" wsu:Id="UsernameToken-1"> <wsse:Username>ws-client</wsse:Username> <wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest">lgdlK6B5YUhVYoPHo2aC5YgPo50=</wsse:Password> <wsse:Nonce EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary">dgz+1ogsuo+xBSFQ7q26IA==</wsse:Nonce> <wsu:Created>2009-11-30T17:55:30.096Z</wsu:Created> </wsse:UsernameToken> </wsse:Security> </soap:Header> <soap:Body> <ns2:sayHi xmlns:ns2="http://cxf.example/"> <arg0>hoge</arg0> </ns2:sayHi> </soap:Body> </soap:Envelope>
この手順で作ったようにクライアント側とサーバ側のパスワード(foobarbaz)が一致していれば普通に動作するし、パスワードを異なるものにすると以下のようなFaultメッセージが返ってきて失敗する。
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"> <soap:Body> <soap:Fault> <faultcode xmlns:ns1="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">ns1:FailedAuthentication</faultcode> <faultstring>The security token could not be authenticated or authorized</faultstring> </soap:Fault> </soap:Body> </soap:Envelope>
目標を達成できてないのであまりめでたくないけど時間切れ。
この手順は Apache CXF Tutorial – WS-Security with Spring | Ben McCann を元にしてます。元記事では SAAJInInterceptor や SAAJOutInterceptor もインターセプターのリストに追加しているけど、CXF公式サイト掲示のXMLのコメントにある通り、現在のバージョンでは不要みたい。
TODO
- 単なる文字列でなくオブジェクトのやりとりをする
- サービス側をDBを使うような本格的なものにする
- クライアント側にユーザ認証を追加し、さらに認証情報をサービス側に受け渡す
- サービスのバージョニング
というか、WS-SecurityとかJAASとかをもっと勉強しないとわけがわからないな……寝る。