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 の改善につながります。

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



2013年12月28日土曜日

文字数、UTF-8 でのバイト数、サロゲートペアの数を数える

  下記のコードは、文字数、UTF-8 で符号化したときのバイト数、サロゲートペアの数、を数え上げるロジックを JavaScript で表現したものです。Java や Objective-C など、文字列を UTF-16 で扱っているプログラミング言語であれば、同じロジックを適用できます。GIST はこちら。Unicode 理解度簡易チェックはこちら。本ブログの英語版はこちら



文字数: 0
UTF-8 でのバイト数: 0
サロゲートペアの数: 0

Count up letters, bytes in UTF-8 and surrogate pairs in JavaScript

Below is a sample code in JavaScript to count up letters, bytes in UTF-8 and surrogate pairs. Available as a gist.



0 letter(s).
0 byte(s) in UTF-8.
0 surrogate pair(s).



2013年12月13日金曜日

Unicode 理解度簡易チェック

次のことが理解できているかを簡単に調べることができます。

  • UTF-8 は Unicode の文字符号化方式の一つである
    (Unicode と UTF-8 の違いが分かっているか?)
  • Java や Objective-C でプログラミングするときに文字の値は UTF-16 になっている
    (ちゃんとプログラミングできるか?)

下記の表の内容を見て、全て理解できれば OK です。



Unicode の規格でこの文字に割り当てられた
Unicode スカラ値 (十六進数表記)
U+3042
この文字の Unicode スカラ値を UTF-8
符号化したときのバイト列 (十六進数表記)
E3 81 82
(3 バイト)
この文字の Unicode スカラ値を UTF-16BE
符号化したときのバイト列 (十六進数表記)
30 42
(2 バイト)
この文字の Unicode スカラ値を UTF-16LE
符号化したときのバイト列 (十六進数表記)
42 30
(2 バイト)
この文字の Unicode スカラ値を UTF-32BE
符号化したときのバイト列 (十六進数表記)
00 00 30 42
(4 バイト)
この文字の Unicode スカラ値を UTF-32LE
符号化したときのバイト列 (十六進数表記)
42 30 00 00
(4 バイト)
// Java

char ch = "あ".charAt(0);   // ch == 0x3042
// Objective-C

NSString *s = @"あ";
unichar ch = [s characterAtIndex:0];   // ch == 0x3042
(おまけ) この文字を Shift-JIS
符号化したときのバイト列 (十六進数表記)
82 A0
(2 バイト)




𠮷 (UTF-16 のとき、サロゲートペア)
Unicode の規格でこの文字に割り当てられた
Unicode スカラ値 (十六進数表記)
U+20BB7
この文字の Unicode スカラ値を UTF-8
符号化したときのバイト列 (十六進数表記)
F0 A0 AE B7
(4 バイト)
この文字の Unicode スカラ値を UTF-16BE
符号化したときのバイト列 (十六進数表記)
D8 42 DF B7
(4 バイト)
この文字の Unicode スカラ値を UTF-16LE
符号化したときのバイト列 (十六進数表記)
42 D8 B7 DF
(4 バイト)
この文字の Unicode スカラ値を UTF-32BE
符号化したときのバイト列 (十六進数表記)
00 02 0B B7
(4 バイト)
この文字の Unicode スカラ値を UTF-32LE
符号化したときのバイト列 (十六進数表記)
B7 0B 02 00
(4 バイト)
// Java

String s = "𠮷";
int  len = s.length();    // len == 2
char ch1 = s.charAt(0);   // ch1 == 0xD842
char ch2 = s.charAt(1);   // ch2 == 0xDFB7
// Objective-C

NSString *s = @"𠮷";
int len     = [s length];               // len == 2
unichar ch1 = [s characterAtIndex:0];   // ch1 == 0xD842
unichar ch2 = [s characterAtIndex:1];   // ch2 == 0xDFB7

2013年12月10日火曜日

Apache Shiro で Web アプリケーションをセキュアにする

# Securing Web Applications with Apache Shiro (Nov 19, 2013) の翻訳

  この文書は、Apache Shiro で Web アプリケーションをセキュアにする手順を一つずつ説明していく入門レベルのチュートリアルです。Shiro について初歩的な知識があることを前提としており、少なくとも次の二つの入門文書を理解していることを想定しています。


  このステップ・バイ・ステップのチュートリアルを完了するのには 45 分から 1 時間くらいかかるでしょう。終了後には、Web アプリケーションで Shiro がどのように動作するかを深く理解できていることでしょう。


概要

  コマンドライン・アプリケーションやサーバー・デーモン、Web アプリケーションなど、Java 仮想マシンで動くアプリケーションであれば何であれ、Apache Shiro でセキュアにすることを可能とする、というのが Apache Shiro の核となる設計目標ですが、このガイドでは最も一般的なユースケース、すなわち、TomcatJetty といったサーブレットコンテナ上で動く Web アプリケーションをセキュアにすること、にフォーカスします。


必須要件

  このチュートリアルをこなしていくため、あなたの手元の開発マシンに下記のツール群をインストールしておいてください。


チュートリアルの構成

  これは、ステップ・バイ・ステップのチュートリアルです。チュートリアルそのものと、その全てのステップは、Git リポジトリとして存在しています。その Git リポジトリをクローンすると、master ブランチが開始点となります。チュートリアル内の各ステップは、個別のブランチとなっています。チュートリアル内で参照中のステップを表すGit ブランチをチェックアウトするだけで、チュートリアルについていくことができます。


Web アプリケーション

  これから作ろうとしている Web アプリケーションは、あなた自身の Web アプリケーションの開始点として使用可能な Web アプリケーションです。ユーザーのログイン、ログアウト、ユーザー別のウェルカム・メッセージ、Web アプリケーションに対する部分的なアクセス制御、置き換え可能なセキュリティーデータ保存領域との統合、を実例で説明していきます。

  プロジェクトのセットアップ、ビルドツールの導入、依存関係の宣言、Web アプリケーションと Shiro 環境を起動するための web.xml の設定、から始めます。

  セットアップ後、セキュリティーデータ保存領域との統合、ユーザーログインとログアウト、アクセス制御などの個々の機能を一つずつ加えていきます。


プロジェクトのセットアップ

  ディレクトリ構造と基本的なファイル群の初期セットを用意する作業は、我々が既に Git リポジトリでやっておきました。


1. チュートリアルプロジェクトをフォークする

  GitHubチュートリアルプロジェクトにいき、右上にある Fork ボタンを押してください。


2. チュートリアルリポジトリをクローンする

  あなたの GitHub アカウントでリポジトリをフォークしたので、次は手元のマシンにクローンしてください。

$ git clone git@github.com:$YOUR_GITHUB_USERNAME/\
apache-shiro-tutorial-webapp.git

(もちろん、$YOUR_GITHUB_USERNAME はあなたの GitHub ユーザー名でおきかえてください)

  これでクローンしたディレクトリに移動し、プロジェクト構成を見ることができます。

$ cd apache-shiro-tutorial-webapp


