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>

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とかをもっと勉強しないとわけがわからないな……寝る。