2013年12月29日日曜日

Apache Shiro Realms (日本語訳)

# Apach Shiro Realms の日本語訳

  Realm は、ユーザー、ロール、パーミッション等のアプリケーション固有のセキュリティーデータにアクセスするコンポーネントです。データソースが幾つ存在していようが、どれだけアプリケーションに特化したデータであろうが、アプリケーション固有のデータが Realm を通して解釈可能なフォーマットに整えられ、それにより Shiro は、単一でかつ理解も容易な Subject というプログラミング API を提供することが可能となっています。

  Realm は通常、リレーショナルデータベースや LDAP ディレクトリ、ファイルシステムといったデータソース、もしくは他の似たようなリソースと、1 対 1 の関係を持っています。そのため、Realm インターフェースの実装では、JDBC やファイル I/O、Hibernate、JPA、その他のデータアクセス API などのデータソース固有の API を用いて、認可情報 (ロールやパーミッションなど) を取得します。

Realm の本質は、セキュリティーに特化した DAO です。

  これらのデータソースは通常、認証データ (パスワードなどの認証情報) と認可データ (ロールやパーミッション等) の両方を保持しているため、全ての Realm は認証処理と認可処理の両方をおこなうことができます。


Realm の設定

  Shiro の INI 設定を使用するのであれば、他のオブジェクトと同様、[main] セクションで Realm を定義したり参照したりすることができます。ただし、securityManager に設定する際には、明示的におこなうか暗黙的におこなうかのどちらか一方の方法をとることになります。


明示的な代入

  INI 設定に関してこれまでに学んだことを考えれば、この設定方法は当然とも言うべきアプローチです。一つ以上の Realm を定義したあと、securityManager のコレクションプロパティーにセットします。

例:
fooRealm = com.company.foo.Realm
barRealm = com.company.another.Realm
bazRealm = com.company.baz.Realm

securityManager.realms = $fooRealm, $barRealm, $bazRealm

  明示的な代入では、設定内容が明確に定まります。認証と認可にどの Realm を使うかという点と併せて、Realm 間の順序をどうするか、も正確に制御することができます。Realm の順序の効果は、認証の章「認証シーケンス」で詳しく説明されています。


暗黙的な代入

非推奨

  暗黙的な代入では、Realm の定義順序が変わることで、想定外の動きになってしまうことがありえます。 この設定方法を避け、設定内容を明確に定めることができる明示的な代入を使うことを推奨します。 暗黙的な設定は、Shiro の将来のリリースで deprecated 扱いになるか削除される可能性があります。

  なんらかの理由で securityManager.realms プロパティーを明示的に設定したくない場合は、Shiro に、定義済みの全ての Realm を見つけさせ、それらを直接 securityManager に設定させることもできます。

  この方法では、 Realm は定義された順番に securityManager に設定されていきます。

  つまり、例えば shiro.ini で下記のようになっていたすると、

blahRealm = com.company.blah.Realm
fooRealm = com.company.foo.Realm
barRealm = com.company.another.Realm

# no securityManager.realms assignment here

基本的には、次の行が追加されているのと同じ効果があります。

securityManager.realms = $blahRealm, $fooRealm, $barRealm

  暗黙的な代入を用いると、認証や認可の試行時に Realm に対してどのように問い合わせがおこなわれるかは、Realm が定義される順番に直接影響を受ける、ということを認識しておいてください。定義順序を変えると、Authenticator の認証シーケンスが変化します。

  このような理由から、 また動作を明確化するためにも、暗黙的な代入ではなく明示的な代入のほうを推奨します。


Realm 認証

  Shiro の認証処理フローを理解したので、次は、認証試行時に Authenticator と Realm とのやりとりで何が行われているのかを正確に知ることが重要となります。


AuthenticationToken のサポート

  認証シーケンスで触れられているように、認証試行時に Realm へ問い合わせがおこなわれる前に、supports メソッドが呼ばれます。戻り値が true であれば、その場合に限り、getAuthenticationInfo(token) メソッドが呼ばれます。

  典型的には、Realm は、処理できるかどうかを確認するため、渡されたトークンの型 (インターフェースもしくはクラス) を調べます。例えば、生体認証データを処理する Realm は UsernamePasswordToken を全く理解することはないでしょうから、この場合 supports メソッドは false を返します。


