CXF と Spring Security でWebServiceのBASIC認証

前回 の続き。WS-Securityはちょっと大げさだし重いという噂なので、もっと単純にHTTPのBASIC認証を試してみる。

前回はWebサービス自身が認証を行う形式だったけど、今回はWebサービスの前に Spring Security のフィルタをかませて、認証は Spring Security に任せる方式にする(その方が慣れているし色々取り替えが利くので)。さらに、クライアント側のWebアプリケーションが送りつけるBASIC認証のユーザ名・パスワードは、Webアプリケーションへログインしたときのユーザ名・パスワードをそのまま使うようにする。

サービス実装側の修正

前回追加したWS-Securityの部分は取り外して、Spring Securityを使えるように設定する。

まず、pom.xml。dependencies要素の中に、Spring Securityに必要な以下の設定を追加。

    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-jdbc</artifactId>
      <version>2.5.6.SEC01</version>
      <exclusions>
        <exclusion>
          <groupId>commons-logging</groupId>
          <artifactId>commons-logging</artifactId>
        </exclusion>
      </exclusions>
    </dependency>
    <dependency>
      <groupId>org.springframework.security</groupId>
      <artifactId>spring-security-core-tiger</artifactId>
      <version>2.0.5.RELEASE</version>
      <exclusions>
        <exclusion>
          <groupId>commons-logging</groupId>
          <artifactId>commons-logging</artifactId>
        </exclusion>
      </exclusions>
    </dependency>

さらに、src/main/resources/applicationContext-security.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:sec="http://www.springframework.org/schema/security"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
            http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-2.0.4.xsd
       ">

  <sec:http auto-config="false" create-session="never" realm="Web Services Realm">
    <sec:http-basic />
    <sec:intercept-url pattern="/**" access="ROLE_USER" />
  </sec:http>

  <sec:authentication-provider>
    <sec:user-service>
      <sec:user name="foo" password="bar" authorities="ROLE_USER" />
    </sec:user-service>
  </sec:authentication-provider>

</beans>
  • sec:http-basic 要素によりBASIC認証機能(HTTPのAuthorizationヘッダからのユーザ名・パスワードの読み取り機能)が追加される
  • sec:intercept-url 要素で、全てのパスに対して ROLE_USER 権限を要求するよう設定
  • sec:http 要素の auto-config でBASIC認証以外の余計な認証機能を構成しないよう設定、create-session でHttpSessionを作成しないよう設定(とりあえず設定してみたけど、ifRequiredでもいい?未確認)、realm でレルム名を設定(BASIC認証なら、ブラウザでアクセスしたときにダイアログに出るやつ)
  • sec:authentication-provider 要素で、固定のfoo/barというユーザ名・パスワードを受け付けて ROLE_USER という権限を付与するよう設定

最後に web.xml を変更し、Spring Security の設定ファイルも読んでくれるように設定ファイル指定にアスタリスクをつけて、フィルタの設定をする。

  <!-- アスタリスクをつける -->
  <context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:applicationContext*.xml</param-value>
  </context-param>

(中略)

  <!-- 追加 -->
  <filter>
    <filter-name>springSecurityFilterChain</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
  </filter>

  <!-- 追加 -->
  <filter-mapping>
    <filter-name>springSecurityFilterChain</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>

サービス実装側はこれだけ。核となるサービス部分はそのままで Spring Security の皮をかぶせただけって感じ。

ああ、あと動作確認用に src/main/resources/logback.xml に以下も追加しておこう。

  <logger name="org.springframework.security">
    <level value="TRACE" />
  </logger>

この変更後、ブラウザで http://localhost:18080/ws-server/ にアクセスすると認証を求められ、foo/bar で認証をパスできるのを確認する。

クライアント側の修正(ログイン機能追加)

クライアント側のWebアプリケーションプロジェクト ws-webclient に、Spring Securityによるログイン機能を追加する。この手順はWebサービスとはあまり関係ない。

こちらもまず pom.xml。サーバ側と同じように、dependencies要素の中に、Spring Securityに必要な以下の設定を追加。

    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-jdbc</artifactId>
      <version>2.5.6.SEC01</version>
      <exclusions>
        <exclusion>
          <groupId>commons-logging</groupId>
          <artifactId>commons-logging</artifactId>
        </exclusion>
      </exclusions>
    </dependency>
    <dependency>
      <groupId>org.springframework.security</groupId>
      <artifactId>spring-security-core-tiger</artifactId>
      <version>2.0.5.RELEASE</version>
      <exclusions>
        <exclusion>
          <groupId>commons-logging</groupId>
          <artifactId>commons-logging</artifactId>
        </exclusion>
      </exclusions>
    </dependency>