3. プロジェクト構成を確認する

  リポジトリをクローンしたあと、master ブランチは次のような構成となっています。

apache-shiro-tutorial-webapp/
  |-- src/
  |  |-- main/
  |    |-- resources/
  |      |-- logback.xml
  |    |-- webapp/
  |      |-- WEB-INF/
  |        |-- web.xml
  |      |-- home.jsp
  |      |-- include.jsp
  |      |-- index.jsp
  |-- .gitignore
  |-- .travis.yml
  |-- LICENSE
  |-- README.md
  |-- pom.xml

  それぞれ次のような意味です。

  • pom.xml: Maven プロジェクトのビルドファイル。Jetty の設定は済ませてあるので、mvn jetty:run ですぐに Web アプリケーションをテストできます。
  • README.md: プロジェクトの README ファイル
  • LICENSE: プロジェクトのライセンス。Apache 2.0 ライセンス
  • .travis.yml: プロジェクトを常にビルド可能状態に保つことを保証するために継続的インテグレーションを実行したくなったときのための Travis CI 設定ファイル。
  • .gitignore: Git 無視設定ファイル。バージョン管理下に置かないファイルのサフィックスやディレクトリをリストします。
  • src/main/resources/logback.xml: Logback 設定ファイル。このチュートリアルで我々は、ロギング API として SLF4J を、実装として Logback を選びました。Log4J や JUL (java.util.logging) にすることも容易にできたでしょう。
  • src/main/webapp/WEB-INF/web.xml: 我々の最初の web.xml ファイルです。すぐあとで Shiro を有効にするよう設定します。
  • src/main/webapp/include.jsp: 共通の import と宣言を含むページで、他の JSP から読み込まれます。これにより import と宣言を一ヶ所で管理できます。
  • src/main/webapp/home.jsp: 我々の Web アプリケーションのデフォルトホームページです。include.jsp を読み込みます (すぐあとで見ることになりますが、他のファイルも include.jsp を読み込みます。)
  • src/main/webapp/index.jsp: デフォルトのサイト index ページです。単に home.jsp ホームページへとリクエストを転送するだけです。


4. Web アプリケーションを実行する

  プロジェクトをクローンしてあるので、コマンドラインで次のように実行すれば Web アプリケーションを起動できます。

$ mvn jetty:run


  続けて Web ブラウザで localhost:8080 を開けば、ホームページに Hello, World! と表示されることが確認できます。

  Ctrl-C (Mac であれば Cmd-C) を入力して Web アプリケーションを終了させてください。


ステップ 1 : Shiro を有効にする

  初期状態の master ブランチは、どんなアプリケーションのテンプレートにもなりうる汎用 Web アプリケーションに過ぎません。次は、Shiro を有効にするための最低限の作業をしていきましょう。

  step1 ブランチをロードするため、次の git checkout コマンドを実行してください。

$ git checkout step1

  このブランチをチェックアウトすると、変更が二つかかります。

  1. 新しく src/main/webapp/WEB-INF/shiro.ini ファイルが追加され、
  2. src/main/webapp/WEB-INF/web.xml が変更されます。


1a : shiro.ini ファイルを追加する

  Web アプリケーション内では、あなたが使用している Web / MVC フレームワークに応じて、様々な方法で Shiro を設定することができます。例えば、Spring, Guice, Tapestry やその他数多くの方法で Shiro を設定することができます。

  ここでは話を簡単に済ますため、Shiro のデフォルトの (そしてとてもシンプルな) INI ベースの設定を使って Shiro 環境を始めてみましょう。

  step1 ブランチをチェックアウトしてあれば、新しくできた src/main/webapp/WEB-INF/shiro.ini ファイルの中身を確認することができます (分かりやすくするため、ヘッダーコメントは取り除いてあります):

[main]

# 実行中の Stormpath への問い合わせの数を減らすため、メモリでの
# キャッシュを使うことにします。実際のアプリケーションでは、
# もっとしっかりしたキャッシュ機構 (Ehcache や分散キャッシュ等) が
# 望ましいでしょう。そのようなキャッシュを使うときは、キャッシュの
# TTL 設定に気を付けてください。TTL が大き過ぎると、Stormpath 側で
# 発生したかもしれない変化がキャッシュに反映されるまでに時間がかなり
# かかってしまいます。一方で、小さ過ぎると、キャッシュが無効になる
# 頻度がかなり高くなってしまいます。
cacheManager = org.apache.shiro.cache.MemoryConstrainedCacheManager
securityManager.cacheManager = $cacheManager

  この .ini ファイルでは [main] セクションで最低限の設定をしています。

  • 新しく cacheManager インスタンスを定義しています。Shiro のアーキテクチャーではキャッシュは重要です。多種多様なデータ保存領域とのやりとりを減らすものだからです。 この例では、単一 JVM で動くアプリケーションにとってだけはかなり良い MemoryConstrainedCacheManager を使っています。複数のホスト (例えばクラスター化された Web サーバーファーム等) にまたがって配備されるアプリケーションの場合は、クラスター化された CacheManager の実装をかわりに使いたくなることでしょう。
  • Shiro の securityManager に新しい cacheManager を設定しています。Shiro の SecurityManager インスタンスは常に存在するので、わざわざ定義する必要はありません。


1b : web.xml で Shiro を有効にする

  設定ファイル shiro.ini は用意できましたが、これを実際にロードして新規に Shiro 環境を開始し、Web アプリケーションで使えるようにする必要があります。

  既に存在する src/main/webapp/WEB-INF/web.xml ファイルに幾つか項目を追加するだけで、全て完了します:

<listener>
    <listener-class>org.apache.shiro.web.env.EnvironmentLoaderListener</listener-class>
</listener>

<filter>
    <filter-name>ShiroFilter</filter-name>
    <filter-class>org.apache.shiro.web.servlet.ShiroFilter</filter-class>
</filter>