サポート対象の AuthenticationToken を処理する

  提示された AuthenticationToken を Realm が処理できるのであれば、Authenticator は Realm の getAuthenticationInfo(token) メソッドを呼びます。これは事実上、Realm の背後にあるデータソースを用いて認証を試みることになります。このメソッドは次の順番で処理をおこないます。

  1. 識別プリンシパル (アカウントを識別する情報) がトークンに含まれているかを調べる。
  2. プリンシパルを元に、データソース内にある対応するアカウントデータを検索する。
  3. トークンが含む認証情報がデータソースに保存されているものと一致することを確認する。
  4. 認証情報が一致する場合は、Shiro が扱える形式でアカウントデータを含めた AuthenticationInfo インスタンスを返す。
  5. 認証情報が一致しない場合は、AuthenticationException を投げる。

  これは、getAuthenticationInfo の全ての実装におけるハイレベル処理フローです。Realm はこのメソッド内で何をやってもよく、例えば、認証試行について監査ログに記録したり、データレコードを更新したり、その他、データソースに対する認証試行に関して意味のあることであれば何でもかまいません。

  唯一求められているのは、与えられたプリンシパルと認証情報が一致するのであれば、null ではない、データソース内の Subject アカウント情報をあらわす AuthenticationInfo インスタンスを返すことです。

時間の節約

  Realm インターフェースを直接実装するのは、時間もかかり、また間違い易くもあります。 ほとんどの人は、ゼロから実装するのではなく、抽象クラス AuthorizingRealm を拡張するほうを選びます。 このクラスには、時間と労力を節約するため、認証と認可の共通処理フローが実装されています。


認証情報の一致検査

  上述の Realm 認証処理フローでは、Realm は提示された Subject の認証情報 (例えばパスワード) がデータソースに保存されている認証情報と一致するかどうかを検証しなければなりません。一致するのであれば、認証成功となり、システムがエンドユーザーの同一性を検証したということになります。

Realm による認証情報一致検査

  提示された認証情報と Realm の背後にあるデータストアに保存されている認証情報とが一致するかどうかを確認するのは、Authenticator ではなく、各 Realm の責任となります。各 Realm は、認証情報のフォーマットとストレージに関する非公開の情報を有し、詳細な認証情報一致検査を実行できますが、一方で Authenticator は汎用的な処理フローコンポーネントに過ぎません。

  認証情報の一致検査処理は、全てのアプリケーションにおいてほぼ同一で、ほとんどの場合、異なるのは比較対象のデータのみです。この処理を必要に応じて取り外したりカスタマイズしたりできるように、AuthenticatingRealm およびそのサブクラスは、認証情報の比較を実行する CredentialsMatcher という概念をサポートしています。

  検索により見つかったアカウントデータと提示された AuthenticationToken は CredentialsMatcher に渡され、提示されたものとデータストアに保存されているものが一致するかどうかが調べられます。

  すぐに利用開始できるよう、SimpleCredentialsMatcherHashedCredentialsMatcher などの CredentialsMatcher の実装が Shiro に用意されています。しかし、カスタム一致検査ロジック用のカスタム実装を用いるように設定したいのであれば、直接おこなうこともできます。

Realm myRealm = new com.company.shiro.realm.MyRealm();

CredentialsMatcher customMatcher =
    new com.company.shiro.realm.CustomCredentialsMatcher();

myRealm.setCredentialsMatcher(customMatcher);

  もしくは、Shiro の INI 設定を用いるのであれば:

[main]
...
customMatcher = com.company.shiro.realm.CustomCredentialsMatcher
myRealm = com.company.shiro.realm.MyRealm
myRealm.credentialsMatcher = $customMatcher
...


単純な等価検査

  Shiro が用意している Realm の実装は全て、デフォルトでは SimpleCredentialsMatcher を使います。SimpleCredentialsMatcher は、保存されているアカウント認証情報と AuthenticationToken に含まれている提示された認証情報とを、何の変換処理もおこなわずに直接等しいかどうか調べます。

  例えば UsernamePasswordToken が提示されたとすると、SimpleCredentialsMatcher は提示されたパスワードとデータストアに保存されているパスワードが正確に一致するかどうかを検証します。

  とはいえ、SimpleCredentialsMatcher は単純な String 比較以上のことをおこないます。String や文字配列、バイト配列、ファイルや入力ストリーム等の一般的なバイトソースを扱うことができます。詳細は JavaDoc を参照してください。


認証情報のハッシュ化

  認証情報をそのままの形で保存して何の変換処理もおこなわずに比較をおこなう方法に代わる、エンドユーザーの認証情報 (パスワード等) をはるかに安全に保存する方法は、データストアに保存する前に一方向ハッシュを求めてそれを保存することです。

  これにより、エンドユーザーの認証情報がそのままの形では保存されなくなるので、誰も元の値を知ることはできません。これは、平文や単純比較よりもはるかに安全な仕組みであり、セキュリティーを気にするアプリケーションは全て、ハッシュ化機構の無いストレージに対しては、このアプローチを取るべきです。

  この好ましい暗号ハッシュ戦略をサポートするため、先に述べた SimpleCredentialsMatcher の代わりに Realm に設定できる HashedCredentialsMatcher の実装が、Shiro により提供されます。

  認証情報のハッシュ処理、ソルトと複数回ハッシュの利点については、Realm に関する本文書の対象外となりますが、原理について詳細に説明している HashedCredentialsMatcher の JavaDoc を是非とも読んでください。