で、Spring Security 用の設定ファイルを src/main/resources/applicationContext-security.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:sec="http://www.springframework.org/schema/security"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
            http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-2.0.4.xsd
       ">

  <sec:http auto-config="true">
    <sec:intercept-url pattern="/faces/login.xhtml" filters="none" />
    <sec:intercept-url pattern="/**" access="ROLE_USER" />
    <sec:form-login authentication-failure-url="/faces/login.xhtml?failure=true" login-page="/faces/login.xhtml" />
    <sec:logout logout-success-url="/faces/login.xhtml" />
  </sec:http>

  <sec:authentication-provider>
    <sec:user-service>
      <sec:user name="foo" password="bar" authorities="ROLE_USER" />
      <sec:user name="hoge" password="password" authorities="ROLE_USER" />
    </sec:user-service>
  </sec:authentication-provider>

</beans>
  • sec:intercept-url 要素で、ログインページ login.xhtml は認証チェックの対象外とし、それ以外は全部 ROLE_USER という権限を必須に
  • sec:form-login 要素で、ログインページを /faces/login.xhtml (コンテキストパスからの相対パス)に指定
  • sec:logout 要素で、ログアウト後に遷移するページを /faces/login.xhtml に指定
  • sec:authentication-provider 要素で認証プロバイダを指定。とりあえず確認用に、 foo/bar、hoge/password という2つの固定のユーザ名・パスワードの組でログインできるようにする。

ログイン画面を src/main/webapp/login.xhtml に作成。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Login</title>
</head>
<body>

<form action="#{request.contextPath}/j_spring_security_check" method="post">
User: <input type="text" name="j_username" size="20" /><br/>
Pass: <input type="password" name="j_password" size="20" /><br/>
<input type="submit" />
</form>

</body>
</html>

元々あった src/main/webapp/index.xhtml にログアウトリンクも一応追加しておく。

(略)
<body>

<!-- ページ上部にログアウトリンクを追加 -->
<p><a href="#{request.contextPath}/j_spring_security_logout">Logout</a></p>

<form jsfc="h:form">
(略)


最後に、サーバ側と同じく web.xml を変更し、Spring Security の設定ファイルも読んでくれるように設定ファイル指定にアスタリスクをつけて、フィルタの設定をする。

  <!-- アスタリスクをつける -->
  <context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:applicationContext*.xml</param-value>
  </context-param>

(中略)

  <!-- 追加 -->
  <filter>
    <filter-name>springSecurityFilterChain</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
  </filter>

  <!-- 追加 -->
  <filter-mapping>
    <filter-name>springSecurityFilterChain</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>

クライアント側の修正(サービス呼び出しのBASIC認証対応)

前述の手順でクライアント側のWebアプリケーションにログイン機能が追加されたので、さらに CXF を使ったWebサービスクライアントをBASIC認証対応させる。

まず、CXFの設定をしてあった src/main/resources/applicationContext.xml に、WebサービスのトランスポートとしてHTTPを使う場合の設定を追加する。

<?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"
  xmlns:wsa="http://cxf.apache.org/ws/addressing"
  xmlns:http="http://cxf.apache.org/transports/http/configuration"
  xsi:schemaLocation="
    http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.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
    http://cxf.apache.org/ws/addressing http://cxf.apache.org/schemas/ws-addr-conf.xsd
    http://cxf.apache.org/transports/http/configuration http://cxf.apache.org/schemas/configuration/http-conf.xsd
    ">

  (中略)

  <http:conduit name="*.http-conduit">
    <http:basicAuthSupplier
      class="example.cxf.MyHttpBasicAuthSupplier" />
  </http:conduit>
  • xmlns:http="http://cxf.apache.org/transports/http/configuration とかを追加してHTTPトランスポートの設定ができるようにする
  • http:conduit 要素を追加して、すべてのエンドポイントへのHTTPトランスポートに対して MyHttpBasicAuthSupplier を適用するようにする

で、この MyHttpBasicAuthSupplier を実装する。basicAuthSupplier に指定するクラスを実装するには、HttpBasicAuthSupplierクラス を継承すればいい。

