読者です 読者をやめる 読者になる 読者になる

M12i.

学術書・マンガ・アニメ・映画の消費活動とプログラミングについて

Spring Security 3.1 リファレンス - テクニカル・サマリ (1)

Java Spring

先日、Spring Securityについての拙い翻訳を掲載したのに続いて、今回はその記事でも言及されていたリファレンスマニュアルの“Technical Overview”のチャプターを訳出してみます。いずれにしてもちょっと長いので、まずは前半のみ。(後半はこちら。)

原典は、“6. Technical Overview”(Spring Security Reference Documentation)で、Ben Alex氏とLuke Taylor氏によるものです。

* * *

6. テクニカル・サマリ (1)

6.1 実行環境

Spring Security 3.0には、Java実行環境のバージョン5.0もしくはそれ以上が必要になります。Spring Securityは自己完結型で動作することを目的としているので、Java実行環境上に特殊な設定ファイルなどを配置する必要はありません。とくに、Java Authentication and Authorization Service(JAAS)〔Java標準の認証API〕のポリシーファイルを特別に設定したり、Spring Securityを共通クラスパス上に配置したりする必要はありません。

同様に、もしあなたがEJBコンテナやサーブレット・コンテナを使用してアプリケーション開発をしているとしても、特別な設定ファイルをどこかに配置しなくてはならないとか、Spring Securityライブラリをサーバのクラス・ローダにロードさせなくてはならないとか、そういったことはありません。すべての必要なファイルはあなたのアプリケーション内に閉じたかたちで内包されることになります。

これによりアプリケーションの配備時間の柔軟性は最大限確保されます。あなたはあるシステムから別のシステムへと単に対象のファイル(JARやWAR、EARなどです)をコピーして、システムを速やかに稼働させることができます。〔速度の問題はともかくとして必要なファイルがアプリケーション内に閉じ込められているためにポータビリティが高まります。〕

6.2 中核となるコンポーネント

Spring Security 3.0では、spring-security-core.jarファイルの中身は最小限のサイズに抑えられています。Webアプリケーション・セキュリティやLDAP名前空間設定〔namespace configuration〕に関連するコードはもはやそこに含まれていないのです。ここでは中核モジュールに含まれるいくつかのJavaの型をちょっと見てみましょう。フレームワークの骨格をなすそれらクラス群は、もし仮にいまのところは直接やり取りする必要がないのだとしても、いずれあなたがシンプルな名前空間設定の域を出てSpring Securityを使用していくとき、それらについて知っていることが重要になってきます。

6.2.1 SecurityContextHolder、SecurityContext、そしてAuthenticationオブジェクト

もっとも基本的なオブジェクトはSecurityContextHolderです。このオブジェクトはアプリケーションの現在のセキュリティ・コンテキスト〔security context〕の詳細情報──使用中のアプリケーションのプリンシパル〔認証対象となり、アクセス権付与の対象となるもの〕の詳細情報を含む──が格納されている場所です。デフォルトでは、SecurityContextHolderはそれら詳細情報を保存するのにThreadLocalオブジェクトを使用しています。これにより、仮に引数として明示的に渡されなくても、保護されたコンテキストは同じスレッド上で実行されるメソッドからであればいつでも利用可能となります。ThreadLocalを使用する方法は、現在のプリンシパルのもとでのリクエストの処理が終わった後スレッドが消去されるなら、非常に安全な方法です。もちろんSpring Securityはこれを自動で行いますから、あなたはこのことをとやかく心配する必要はありません。

しかしアプリケーションによっては、ThreadLocalを使用する方法が必ずしも適切なではないこともあります。 というのもこの方法では認証のメカニズムが〔まさに〕個々のスレッドとともに働くことになるからです。例えば、SwingアプリケーションではJava仮想マシン上で実行中のすべてのスレッドが同じセキュリティ・コンテキストを使う必要があるでしょう。SecurityContextHolderはその起動時の戦略次第で、どのようにしてコンテキストを保持すべきかを指定することができます。スタンドアローンのアプリケーションのためには、SecurityContextHolder.MODE_GLOBALの戦略を使うことになるでしょう。その他のアプリケーションには、同じセキュリティ・アイデンティティを持つと見なされるセキュアなスレッドから起動されたスレッド群が必要になるかも知れません。これはSecurityContextHolder.MODE_INHERITABLETHREADLOCALの戦略で実現できます。デフォルトのSecurityContextHolder.MODE_THREADLOCALの戦略を別のものに変更するには2つの方法が用意されています。その1つめはシステム・プロパティで設定するもので、2つめはSecurityContextHolderの静的メソッドを呼ぶものです。ほとんどのアプリケーションではデフォルトの設定を変更する必要はないでしょう。しかしもし必要ならば、JavadocでSecurityContextHolderについてより詳しく調べてみてください。