ハッシュ化と対応する Matcher

  Shiro を用いたアプリケーションでこれを簡単におこなうにはどうしたらよいでしょうか?

  Shiro には HashedCredentialsMatcher のサブクラスの実装が複数あります。あなたがユーザーの認証情報をハッシュ化するときに用いるアルゴリズムに合わせて、特定の実装を選んで Realm に設定しなければなりません。

  例えば、あなたのアプリケーションがユーザー名とパスワードの組により認証をおこなうものとしましょう。そして、先に説明した認証情報ハッシュ化の利点を享受するため、ユーザーアカウントを作る際に SHA-256 アルゴリズムを用いてユーザーのパスワードを一方向ハッシュ処理したいとしましょう。このとき、ユーザーが入力した平文パスワードをハッシュ化し、その値を保存する処理は次のようになります。

import org.apache.shiro.crypto.hash.Sha256Hash;
import org.apache.shiro.crypto.RandomNumberGenerator;
import org.apache.shiro.crypto.SecureRandomNumberGenerator;
...

// ソルトを生成するため乱数生成器を用います。これは、ユーザー名を
// ソルトとして用いたり、まったくソルトを用いない場合に比べて
// はるかに安全です。Shiro を使えばとても簡単です。
//
// 普通のアプリでは、毎回乱数生成器を新しく作成するのではなく、
// 属性を参照するだけにとどめます。
RandomNumberGenerator rng = new SecureRandomNumberGenerator();
Object salt = rng.nextBytes();

// ランダムに生成したソルトを用いて平文パスワードをハッシュ化し、
// それを何回も繰り返してから、Base64 で符号化します (Base64 は
// 十六進数表記よりも必要領域が少なくて済みます)。
String hashedPasswordBase64 =
    new Sha256Hash(plainTextPassword, salt, 1024).toBase64();

User user = new User(username, hashedPasswordBase64);

// ソルトを新しいアカウントと一緒に保存します。ソルトは、ログイン
// 試行時に HashedCredentialsMatcher が必要とします。
user.setPasswordSalt(salt);
userDAO.create(user);

  SHA-256 でユーザーのパスワードをハッシュ化したので、このハッシュ処理に合う HashedCredentilsMatcher を使うように Shiro に伝える必要があります。この例では、セキュリティーを強化するため、ランダムにソルトを生成し、ハッシュ処理を 1024 回繰り返しました (理由は HashedCredentialsMatcher の JavaDoc を参照してください)。Shiro の INI 設定では次のように設定することになります。

[main]
...
credentialsMatcher = org.apache.shiro.authc.credential.Sha256CredentialsMatcher

# この例では十六進数ではなく Base64
credentialsMatcher.storedCredentialsHexEncoded = false
credentialsMatcher.hashIterations = 1024

# 下記のプロパティーは Shiro 1.0 で必要。1.1 以降では取り除く。
credentialsMatcher.hashSalted = true

...
myRealm = com.company.....
myRealm.credentialsMatcher = $credentialsMatcher
...


SaltedAuthenticationInfo

  この作業の仕上げとしてやらなければならないことは、あなたの Realm の実装で、いつもの AuthenticationInfo ではなく、SaltedAuthenticationInfo のインスタンスを返すことです。SaltedAuthenticationInfo により、あなたがユーザーアカウントを作成したときに使用したソルト (上記の例で言えば user.setPasswordSalt(salt) で保存したソルト) を HashedCredentialsMatcher が参照できるようになります。

  HashedCredentialsMatcher は、データストアに保存してあるトークンと提示された AuthenticationToken が一致することを確認するため、同じハッシュ処理を実行しますが、その際にソルトを必要とします。ですので、ユーザーパスワード用にソルトを用いるのであれば (用いるべきです!!!)、Realm の実装で SaltedAuthenticationInfo インスタンスを返すようにしてください。


認証無効化

  なんらかの理由により (おそらく Realm で認可だけを扱いたいという理由により)、Realm に認証処理をさせたくない場合は、Realm の supports メソッドで常に false を返すことにより Realm の認証サポートを完全に無効化することができます。そうすることで、認証試行の際に Realm に問い合わせがくることはなくなります。

  もちろん、Subject を認証したいのであれば、少なくとも一つの Realm は AuthenticationToken をサポートするよう設定しておく必要があります。


Realm 認可

  記述予定


文書のご協力をお願いします

  本文書が Apache Shiro を扱う作業の手助けとなることを望みつつ、コミュニティーでは常に文書の改良と拡張をおこなっています。Shiro プロジェクトを手伝ってくださるのであれば、必要だと思われた箇所から文書の訂正、拡張、追加のご検討をお願いします。どんなに小さなことであっても、あなたの貢献は全て、コミュニティーの発展、そして Shiro の改善につながります。

  文書による貢献の最もやり易い方法は、ユーザーフォーラムもしくはユーザーメーリングリストに文書を送付いただくことです。