クライアント側Webアプリケーションで Spring Security を使うようにしたので、ログイン時に入力された認証情報を SecurityContextHolder から得ることができる。

ということで、src/main/java/example/cxf/MyHttpBasicAuthSupplier.java を以下のように作成。

package example.cxf;

import java.net.URL;

import org.apache.cxf.message.Message;
import org.apache.cxf.transport.http.HttpBasicAuthSupplier;
import org.springframework.security.Authentication;
import org.springframework.security.context.SecurityContextHolder;

public class MyHttpBasicAuthSupplier extends HttpBasicAuthSupplier {

    @Override
    public UserPass getPreemptiveUserPass(String conduitName, URL currentURL, Message message) {
        // Authorizationヘッダに指定するユーザ名・パスワードを持つ UserPass を作成し、返すことが求められている
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        return createUserPass(auth.getName(), auth.getCredentials().toString());
    }

    @Override
    public UserPass getUserPassForRealm(String conduitName, URL currentURL, Message message,
            String realm) {
        // とりあえずレルムは無視
        return getPreemptiveUserPass(conduitName, currentURL, message);
    }
}

確認

これで完成したので確認してみる。

Webアプリケーションにアクセスして、foo/bar でログインしてから greet ボタンを押すと、正しくメッセージが返ってくる。サービスプロバイダ側のログは以下のような感じ。HTTPリクエストにAuthorizationヘッダが付与されているのが確認できる。

2009/12/12 17:37:40 org.apache.cxf.interceptor.LoggingInInterceptor logging
情報: Inbound Message
----------------------------
ID: 1
Address: /ws-server/HelloWorld
Encoding: UTF-8
Content-Type: text/xml; charset=UTF-8
Headers: {cache-control=[no-cache], content-type=[text/xml; charset=UTF-8], connection=[keep-alive], host=[localhost:18080], Authorization=[Basic Zm9vOmJhcg==], Content-Length=[229], SOAPAction=[""], user-agent=[Apache CXF 2.2.5], Content-Type=[text/xml; charset=UTF-8], Accept=[*/*], pragma=[no-cache]}
Payload: <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Body><ns2:sayHi xmlns:ns2="http://cxf.example/"><user><firstName>moge</firstName><lastName>Test</lastName></user></ns2:sayHi></soap:Body></soap:Envelope>
--------------------------------------
2009/12/12 17:37:40 org.apache.cxf.interceptor.LoggingOutInterceptor$LoggingCallback onClose
情報: Outbound Message
---------------------------
ID: 1
Encoding: UTF-8
Content-Type: text/xml
Headers: {}
Payload: <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Body><ns2:sayHiResponse xmlns:ns2="http://cxf.example/"><return><text>Hello moge Test</text></return></ns2:sayHiResponse></soap:Body></soap:Envelope>
--------------------------------------

で、hoge/password という、Webアプリケーション側ではログインできるけど、サービスプロバイダ側で対応していないユーザ名・パスワードでログインしてから greet ボタンを押すと、サービスプロバイダ側で Spring Security によって拒否されているのが確認できる。CXFまで到達していないのでメッセージのログは出ていない。

[Server] 17:41:06 [http-18080-1] DEBUG o.s.s.u.b.BasicProcessingFilter - Authorization header: Basic aG9nZTpwYXNzd29yZA==
[Server] 17:41:06 [http-18080-1] DEBUG o.s.s.providers.ProviderManager - Authentication attempt using org.springframework.security.providers.dao.DaoAuthenticationProvider
[Server] 17:41:06 [http-18080-1] DEBUG o.s.s.u.b.BasicProcessingFilter - Authentication request for user: hoge failed: org.springframework.security.BadCredentialsException: Bad credentials

このテストでは固定のユーザ名・パスワードを設定ファイルに指定しているけど、Spring Securityなら認証プロバイダを差し替えるだけでDBに入っているユーザ情報を使ったり、SSOしたりも容易。WS-Securityでメッセージ内に認証情報を含めるよりは簡単で柔軟そうな気がするのでこっちのほうが良さそう。

残りTODO

  • 単なる文字列でなくオブジェクトのやりとりをする
  • サービス側をDBを使うような本格的なものにする
  • サービスのバージョニング

1は単に sayHi メソッドの引数を String から自分で作ったクラスにすればいいだけだったのでTODOにするほどでもなかった。2は特にWebサービスとは関係ないし、あとは3だなぁ。