現在のユーザについて情報を得る

SecurityContextHolderの内部では、アプリケーションと現在やり取りをしている最中のプリンシパルの詳細情報が保持されています。Spring Securityはこの情報を表すのにAuthenticationオブジェクトを使用します。たいていはAuthenticationオブジェクトをわざわざ自分で作成する必要はないでしょうが、Authenticationオブジェクトを問い合わせる〔取得する〕というのはかなり一般的な処理になるでしょう。例えば、次に示すコードブロックにより、アプリケーション上のどこからでも、現在の認証済みユーザの名前を取得することができます:

Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();

if (principal instanceof UserDetails) {
  String username = ((UserDetails)principal).getUsername();
} else {
  String username = principal.toString();
}

getContext()メソッドにより返されるオブジェクトはSecurityContextインターフェースを実装したクラスのインスタンスです。これはスレッド・ローカルに保持されているオブジェクトです。このあと見ていくように、Spring Securityにおけるほとんどの認証メカニズムは、プリンシパルとしてUserDetailsのインスタンスを返します。

6.2.2 UserDetails Service

前掲のコード断片の中で注目すべきもうひとつの事項は、プリンシパルはAuthenticationオブジェクトから取得できるということです。プリンシパルは文字通り1つのオブジェクトです。〔型が決定されておらず、つまりは何でもそこに突っ込める、ということです。〕ほとんどのケースで、このオブジェクトはUserDetailsにキャストされることになるでしょう。UserDetailsはSpring Securityにおける中核インターフェースです。それはプリンシパルを体現するものですが、しかしその方法〔実装〕は拡張可能でアプリケーション固有のものとなります。UserDetailsはあなたのアプリケーションのユーザ・データベースとSpring SecurityがSecurityContextHolderの内側で必要とするものとを仲立ちするものと考えてください。ユーザ・データベースから取り出してきたなにがしかのものを表すそれを、アプリケーションのコード内では非常にしばしばアプリケーション固有のオブジェクトへとキャストして、〔そのアプリケーションが実行する〕ビジネス固有のメソッドを呼び出すことになるはずです。(例えばgetEmail()とかgetEmployeeNumber()とかそういう類いのものです。)

さてあなたは不思議に思っているかも知れません。〔それを取得する方法はわかったけれど〕UserDetailsオブジェクトはいつSpring Securityに渡すのだ? そしてそれはどのように? 筆者は「それは宣言的なものであって〔訳者:何が必要なのかが重要であって、それをどう実現するかはフレームワークが考えること、という意味か・・・〕いかなるJavaコードも記述する必要がなかった」と言っているようだけど、では一体何を与えたのか?──この疑問に対する簡潔な答えは、UserDetailsServiceという特別なインターフェースがあるのだ、ということです。このインターフェースはただ1つだけメソッドを持っていて、そのメソッドは引数としてString型でユーザ名をとり、戻り値としてUserDetailsを返します:

  UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; 

これこそフレームワーク内部でユーザ・データを読み込むために使用しているもっとも一般的な方法であり、ユーザ・データが必要になったときにいつも使用されることになる方法なのです。

認証が成功すると、UserDetailsオブジェクトはSecurityContextHolder に格納されるAuthenticationオブジェクトを組み立てるのに使用されます。(詳しくは6.3で後述します)さいわいUserDetailsServiceの実装は──インメモリ・マップを使用したもの(InMemoryDaoImpl)やJDBCを使用したもの(JdbcDaoImpl)など──あらかじめいくつも提供されています。ほとんどのユーザ〔開発者〕は自分の手を動かす必要はありませんし、そうでなくてもUserDetailsServiceの実装はしばしば“従業員”や“カスタマー”、あるいはその他のアプリケーション・ユーザ〔のエンティティ〕を表すDAO実装のほとんどそのままになるでしょう。いずれにせよ前掲のコード断片でやっているように、UserDetailsServiceが返すオブジェクトはいつでもSecurityContextHolderを通じて取得できるということを覚えておいてください。


ノート

UserDetailsServiceについては、よく混乱される方がいるようです。それは純粋にユーザ・データにアクセスするためのDAOであって、フレームワークのなかでそのデータを他のコンポーネントに供給するような機能は持っていません。とりわけても、このインターフェースはユーザを認証するようなことはしません。それはAuthenticationManagerの仕事です。多くの場合、もしカスタムメードの認証プロセスが必要ならば直接AuthenticationProviderを実装するのがいいでしょう。

6.2.3 GrantedAuthority