<filter-mapping>
    <filter-name>ShiroFilter</filter-name>
    <url-pattern>/*</url-pattern>
    <dispatcher>REQUEST</dispatcher>
    <dispatcher>FORWARD</dispatcher>
    <dispatcher>INCLUDE</dispatcher>
    <dispatcher>ERROR</dispatcher>
</filter-mapping>

  • <listener> 宣言では、Web アプリケーション起動時に Shiro 環境 (Shiro の SecurityManager を含む) を開始する ServletContextListener を定義しています。このリスナーは、Shiro の設定を探すときにデフォルトで WEB-INF/shiro.ini を見にいく、という動作を自動的におこないます。
  • <filter> 宣言では、マスター ShiroFilter を定義しています。Web アプリケーションにリクエストを渡す前に必要な識別処理やアクセス制御処理を Shiro がおこなえるよう、このフィルターで Web アプリケーションに届く全てのリクエストにフィルターをかけることを想定しています。
  • <filter-mapping> 宣言では、全てのリクエストタイプを ShiroFilter で処理するようにしています。ほとんどの場合 filter-mapping 宣言で <dispatcher> 要素を指定することはありませんが、Shiro では、Web アプリケーションが実行しうる全てのリクエストタイプにフィルターをかけられるよう、全て定義する必要があります。


1c : Web アプリケーションを実行する

  step1 ブランチをチェックアウト後、Web アプリケーションを実行してください:

$ mvn jetty:run

  今回は、下記と似た感じのログ出力を見ることになります。これは、Web アプリケーション内で Shiro が実際に実行されていることを示すものです。

16:04:19.807 [main] INFO  o.a.shiro.web.env.EnvironmentLoader - Starting Shiro environment initialization.
16:04:19.904 [main] INFO  o.a.shiro.web.env.EnvironmentLoader - Shiro environment initialized in 95 ms.

  Ctrl-C (Mac であれば Cmd-C) を入力して Web アプリケーションを終了させてください。



ステップ 2 : ユーザー格納領域に接続する


  step2 ブランチをロードするため、次の git checkout コマンドを実行してください。

$ git checkout step2

  Shiro を統合し、Web アプリケーション内で走らせました。しかし、我々はまだ Shiro に何も指示を出していません!

  ログインやログアウト、ロールやパーミッションに基づくアクセス制御、その他セキュリティーに関係することをやる前に、まずはユーザーが必要です!

  ログイン試行時のユーザー検索や、ロールに基づくセキュリティー判断などのためには、何らかのユーザー格納領域にアクセスするように Shiro を設定する必要があります。アプリケーションがアクセスすることになるユーザー格納領域は多種多様です。MySQL データベースにユーザーを格納するかもしれませんし、MongoDB かもしれません。あなたの会社ではユーザーアカウントを LDAP や Active Directory で管理しているかもしれませんし、ただのファイルやその他のプロプライエタリーなデータストアかもしれません。

  Shiro ではこれを、いわゆるレルム (Realm) という形で扱います。Shiro のドキュメンテーションからの引用です:

  レルムは、Shiro とアプリケーションのセキュリティーデータとの間のブリッジ/コネクタとして振る舞います。認証 (ログイン) や認可 (アクセス制御) のためにユーザーアカウントのようなセキュリティーデータと実際にやりとりを行うとき、Shiro はアプリケーション用に設定された一つ以上のレルムに対して多くのことを問い合わせます。

  この意味において、レルムは本質的にセキュリティーに特化した DAO であると言えます。データソースとの接続詳細をカプセル化し、必要なときに関連データを Shiro に提供するものです。Shiro を設定するとき、認証/認可で使用するレルムを一つ以上指定しなければなりません。SecurityManager に複数のレルムを設定してもよいですが、少なくとも一つは必要です。

  Shiro は、LDAP やリレーショナルデータベース (JDBC)、INI や properties ファイル等のテキストベースの設定など、多数のセキュリティーデータソース (またの名をディレクトリ) に接続するためのレルムをすぐ使える状態で提供しています。もし要件に合うレルムがなければ、カスタムのデータソースを表現するレルムの実装をプラグインすることもできます。

  というわけで、ユーザーを扱うためにレルムを設定する必要があります。


2a : Stormpath をセットアップする

  チュートリアルをできるだけシンプルにしようと思っていますので、複雑な話を持ち込んだり範囲を広げたりして Shiro を学ぶという目的から外れるようなことは避けたいと思います。かわりに最もシンプルなレルムの一つである Stormpath レルムを使用することにします。

  Stormpath はクラウド上のユーザー管理サービスで、開発用途であれば完全に無料です。つまり、Stormpath を有効にすると、下記のものがすぐに使えるようになります。

  • アプリケーション、ディレクトリー、アカウント、グループを管理するためのユーザーインターフェース。これらは Shiro には全く含まれていないので、このチュートリアルをこなしていく上では便利であり、時間を節約できます。
  • ユーザーのパスワードをストレージ内でセキュアに保つ仕組み。あなたのアプリケーションでは、パスワードのセキュリティー、比較、保存について気にする必要は全くありません。Shiro でこれらを扱うこともできますが、設定をしなければならず、暗号の概念についても理解していなければなりません。Stormpath はパスワードセキュリティーを自動化しているので、あなた (と Shiro) は「正しいやり方」について心配したり、問題を抱えたりする必要はありません。
  • メールによるアカウント検証やパスワードリセットなどのセキュリティー・ワークフロー。これは通常アプリケーション固有の事項なので、Shiro ではサポートしていません。
  • ホストされ、監視されている「常時稼働」インフラ。維持管理のために何かを設定する必要はありません。

  チュートリアルの目的からすると、別途 RDBMS サーバーをセットアップしたり、SQL やパスワード符号化問題を心配したりするよりも、Stormpath を使うほうがはるかに簡単です。ですので、ここでは Stormpath を使います。

  もちろん、Stormpath は、Shiro がやりとりできる数あるバックエンド・データストアの一つでしかありません。もっと複雑なデータストアやアプリケーション固有の設定については、あとで説明します。


Stormpath にサインアップする

  1. Stormpath の登録フォームを埋め、送信します。これにより確認メールが届きます。
  2. 確認メールに含まれるリンクをクリックします。


Stormpath API キーを取得する

  Stormpath とやりとりする Stormpath レルムには、Stormpath API キーが必要です。Stormpath API キーを取得する手順は次のとおりです。

  1. Stormpath に登録したメールアドレスとパスワードを使って Stormpath 管理コンソールにログインします。
  2. 表示されたページの右上角から、SettingsMy Account と進みます。
  3. アカウント詳細ページで、Security Credentials の下にある Create API Key をクリックします。これにより、あなた用の API キーが作成され、apiKey.properties ファイルが手元のコンピューターにダウンロードされます。そのファイルをテキストエディタで開くと、次のような内容を確認できます。
    apiKey.id = 144JVZINOF5EBNCMG9EXAMPLE
    apiKey.secret = lWxOiKqKPNwJmSldbiSkEbkNjgh2uRSNAb+AEXAMPLE
    
  4. このファイルを、ホームディレクトリ下の隠しディレクトリ .stormpath などといった安全な場所に保存します。例:
    $HOME/.stormpath/apiKey.properties
    
  5. また、このファイルを自分だけしか見れないようにするため、ファイルのパーミッションを変更してください。例えば、Unix 系オペレーティングシステムであれば:
    $ chmod go-rwx $HOME/.stormpath/apiKey.properties
    


Web アプリケーションを Stormpath に登録する

  アプリケーションから Stormpath を使いユーザーの管理と認証をおこなうためには、Web アプリケーションを Stormpath に登録しなければなりません。アプリケーションの登録は Stormpath に REST リクエストを投げるだけでできます。下記は、新しいアプリケーションのリソースを Stormpath の applications URL に POST する方法です。

curl -X POST --user $YOUR_API_KEY_ID:$YOUR_API_KEY_SECRET \
    -H "Accept: application/json" \
    -H "Content-Type: application/json" \
    -d '{
           "name" : "Apache Shiro Tutorial Webapp"
        }' \
    'https://api.stormpath.com/v1/applications?createDirectory=true'

  ここで、

  • $YOUR_API_KEY_IDapiKey.properties 内の apiKey.id の値で、
  • $YOUR_API_KEY_SECRETapiKey.properties 内の apiKey.secret の値です。

  これによりアプリケーションが作成されます。レスポンスの例は次のようになります。

{
    "href": "https://api.stormpath.com/v1/applications/aLoNGrAnDoMAppIdHeRe",
    "name": "Apache Shiro Tutorial Webapp",
    "description": null,
    "status": "ENABLED",
    "tenant": {
        "href": "https://api.stormpath.com/v1/tenants/sOmELoNgRaNDoMIdHeRe"
    },
    "accounts": {
        "href": "https://api.stormpath.com/v1/applications/aLoNGrAnDoMAppIdHeRe/accounts"
    },
    "groups": {
        "href": "https://api.stormpath.com/v1/applications/aLoNGrAnDoMAppIdHeRe/groups"
    },
    "loginAttempts": {
        "href": "https://api.stormpath.com/v1/applications/aLoNGrAnDoMAppIdHeR/loginAttempts"
    },
    "passwordResetTokens": {
        "href": "https://api.stormpath.com/v1/applications/aLoNGrAnDoMAppIdHeRe/passwordResetTokens"
    } 
}

  トップレベルの href である https://api.stormpath.com/v1/applications/$YOUR_APPLICATION_ID を書き留めておいてください。この href は、shiro.ini の設定で使います。


アプリケーションテスト用ユーザーアカウントを作成する

  アプリケーションができたので、サンプルテストユーザーを作成しようと思います:

curl -X POST --user $YOUR_API_KEY_ID:$YOUR_API_KEY_SECRET \
    -H "Accept: application/json" \
    -H "Content-Type: application/json" \
    -d '{
           "givenName": "Jean-Luc",
           "surname": "Picard",
           "username": "jlpicard",
           "email": "capt@enterprise.com",
           "password":"Changeme1"
        }' \
 "https://api.stormpath.com/v1/applications/$YOUR_APPLICATION_ID/accounts"

  上記の URL の $YOUR_APPLICATION_ID をあなたのアプリケーション ID に変更することを忘れないでください。


2b : shiro.ini でレルムを設定する

  Shiro の要請にあわせて、少なくとも一つの接続先ユーザー格納領域を選び、それからそのデータ領域を表現する Realm を設定し、Shiro の SecurityManager に伝える必要があります。

  step2 ブランチをチェックアウトすると、次の内容が shiro.ini ファイルの [main] セクションに追加されます。

# ユーザーデータ格納領域に接続する Realm を設定します。このシンプルな
# チュートリアルでは、Stormpath を指すだけです。セットアップ時間は 5 分。
stormpathClient = com.stormpath.shiro.client.ClientFactory
stormpathClient.cacheManager = $cacheManager
stormpathClient.apiKeyFileLocation = $HOME/.stormpath/apiKey.properties
stormpathRealm = com.stormpath.shiro.realm.ApplicationRealm
stormpathRealm.client = $stormpathClient

# Stormpath コンソールで、あなたが作成するアプリケーション用の URL を
# 見つけてください (Applications → アプリケーション名選択 → Details
# → REST URL)。
stormpathRealm.applicationRestUrl = https://api.stormpath.com/v1/applications/$STORMPATH_APPLICATION_ID
stormpathRealm.groupRoleResolver.modeNames = name
securityManager.realm = $stormpathRealm

  次の変更をおこなってください。

  1. 最終的に stormpathClient.apiKeyFileLocation の値が /home/jsmith/.stormpath/apiKey.properties のようなものになるよう、$HOME を実際のホームディレクトリパス (例:/home/jsmith) に変更してください。このパスは、ステップ 2a でダウンロードした apiKey.properties ファイルの場所と同じでなければなりません。
  2. $STORMPATH_APPLICATION_ID を、ステップ 2a の最後で Stormpath が href として返してきた、実在する ID に変更してください。最終的に stormpathRealm.applicationRestUrl の値は次のような感じになります: https://api.stormpath.com/v1/applications/6hsPwoRZ0hCk6ToytVxi4D (もちろんアプリケーション ID の部分は異なります)


2c : 変更をコミットする

  変更した $HOME$STORMPATH_APPLICATION_ID  の値はあなたのアプリケーションに固有のものです。これらの変更をあなたのブランチにコミットしてください。

$ git add . && git commit -m "アプリ固有の値に置き換えた。"


2d : Web アプリケーションを実行する

  ステップ 2b と 2c で説明した変更を加えたあと、Web アプリケーションを起動してください。

$ mvn jetty:run

  今回は、次のようなログ出力が得られます。あなたの Web アプリケーションで、Shiro と新しいレルムが適切に設定されていることを示しています。

16:08:25.466 [main] INFO  o.a.shiro.web.env.EnvironmentLoader - Starting Shiro environment initialization.
16:08:26.201 [main] INFO  o.a.s.c.IniSecurityManagerFactory - Realms have been explicitly set on the SecurityManager instance - auto-setting of realms will not occur.
16:08:26.201 [main] INFO  o.a.shiro.web.env.EnvironmentLoader - Shiro environment initialized in 731 ms.

  Ctrl-C (Mac であれば Cmd-C) を入力して Web アプリケーションを終了させてください。


ステップ 3 : ログイン・ログアウトを有効にする

  さて、我々にはユーザーがいます。そして、UI を使って簡単にユーザーの追加、削除、無効化ができます。アプリケーションへのログイン・ログアウトやアクセス制御といった機能を有効にする作業を始めることができます。

  step3 ブランチをロードするため、次の git checkout コマンドを実行してください。

$ git checkout step3

  このチェックアウトにより、次の二つの変更がかかります。

  • 簡単なログインフォームを持つ src/main/webapp/login.jsp ファイルが新しく追加されます。これでログインをおこないます。
  • Web (URL) 固有の機能をサポートするために shiro.ini ファイルが更新されます。


3a : Shiro のフォームログイン・ログアウトのサポートを有効にする

  step3 ブランチの src/main/webapp/WEB-INF/shiro.ini ファイルには次の二つが追加されます。

[main]
shiro.loginUrl = /login.jsp

# [main] セクションでこれまでに設定した内容は、分かりやすくするため省略。

[urls]
/login.jsp = authc
/logout = logout


shiro.*

  [main] セクションの先頭に、新しい行があります。

shiro.loginUrl = /login.jsp

  これは特別な設定ディレクティブで、Shiro に対して、「Shiro のデフォルトフィルター群のうち loginUrl プロパティーを持つもの全てについて、そのプロパティー値を /login.jsp に設定せよ」、と指示するものです。

  これにより、Shiro のデフォルト authc フィルター (デフォルトでは FormAuthenticationFilter) がログインページについて知ることができます。FormAuthenticationFilter が正しく動作するためには、これが必要なのです。


[urls] セクション

  [urls] セクションは、新しい Web 固有の INI セクションです。

  このセクションでは、非常に簡潔な {名前 - 値}・シンタックスを用いて、指定の URL パスに対するリクエストをどのようにフィルターするかを Shiro に指示します。[urls] セクション内の全てのパスは、Web アプリケーションの HttpServletRequest.getContextPath() の値からの相対パスです。

  この {名前 - 値} の組は、リクエストをフィルターする極めて強力な方法を提供し、あらゆる種類のセキュリティールールを扱えます。URL とフィルターのチェインに関する深い説明はこの文書の範囲外ですが、興味があれば、こちらをお読みください

  ここでは、追加された 2 行を説明しましょう。

/login.jsp = authc
/logout = logout

  • 一番目の行は、「/login.jsp という URL へのリクエストを受けたら常に、そのリクエストの間、Shiro の authc フィルタを有効にする」、ということを示しています。
  • 二番目の行は、「/logout という URL へのリクエストを受けたら常に、そのリクエストの間、Shiro の logout フィルターを有効にする」、ということを意味しています。

  これらのフィルターは両方とも少し特殊です。両方とも、実際には、何かが後ろに控えていることを要求しないのです。フィルターするかわりに、これらのフィルターはリクエストを全て処理してしまいます。つまり、これらの URL に対するリクエストを処理するためにあなたがやらなければならないことは何もないのです (コントローラーを書かなくてもよいのです!)。Shiro が必要に応じてリクエストを処理します。


3b : ログインページを追加する

  ステップ 3a でログイン・ログアウトのサポートを有効にしたので、次は、ログインフォームを表示する /login.jsp ページを実際に用意する必要があります。

  step3 ブランチには、新しく src/main/webapp/login.jsp ページが含まれています。Bootstrap をテーマに用いた HTML で、十分にシンプルなログインページとなっていますが、そこには 4 つの重要なポイントがあります。

  1. フォームの action は空文字列です。フォームに action が設定されていないとき、ブラウザはフォームリクエストを同じ URL  に submit します。これで良いのです。なぜなら、自動的にログイン submit を処理するよう、対象 URL を Shiro に伝えるからです。shiro.ini 内の /login.jsp = authc という行が、authc フィルターでログイン submit を処理することを指示している部分です。
  2. username というフォームフィールドがあります。Shiro の authc フィルターはログイン submit を処理するとき、自動的に username というリクエストパラメーターを探し、その値をログインに使用します (多くのレルムが username でメールアドレスやユーザー名を受け取ります)。
  3. pasword というフォームフィールドがあります。Shiro の authc フィルターはログイン submit を処理するとき、自動的に password というリクエストパラメーターを探します。
  4. rememberMe チェックボックスがあります。checked 状態の値は、真だと認識できそうな値 (true, t, 1, enabled, y, yes, on) にすることができます。

  我々の login.jsp フォームでは、デフォルトの username, password, rememberMe フォールフィールド名を使用しています。これらの名前は設定で変更することも可能です。詳細は FormAuthenticationFilter の JavaDoc を参照してください。


3c : Web アプリケーションを実行する

  ステップ 3a と 3b で説明した変更を加えたあと、Web アプリケーションを起動してください。

$ mvn jetty:run


3d : ログインする

  Web ブラウザで localhost:8080/login.jsp を開くと、そこには我々の輝かしいログインフォームがあります。

  ステップ 2 の最後で作成したアカウントのユーザー名とパスワードを入力し、Login を押してください。ログインに成功すれば、ホームページに遷移します! ログインに失敗した場合はログインページが再度表示されます。

  Tip: ログイン成功時にホームページ (= コンテキストパス / ) 以外の場所にユーザーをリダイレクトさせたいときは、authc.successUrl = /whatever を INI ファイルの [main] セクションで設定してください。

  Ctrl-C (Mac であれば Cmd-C) を入力して Web アプリケーションを終了させてください。


ステップ 4 : ユーザーごとに UI を変更する

  ユーザーが誰であるかに応じて Web ユーザーインターフェースを変更するという要望はよくあることです。Shiro は、現在ログイン中の Subject (ユーザー) をベースに処理をおこなうための JSP タグライブラリをサポートしているので、こういったことは簡単におこなえます。

  step4 ブランチをロードするため、次の git checkout コマンドを実行してください。

$ git checkout step4

  これにより、home.jsp ページに次の変更が加えられます。

  • 現在ページを閲覧中のユーザーがログインしていない場合、「Welcome Guest」のメッセージとログインページへのリンクが表示されます。
  • 現在ページを閲覧中のユーザーがログインしている場合、「Welcome ユーザー名」というメッセージとログアウトするリンクが表示されます。

  画面右上にユーザーコントロールを表示するナビゲーションバーでは、この手の UI カスタマイズはとても一般的なことです。


4a : Shiro タグライブラリの宣言を追加する

  home.jsp への変更で、先頭に次の 2 行が追加されます。

<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>

  この二つの JSP ページディレクティブで、Core タグライブラリ (c:) と Shiro タグライブラリ (shiro:) を使用できるようになります。


4b : Shiro のゲストタグとユーザータグを追加する

  home.jsp はページのボディー部分 (<h1> ウェルカムメッセージの直後) も変更され、<shiro:guest><shiro:user> タグの両方が追加されます。

<p>Hi <shiro:guest>Guest</shiro:guest><shiro:user>
<%
  // 通常、これはページ内でやるべきではなく、かわりに、何らかの
  // 適切な MVC コントローラー内でやるべきです。しかし、この
  // チュートリアルでは、下記の <c:out/> タグで参照するため、
  // Shiro の PrincipalCollection から Stormpath アカウントの
  // データを取り出しています。
  request.setAttribute("account",
    org.apache.shiro.SecurityUtils.getSubject().getPrincipals()
    .oneByType(java.util.Map.class));
%>
<c:out value="${account.givenName}"/></shiro:user>!
  ( <shiro:user>
      <a href="<c:url value="/logout"/>">Log out</a>
    </shiro:user>
    <shiro:guest>
      <a href="<c:url value="/login.jsp"/>">Log in</a>
    </shiro:guest> )
</p>

  形を整えるため少し読みづらくなっていますが、二つのタグは次の箇所で使われています。

  • <shiro:guest>: このタグは、現在の Shiro Subject がアプリケーションのゲストである場合のみ、内部コンテンツを表示します。Shiro は、アプリケーションにログインしていない、もしくは (Shiro の remember me 機能を利用して) 前回のログインを記憶していない Subject をゲストと定義しています。
  • <shiro:user>: このタグは、現在の Shiro Subject がアプリケーションのユーザーである場合のみ、内部コンテンツを表示します。Shiro は、現在アプリケーションにログインしている (認証されている)、もしくは (Shiro の remember me 機能を利用して) 前回のログインを記憶している Subject をユーザーと定義しています。

  上記のコードは、Subject がゲストであれば次の表示をおこないます。

Hi Guest! (Log in)

  ここで、Log in は /login.jsp へのリンクになります。

  Subject がユーザーの場合は次のように表示します。

Hi jsmith! (Log out)

  ここではログインしているアカウントのユーザー名が jsmith であると仮定しています。Log out は、Shiro の logout フィルターで処理される /logout という URL へのリンクになります。

  ここまで見てきたとおり、ページセクション全体や、機能、UI コンポーネントといったものを有効化・無効化することができます。<shiro:guest><shiro:user> だけではなく、現在の Subject に関する様々な情報をもとに UI をカスタマイズするための数多くの便利な JSP タグを、Shiro はサポートしています。


4c : Web アプリケーションを実行する

  step4 ブランチをチェックアウト後、Web アプリケーションを起動してください。

$ mvn jetty:run

  localhost:8080 にゲストとしてアクセスし、それからログインしてください。ログインが成功すると、あなたが既知のユーザーであることを反映してページの内容が更新されます!

  Ctrl-C (Mac であれば Cmd-C) を入力して Web アプリケーションを終了させてください。


ステップ 5 : 認証済みユーザーのみアクセスを許可する

  Subject の状態に応じてページの内容を変更することは可能ですが、一方で、ユーザーが認証済みかどうかに応じて、現在のインタラクション中は Web アプリケーションのセクション全体に制限をかけたいと思うこともあるでしょう。

  これは、Web アプリケーションのユーザーのみが見ることを許された慎重に扱うべき情報、例えば支払詳細や他のユーザーを制御する機能、などを表示するときは特に重要です。

  step5 ブランチをロードするため、次の git checkout コマンドを実行してください。

$ git checkout step5

  ステップ 5 では次の 3 つが変更されています。

  1. 認証済みユーザーのみにアクセスを許す新しいセクション (URL パス) が追加されています。
  2. shiro.ini が変更され、認証済みユーザーのみ当該セクションにアクセスが許されるようになっています。
  3. 現在の Subject が認証済みであるかどうかに応じて出力を変えるよう、ホームページが変更されています。

5a : 制限付きの新しいセクションを追加する

  src/main/webapp/account ディレクトリが新しく追加されています。このディレクトリ (および下位の全てのパス) は「プライベート」もしくは「認証済みに限る」セクションを表していて、ログインしているユーザーのみに制限するセクションをシミュレートしています。src/main/webapp/account/index.jsp ファイルは、シミュレートされる「ホームアカウント」ページです。


5b : shiro.ini を設定する

  shiro.ini も変更され、[urls] セクションの末尾に次の行が追加されています。

/account/** = authc

  ここで Shiro フィルターチェインを定義していますが、意味は、「/account (およびその下位パス) へのリクエストは全て認証されなければならない」、となります。

  しかしながら、もし誰かがこのパスもしくはその下位パスにアクセスした場合、何が起こるのでしょうか?

  ステップ 3 で [main] セクションに次の行を追加したことを覚えているでしょうか?

shiro.loginUrl = /login.jsp

  この行により、authc フィルターに Web アプリケーションのログイン URL が自動的に設定されたのでしたね。

  この設定行により、authc フィルターは賢くなっていて、現在の Subject が /account にアクセスしたときに認証済みでなければ、Subject を /login.jsp ページに自動的にリダイレクトします。ログイン成功後は、ユーザーがアクセスしようとしていたページ (/account) に自動的にリダイレクトされます。便利ですね!


5c : ホームページを更新する

  ステップ 5 の最後の変更は、Web サイトに新しくアクセスできるようになった場所があることをユーザーに知らせるよう、 /home.jsp ページを更新することです。ウェルカムメッセージのあとに次の行が追加されています。

<shiro:authenticated>
  <p>
    Visit your <a href="<c:url value="/account"/>">account page</a>.
  </p>
</shiro:authenticated>
<shiro:notAuthenticated>
  <p>
    If you want to access the authenticated-only
    <a href="<c:url value="/account"/>">account page</a>,
    you will need to log-in first.
  </p>
</shiro:notAuthenticated>

  <shiro:authenticated> タグは、現在のセッションで Subject が既にログイン済み (認証済み) の場合のみ、内容を表示します。これにより Subject は、Web サイトに新しくアクセスできるようになった場所があることを知ります。

  <shiro:notAuthenticated> タグは、現在のセッションで Subject がまだ認証されていない場合のみ、内容を表示します。

  ところで、notAuthenticated の内容に /account セクションの URL が含まれていることには気付かれたでしょうか? これは問題ありません。authc フィルターは、ログイン→リダイレクト・フローを先に説明したように扱います。

  新しい変更とともに Web アプリケーションを起動し、試してみてください!



5d : Web アプリケーションを実行する

  step5 ブランチをチェックアウト後、Web アプリケーションを起動してください。

$ mvn jetty:run

  localhost:8080 にアクセスして新しくできた /account リンクをクリックし、ログインページにリダイレクトされることを確認してください。ログイン後、ホームページに戻り、内容が変化してあなたが認証されていることを確認してください。ログアウトするまでは、アカウントページとホームページの行き来を何回でも望むだけできます。素晴らしいでしょう!

  Ctrl-C (Mac であれば Cmd-C) を入力して Web アプリケーションを終了させてください。


ステップ 6 : ロールに基づくアクセス制御

  認証に基づくアクセス制御に加え、現在の Subject に与えられたロールに応じてアプリケーションの特定部部へのアクセスを制限するという要望も、よくあるものです。

  step6 ブランチをロードするため、次の git checkout コマンドを実行してください。

$ git checkout step6


6a : ロールを追加する

  ロールに応じたアクセス制御をおこなうには、ロールが存在しなければなりません。

  このチュートリアルでそれをおこなう最速の方法は、Stormpath にグループを作ることです (Stormpath では、Stormpath Group をロールと同様の目的で使えます)。

  これをおこなうには、UI にログインし次のように進み、

DirectoriesApache Shiro Tutorial Webapp DirectoryGroups

下記の 3 つのグループを追加してください。

  • Captains
  • Officers
  • Enlisted

(スター・トレックのアカウントテーマを保つためです :) )

  グループ作成後、Jean-Luc Picard アカウントを Captains グループと Officers グループに追加してください。その場限りのアカウントを作成して好きなグループに追加してもよいでしょう。ユーザーアカウントに割り当てられたグループの違いに応じた変化を確認できるよう、幾つかのアカウントはグループがかぶらないようにしてください。


6b : RBAC (Role-Based Access Control) タグ

  どのロールを持っていてどのロールを持っていないのかをユーザーが確認できるよう、/home.jsp ページを更新します。ホームページに <h2>ロール</h2> セクションを新しく作成し、次のメッセージ群を加えてください。

<h2>ロール</h2>

<p>
あなたが持っているロールと持っていないロールです。
ログアウトして別のユーザーアカウントでログインし直すと、
ロールが変化します。
</p>

<h3>あなたが持っているロール:</h3>

<p>
    <shiro:hasRole name="Captains">Captains<br/></shiro:hasRole>
    <shiro:hasRole name="Officers">Bad Guys<br/></shiro:hasRole>
    <shiro:hasRole name="Enlisted">Enlisted<br/></shiro:hasRole>
</p>

<h3>あなたが持っていないロール:</h3>

<p>
    <shiro:lacksRole name="Captains">Captains<br/></shiro:lacksRole>
    <shiro:lacksRole name="Officers">Officers<br/></shiro:lacksRole>
    <shiro:lacksRole name="Enlisted">Enlisted<br/></shiro:lacksRole>
</p>

  <shiro:hasRole> タグは、指定されたロールが現在の Subject に割り当てられている場合のみ、内容を表示します。

  <shiro:lacksRole> タグは、指定されたロールが現在の Subject に割り当てられていない場合のみ、内容を表示します。


6c : RBAC フィルターチェイン

  読者の皆さんに残された練習 (ステップとしては定義されていません) は、Web サイトに新しいセクションを作成し、現在のユーザーに割り当てられたロールに基づいてそのセクションへの URL アクセスを制限することです。

  ヒント: ロール・フィルターを用いて、Web アプリケーションの新しい箇所へのフィルターチェインを宣言します。



6d : Web アプリケーションを実行する

  step6 ブランチをチェックアウト後、Web アプリケーションを起動してください。

$ mvn jetty:run

  localhost:8080 を開き、異なるロールを割り当てられた別々のユーザーアカウントでログインし、ホームページのロール・セクションの内容が変化することを確認してください!

  Ctrl-C (Mac であれば Cmd-C) を入力して Web アプリケーションを終了させてください。


まとめ

  Web アプリケーションで Shiro を活用する方法を説明したこの入門チュートリアルが、あなたの役に立てたのであれば幸いです。このチュートリアルの続編では次のトピックをカバーする予定です。

  • Shiro の極めて強力なパーミッションとパーミッションに基づくアクセス制御
  • RDBMS や NoSQL データストアなどの、異なるユーザーデータ格納領域の組み込み


修正と Pull リクエスト

  何か間違いがあれば修正を GitHub の Pull リクエストとして https://github.com/lhazlewood/apache-shiro-tutorial-webapp リポジトリに送ってください。ご協力に感謝します!!!


2013年12月1日日曜日

Google App Engine + Jersey (JAX-RS)

Web サービスを Google App Engine + Jersey (JAX-RS) で作成するときのメモ。

  1. Eclipse の m2e プラグインを更新して Maven 3.1 以上が使えるようにする。guestbook-archetype (後述) が Maven 3.1 を要求するため。ただし、2013年11月末では m2e プラグインの stable (1.4) が Maven 3.1 を含んでいないため、Eclipse → Help → Install New Software... で http://download.eclipse.org/technology/m2e/milestones/1.5 を追加し、そちらから m2e プラグインの更新をかける。
  2. Eclipse の Google App Engine 関連のプラグインを更新する。
  3. Eclipse で Maven プロジェクトを作成する。ウィザードの Filter に guestbook と入力して com.google.appengine.archetypes:guestbook-archetype を表示し、選択する。 
  4. pom.xml 内の appengine.target.version を最新のものに更新する。現時点では 1.8.8。
  5. Jersey の maven モジュール群を pom.xml に追加する。Jersey サイト (http://jersey.java.net/) のドキュメントを参照し、Jersey の最新バージョンを確認する (現在 2.4.1)。そして、GAE がサポートする Servlet API (現在 2.5) の場合、どのモジュールを選べばよいのか確認する。Chapter 2. Modules and dependencies / 2.3.2 Servlet based server-side application (https://jersey.java.net/documentation/latest/modules-and-dependencies.html#servlet-app-general) によると、Servlet API 3.0 より前のバージョンの場合は jersey-container-servlet ではなく jersey-container-servlet-core のほうを使えと書いてある。結果、次の dependency を pom.xml に追加する。
    <dependency>
        <groupId>org.glassfish.jersey.containers</groupId>
        <artifactId>jersey-container-servlet-core</artifactId>
        <version>2.4.1</version>
    </dependency>
    
  6. **.core パッケージを作る。(アプリケーション全体に関係するものは、このパッケージに置く。これは自分ルール。)
  7. javax.ws.rs.Application クラスを拡張して App クラスを作り、core パッケージ下に置く。とりあえず、ほぼ空実装。
    package com.example.myapp.server.core;
    
    import java.util.HashSet;
    import java.util.Set;
    import javax.ws.rs.core.Application;
    
    public class App extends Application
    {
        private final Set<Object> singletons = new HashSet<Object>();
        private final Set<Class<?>> classes = new HashSet<Class<?>>();
    
        @Override
        public Set<Class<?>> getClasses()
        {
            return classes;
        }
    
        @Override
        public Set<Object> getSingletons()
        {
            return singletons;
        }
    }
  8. src/main/webapp/WEB-INF/web.xml に Jersey サーブレットを登録する。ルートパス以下を全て Jersey サーブレットに処理させる。
    <servlet>
      <servlet-name>JAX-RS Servlet</servlet-name>
        <servlet-class>org.glassfish.jersey.servlet.ServletContainer
        </servlet-class>
        <init-param>
          <param-name>javax.ws.rs.Application</param-name>
          <param-value>com.example.myapp.server.core.App
          </param-value>
        </init-param>
    </servlet>
    
    <servlet-mapping>
      <servlet-name>JAX-RS Servlet</servlet-name>
      <url-pattern>/</url-pattern>
    </servlet-mapping>
    
  9. とりあえず、ここで一回アプリを実行してみる。プロジェクトのコンテキストメニューから、「Run As」→「Maven build」→「DevAppServer : appengine:devserver」を実行する。
  10. しかし、ここでエラーが出る。(2014/2/18 追記: Jersey version 2.5.1 で本不具合が修正されていることを確認。)
    java.lang.NoClassDefFoundError: sun.misc.Unsafe is a restricted class.
    Please see the Google  App Engine developer's guide for more details.
    
    ググってみると、jersey-common に含まれている org/glassfish/jersey/internal/util/JdkVersion.java の static イニシャライザの次のコードが原因とのこと (Answer to "NoClassDefFoundError when running Jersey in GAE")。既に 2013/10/10 に不具合報告 JERSEY-2136 があがっていることも判明。(というか、JERSEY-2248 で不具合報告したら duplicate にされたので、報告済みの問題だということを知った。)
       static {
    
           boolean isUnsafeFound;
    
           try {
               isUnsafeFound = Class.forName("sun.misc.Unsafe") != null;
           } catch (Throwable t) {
               isUnsafeFound = false;
           }
    
           IS_UNSAFE_SUPPORTED = isUnsafeFound;
       }
    
    上記のコードは、Java 実行環境に sun.misc.Unsafe クラスが存在すれば IS_UNSAFE_SUPPORTED フラグを true にし、そうでなければ false にするという意図がある。

    なぜこのコードが GAE 上で問題を起こすかというと、GAE の Java 実行環境に sun.misc.Unsafe は存在するものの、GAE の都合によりそのクラスが利用不可となっているせいである (利用不可の理由は知らない)。

    上記のコードを GAE でも機能させるには、次のようなパッチをあてればよい。
    --- JdkVersion.java.old Thu Nov 28 04:55:11 2013
    +++ JdkVersion.java     Sun Dec  1 02:27:06 2013
    @@ -50,16 +50,18 @@
         private static final boolean IS_UNSAFE_SUPPORTED;
    
         static {
    +        IS_UNSAFE_SUPPORTED =
    +             classExists("sun.misc.Unsafe") &&
    +            !classExists("com.google.appengine.api.LifecycleManager");
    +    }
    
    -        boolean isUnsafeFound;
    -
    +    private static boolean classExists(String className) {
             try {
    -            isUnsafeFound = Class.forName("sun.misc.Unsafe") != null;
    +            Class.forName(className);
    +            return true;
             } catch (Throwable t) {
    -            isUnsafeFound = false;
    +            return false;
             }
    -
    -        IS_UNSAFE_SUPPORTED = isUnsafeFound;
         }
    
         private static final JdkVersion UNKNOWN_VERSION = new JdkVersion(-1, -1, -1, -1);
    
    このパッチだと分かりにくいが、結果はこのようなコードになる。
        static {
            IS_UNSAFE_SUPPORTED =
                 classExists("sun.misc.Unsafe") &&
                !classExists("com.google.appengine.api.LifecycleManager");
        }
    
        private static boolean classExists(String className) {
            try {
                Class.forName(className);
                return true;
            } catch (Throwable t) {
                return false;
            }
        }
    
    このパッチをあてた JdkVersion.java を自分のアプリケーションのソースコードツリーに取り込んでビルドし、再度アプリを実行してみたが、どうも変更した JdkVersion が参照されない。そこで、WEB-INF/lib/jersey-common-2.4.1.jar 内の JdkVersion.class よりも先に自分のアプリ内に取り込んだ JdkVersion.class が先にクラスロードされるよう、pom.xml を次のように編集して WEB-INF/classes 以下にパッチをあてた JdkVersion.class が配置されるようにした。 (WEB-INF/lib/*.jar よりも WEB-INF/classes 以下のクラス群のほうがクラスローディングの順番が先との仕様があるとのことなので。Servlet API 2.4 から?)。
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-war-plugin</artifactId>
      <version>2.3</version>
      <configuration>
        <archiveClasses>true</archiveClasses>
        <webResources>
          <!-- in order to interpolate version from pom into appengine-web.xml -->
          <resource>
            <directory>${basedir}/src/main/webapp/WEB-INF</directory>
            <filtering>true</filtering>
            <targetPath>WEB-INF</targetPath>
          </resource>
          <resource>
            <directory>${project.build.outputDirectory}</directory>
            <targetPath>WEB-INF/classes</targetPath>
            <filtering>true</filtering>
            <includes>
              <include>org/glassfish/jersey/internal/util/JdkVersion.class</include>
            </includes>
          </resource>
        </webResources>
      </configuration>
    </plugin>
    
    しかし今度は別のエラーが出る。
    Unable to instrument org.glassfish.jersey.internal.util.JdkVersion.
    Security restrictions may not be entirely emulated.
    java.lang.ArrayIndexOutOfBoundsException: 8669
    at com.google.appengine.repackaged.org.objectweb.asm.ClassReader.<init>(ClassReader.java:181)
    
    ググってみると、このエラーは、バージョンの異なる複数のクラスが存在するときに起こるらしい。しかしながら、バージョンが異なるクラスが複数あっても、クラスローディングの過程で先に見つかったほうを利用するという仕様のはずだから、問題が起こるのはおかしくないか? と、思いながらスタックトレースを見ると、com.google.appengine.repackaged というパッケージ名に気が付いた。ここからは推測だが、おそらく、GAE 用にクラス群を再パッケージング (repackage) する仕組みがあって、そいつが悪さしているのだろう。Java の仕様に敬意を払わない Google なら普通にやりかねない。

    そういうわけで、美しくない方法だがやむをえない。既にローカルの MAVEN リポジトリ (~/.m2) にダウンロード済みの jersey-common-2.4.1 を一回削除し、JdkVersion.class を置き換えたバージョンの jersey-common-2.4.1 をローカルインストールする。
    # 作業用のディレクトリを作成し、そこに移動する。
    $ mkdir -p ~/tmp/jersey-common-2.4.1-fixed
    $ cd ~/tmp/jersey-common-2.4.1-fixed
    
    # 既存の jersey-common-2.4.1 の jar と pom を、ローカルの
    # Maven ディレクトリから作業ディレクトリにコピーする。
    $ cp ~/.m2/repository/org/glassfish/jersey/core/jersey-common/2.4.1/\
    jersey-common-2.4.1.jar .
    $ cp ~/.m2/repository/org/glassfish/jersey/core/jersey-common/2.4.1/\
    jersey-common-2.4.1.pom .
    
    # パッチを当ててコンパイルした JdkVersion.class を
    # 作業ディレクトリにコピーする。
    $ mkdir -p org/glassfish/jersey/internal/util
    $ cp ${プロジェクトディレクトリ}/target/classes/org/glassfish/jersey/\
    internal/util/JdkVersion.class org/glassfish/jersey/internal/util/
    
    # jar ファイル内の JdkVersion.class を置き換える。
    $ jar -uf jersey-common-2.4.1.jar \
          org/glassfish/jersey/internal/util/JdkVersion.class
    
    # ローカルの Maven ディレクトリから jersey-common を削除する。
    $ rm -rf ~/.m2/repository/org/glassfish/jersey/core/jersey-common
    
    # 手を加えた jersey-common-2.4.1 をローカルの Maven
    # リポジトリにインストールする。
    # 参考: Installing an artifact to a specific local repository path
    $ mvn install:install-file -Dfile=jersey-common-2.4.1.jar \
          -DgroupId=org.glassfish.jersey.core \
          -DartifactId=jersey-common \
          -Dversion=2.4.1 \
          -Dpackaging=jar \
          -DpomFile=jersey-common-2.4.1.pom
    
    ここまでやってから、プロジェクトを一度 Maven clean し、Web アプリを再起動すれば、エラーなく Jersey サーブレットが起動する。
  11. Jersey が動くことが確認できたので、JAX-RS でいうところのリソースを格納するためのパッケージ **.resource を作成する。
  12. リソースクラス群のためのベースクラスを作る。とりあえず空実装。
    package com.example.myapp.server.resource;
    
    public class ResourceBase
    {
    
    }
    
  13. 次はルートリソースを表現するクラスを作る。GET リクエストがきたときに hello と返すだけの実装をしてみる。
    package com.example.myapp.server.resource;
    
    import javax.ws.rs.GET;
    import javax.ws.rs.Path;
    
    @Path("/")
    public class RootResource extends ResourceBase
    {
        @GET
        public String get()
        {
            return "hello";
        }
    }
    
  14. ルートクラスを前述の App クラスで登録する。
        @Override
        public Set<Class<?>> getClasses()
        {
            classes.add(RootResource.class);
    
            return classes;
        }
    
  15. ここまでやったら、Web アプリケーションを再起動する。http://localhost:8080/ にアクセスして hello と表示されたら成功。
  16. Jersey 2.x 系を Google App Engine で動かすのがこんなに手間だとは思わなかった。疲れたので、ユーザー管理等のアプリ寄りの話は、書くとしても別ブログ。
  17. おしまい。