プリンシパルの話に戻りましょう。もう1つ重要なメソッドとしてAuthenticationオブジェクトはgetAuthorities()を提供しています。このメソッドはGrantedAuthorityオブジェクトの配列を返します。ご察しの通り、GrantedAuthorityはプリンシパルに対して付与された権限のことです。権限というのは、ROLE_ADMINISTRATOR やROLE_HR_SUPERVISORのような“ロール”のことです。これらの“ロール”は、Webサイト・リソースへのリクエストの権限制御、メソッド実行の権限制御、そしてドメイン・オブジェクトの権限制御の設定に基づくものです。Spring Securityの他の部分でもそれらの“ロール”を解釈することができ、それらが現にあることを前提としています。〔ロールによって処理を切り替える仕組みが組み込みで存在するということか。〕GrantedAuthorityオブジェクトはたいていの場合、UserDetailsServiceによって設定されます。

ふつうGrantedAuthorityオブジェクトが表すのはアプリケーションの全体にわたるパーミッションであって、特定のドメイン・オブジェクト〔へのアクセス〕に限定されたものではありません。例えばGrantedAuthorityがEmployeeオブジェクトの54番目のものに対するパーミッションを表すようなことは期待されないでしょう──こうした〔個々のインスタンスに対する〕権限が何千個とあったらあなたの脳内メモリはすぐに使い果たされてしまいます。(あるいは少なくとも、ユーザの認証を行うのにアプリケーションは長大な時間を使うことになるでしょう。)もちろん、Spring Securityはこの一般的な要件を実現することをとくに意図して設計されています。けれどもプロジェクトのドメイン・オブジェクトを対象としたセキュリティの機能も使うことはできます。

6.2.4 要約

これまで見てきたSpring Securityの骨格についてまとめましょう:

  • SecurityContextHolderは、SecurityContextへのアクセス手段を提供します。
  • SecurityContextは、Authenticationと場合によっては特定リクエストに関連するセキュリティ情報を保持します。
  • Authenticationは、Spring Security固有の慣習のなかでプリンシパルを表します。
  • GrantedAuthorityは、プリンシパルに付与されたアプリケーション全体にわたるパーミッションを反映します。
  • UserDetailsは、あなたのアプリケーションのDAOやその他から得られるセキュリティ情報に由来し、Authenticationオブジェクトを構築するために必要な情報を提供します。
  • UserDetailsServiceは、String型の引数としてユーザ名(もしくは認証IDやそれに類するもの)を受け取りUserDetailsを生成します。

さて、フレームワークで繰り返し使用されるコンポーネントについては理解できたと思います。これから認証プロセスをもっと詳しく見ていきましょう。

6.3 認証

Spring Securityは多くの異なる認証環境に適用できます。とはいえ、私たちはSpring Securityを認証のために使うように推奨しているのですが、既存のコンテナ管理型認証との統合についてはおすすめできません。たとえプロプライエタリな認証システムとの統合はサポートしているとはいえです。

6.3.1 Spring Securityにおける認証とは何か?

ここで皆さんもよく親しんでいると思われる標準的な認証のシナリオを見てみましょう。

  1. ユーザはユーザ名とパスワードの入力を求められます。
  2. システムは入力されたユーザ名に対してパスワードが正しいことを検証します。(そして検証に成功したとします。)
  3. ユーザについてのコンテキスト情報が得られます。(ロールのリストだとか、その類いです。)
  4. ユーザのセキュリティ・コンテキストが確立されます。
  5. ユーザはアクセス・コントロール機構により保護されている操作を実行できるようになります。コントロール機構は現在のユーザのセキュリティ・コンテキスト情報に、その操作を実行するのに必要なパーミッションがあるかどうかチェックします。

最初の3つの項目は認証プロセスを構成するものです。これらがSpring Securityの中でどのように遂行されているかを見ていきましょう。

  1. ユーザ名とパスワードが得られ、それらがUsernamePasswordAuthenticationToken(先に見たAuthenticationインターフェースの実装の1つです)のインスタンスのなかに格納されます。
  2. トークンは検証のためAuthenticationManagerインスタンスに渡されます。
  3. 認証に成功するとAuthenticationManagerは十全な情報を設定された状態のAuthenticationインスタンスを返します。
  4. このAuthenticationインスタンスを引数にしてSecurityContextHolder.getContext().setAuthentication(...)というメソッド・コールを行うことによってセキュリティ・コンテキストが確立されます。

これ以降、ユーザは“認証済みである”と見なされるようになります。例としていつくかのコードを見ていきましょう。

import org.springframework.security.authentication.*;
import org.springframework.security.core.*;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;

public class AuthenticationExample {
  private static AuthenticationManager am = new SampleAuthenticationManager();

  public static void main(String[] args) throws Exception {
    BufferedReader in = new BufferedReader(new InputStreamReader(System.in));

    while(true) {
      System.out.println("Please enter your username:");
      String name = in.readLine();
      System.out.println("Please enter your password:");
      String password = in.readLine();
      try {
        Authentication request = new UsernamePasswordAuthenticationToken(name, password);
        Authentication result = am.authenticate(request);
        SecurityContextHolder.getContext().setAuthentication(result);
        break;
      } catch(AuthenticationException e) {
        System.out.println("Authentication failed: " + e.getMessage());
      }
    }
    System.out.println("Successfully authenticated. Security context contains: " +
              SecurityContextHolder.getContext().getAuthentication());
  }
}

class SampleAuthenticationManager implements AuthenticationManager {
  static final List<GrantedAuthority> AUTHORITIES = new ArrayList<GrantedAuthority>();

  static {
    AUTHORITIES.add(new SimpleGrantedAuthority("ROLE_USER"));
  }

  public Authentication authenticate(Authentication auth) throws AuthenticationException {
    if (auth.getName().equals(auth.getCredentials())) {
      return new UsernamePasswordAuthenticationToken(auth.getName(),
        auth.getCredentials(), AUTHORITIES);
      }
      throw new BadCredentialsException("Bad Credentials");
  }
}

ここでは、ユーザにユーザ名とパスワードの入力を促して認証を行う簡単なプログラムを書いてみました。私たちが実装したAuthenticationManagerはユーザ名とパスワードが同じならともかく何でも認証してくれるでしょう。どのユーザに対してもロールはひとつっきりです。プログラムの出力内容はこんな感じです:

Please enter your username: bob 
Please enter your password: password 
Authentication failed: Bad Credentials 
Please enter your username: bob 
Please enter your password: bob 
Successfully authenticated. Security context contains: \
org.springframework.security.authentication.UsernamePasswordAuthenticationToken@441d0230: \
Principal: bob; Password: [PROTECTED]; \  
Authenticated: true; Details: null; \  
Granted Authorities: ROLE_USER         

注意しておいてほしいのですが、ふつうこんなにコードを書く必要はありません。このプロセスは例えばWeb認証フィルタにおいて内部的に処理されているものです。また上記のコードにはAuthenticationオブジェクトが実際にどのように構成されるのかという問いに対するシンプルな回答も組み込まれています。いずれにせよ完全な情報が設定されたAuthenticationオブジェクトがSecurityContextHolderに格納されたとき、ユーザは認証済み状態となります。

6.3.2 SecurityContextHolderの内容を直接設定する

実際のところ、SecurityContextHolder の内部にAuthenticationオブジェクトが格納される方法についてSpring Securityは関知しません。ただひとつ守らなくてはならない要件は、AbstractSecurityInterceptor(このオブジェクトについてはあとで見ます)がユーザの操作を承認しなくてはいけなくなるより前に、AuthenticationをSecurityContextHolderに設定しておかなくてはならない、というものです。

(多くのユーザ〔開発者〕が実際に行っていることですが)Spring Securityに基づかないシステムとの相互運用性を確保するために、独自のフィルタやMVCコントローラを定義することもできます。例えば、ThreadLocalやJNDIロケーションから取得したユーザ情報を有効化するのにコンテナ管理型認証を使用することがあるかも知れません。あるいはまた、あなたがレガシー・プロプライエタリな認証システムを持つ会社で働くということもあるかも知れません。企業の“技術標準”を前に、あなたの自由にできることは少ないでしょう。このようなシチュエーションであってもSpring Securityは簡単に導入でき、また認証機能を提供することができます。あなたがすべきことはフィルタ(あるいはそれの同等物)を実装することです。フィルタがすべきことは、その場でサードパーティ〔の認証機構〕のユーザ情報をと読み取り、Spring Security固有のAuthenticationオブジェクトをくみ上げ、SecurityContextHolderに突っ込むことです。このケースでは、組み込みの認証インフラストラクチャがたいてい自動的にこなしている処理について考えることも必要です。例えば、リクエストとリクエストの間でセキュリティ・コンテキストをキャッシュしておくために、クライアントへのレスポンスを返す処理のまえに、あらかじめHTTPセッションを生成していく必要がある、というようなものについてです。

現実のアプリケーションでも使われるようなAuthenticationManagerの実装例はどのようになものかと疑問に思われている方は、コア・サービスのチャプター〔7.1節〕ご覧になってください。

* * *

原典は、“6. Technical Overview”(Spring Security Reference Documentation)で、Ben Alex氏とLuke Taylor氏によるものです。

続く「6.4 Webアプリケーションにおける認証」以降は、次の投稿でアップします


Spring関連の訳出記事まとめ - M12i.