SeamFramework.orgCommunity Documentation
SeamのセキュリティAPIはSeamベースのアプリケーションに種々のセキュリティ関連機能を提供します。 これらの機能には、以下のようなものがあります。
認証ー種々のセキュリティプロバイダによるユーザー認証を可能にする、拡張可能なJAASベースの認証層を提供します
ID管理ーSeamアプリケーション動作時にユーザーとロールを管理するAPIを提供します
認可ーユーザーロール、永続的なルールをベースにしたパーミッションや、カスタマイズ可能でセキュリティロジックを簡単に実装できるプラグイン可能な許可リゾルバーをサポートする、非常に包括的な、認可フレームワーク
パーミッション管理ーアプリケーションのセキュリティポリシーを容易に管理する事を可能にする一連のSeam組み込みコンポーネント
CAPCHAのサポートーSeamで作られたサイトを自動検索プログラムスクリプトによる予期しない動作から保護するための支援をします
等々
この章ではこれらの機能の詳細について説明します
In some situations it may be necessary to disable Seam Security, for instances during unit tests or because you are using a different approach to security, such as native JAAS. Simply call the static method Identity.setSecurityEnabled(false)
to disable the security infrastructure. Of course, it's not very convenient to have to call a static method when you want to configure the application, so as an alternative you can control this setting in components.xml:
エンティティのセキュリティ
Hibernateセキュリティインタセプタ
Seamセキュリティインタセプタ
ページ単位の制約
Servlet API security integration
Assuming you are planning to take advantage of what Seam Security has to offer, the rest of this chapter documents the plethora of options you have for giving your user an identity in the eyes of the security model (authentication) and locking down the application by establishing constraints (authorization). Let's begin with the task of authentication since that's the foundation of any security model.
Seam セキュリティの提供する認証機構は JAAS (Java Authentication and Authorization Service) の上に構築されており、 ユーザー認証のための堅牢で設定の自由度の高い API を提供しています。 しかしながら、 Seamで は JAAS の複雑さを隠蔽したより単純化された認証機構も提供しています。
この章で解説するSeamのID管理機能を使用するのであれば、authenticator(認証)コンポーネントを作成する必要はありませんのでこの章を飛ばしても結構です。
単純な認証機構ではSeamアプリケーションのコンポーネントに認証を委ねるSeamLoginModule
(これは、Seamに内蔵されているJAASのログインモジュールです)を使います。 このログインモジュールはSeamのデフォルトのアプリケーションポリシーとして予め設定されていますので、新たに設定に追加する事なく使用することができます。 また、作成したアプリケーションのエンティティクラスを利用して、認証メソッドを記述したり、 サード―パーティのプロバイダを使った認証をする事ができます。 この「単純な認証機構」を利用するためにはcomponents.xml
に下記のようにidentity
コンポーネントを設定する必要があります。
<components xmlns="http://jboss.com/products/seam/components"
xmlns:core="http://jboss.com/products/seam/core"
xmlns:security="http://jboss.com/products/seam/security"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation=
"http://jboss.com/products/seam/components http://jboss.com/products/seam/components-2.1.xsd
http://jboss.com/products/seam/security http://jboss.com/products/seam/security-2.1.xsd">
<security:identity authenticate-method="#{authenticator.authenticate}"/>
</components
>
EL式 #{authenticator.authenticate}
はauthenticator
コンポーネントのauthenticate
メソッドを使って、ユーザーの認証を行うことを示すメソッドバインディングです。
components.xml
中のidentity
のauthenticate-method
プロパティでSeamLoginModule
にユーザーの認証に使うメソッドを指定します。 このメソッドはパラメータを取らず、認証が成功したか否かのboolean型を返します。 ユーザーのusernameとpasswordはCredentials.getUsername()
と Credentials.getPassword()
からそれぞれ取得します(また、Identity.instance().getCredentials()
からcredentials
コンポーネントを参照する事もできます)。 ユーザーがメンバーとして参加するロールはIdentity.addRole()
により指定される必要があります。 以下にPOJOコンポーネント中の認証メソッドの完全な例を示します。
@Name("authenticator")
public class Authenticator {
@In EntityManager entityManager;
@In Credentials credentials;
@In Identity identity;
public boolean authenticate() {
try {
User user = (User) entityManager.createQuery(
"from User where username = :username and password = :password")
.setParameter("username", credentials.getUsername())
.setParameter("password", credentials.getPassword())
.getSingleResult();
if (user.getRoles() != null) {
for (UserRole mr : user.getRoles())
identity.addRole(mr.getName());
}
return true;
}
catch (NoResultException ex) {
return false;
}
}
}
上記の例では、User
とUserRole
はアプリケーション独自のエンティティBeanとなっています。 パラメータ roles
は "admin", "user" の様に文字列として、Set
に追加されてゆく必要があります。 この例の場合、userが見付からずにNoResultException
が投げられた場合には、認証メソッドはfalse
を返して、認証が失敗したことを示します。
認証メソッドを記述する場合、副次的な影響を受けない、あるいは影響が最小になるようにすることが重要です。 これは、セキュリティAPIにより何回認証メソッドが呼び出されるのか保証が無く、一回の要求で複数回実行されることもあるからです。 このため、認証が成功あるいは失敗した時に実行されるコードはイベントオブザーバを利用して記述されるべきです。 Seamのセキュリティにより発生するイベントについての詳細はこの章の後半に記載されています。
Identity.addRole()
メソッドは現在のユーザーが認証されているか否かで動作が異なります。 認証されていないセッションの場合には、addRole()
は認証過程でのみ呼び出され、指定されたロールは、認証されていないロールの仮のリストに登録され、認証が成功すると、仮のロールから実際のロールに移行し、このロールでIdentity.hasRole()
が実行され、trueが返されます。 以下のシークエンス図に、仮の認証ロールリストの認証プロセスにおける役割について示します。
カレントセッションがすでに認証されている場合にIdentity.addRole()
を呼ぶと指定されたロールが即座に現在のユーザーに付与されます。
例として、ログインに成功する度にユーザーの統計データを更新する場合を考えてみましょう。 これは、org.jboss.seam.security.loginSuccessful
イベントのオブザーバを記述する事により、以下のように実現できます。
@In UserStats userStats;
@Observer("org.jboss.seam.security.loginSuccessful")
public void updateUserStats()
{
userStats.setLastLoginDate(new Date());
userStats.incrementLoginCount();
}
このオブザーバメソッドは、Authenticatorコンポーネントを含め、どこにおいても構いません。 セキュリティ関連のイベントについてこの章でさらに見てゆきます。
credentials
コンポーネントはusername
およびpassword
属性を保持しており、一般的な認証処理に対応できるようになっています。 これらの属性はログインフォームのusernameとpasswordフィールドに直接バインドする事が可能です。 これらの属性が設定されてしまえば、後はidentity.login()
を呼び出すことにより、保持されているusernameとpasswordによるユーザーの認証が行われます。簡単なログインフォームの例を示します。
<div>
<h:outputLabel for="name" value="Username"/>
<h:inputText id="name" value="#{credentials.username}"/>
</div>
<div>
<h:outputLabel for="password" value="Password"/>
<h:inputSecret id="password" value="#{credentials.password}"/>
</div>
<div>
<h:commandButton value="Login" action="#{identity.login}"/>
</div
>
(loginと)同様にログアウトも、#{identity.logout}
を呼び出すことにより実行されます。ログアウトを実行することにより、現在認証されているユーザーのセキュリティの状態をクリアし、セッションは無効化されます。
まとめとして、認証システムを設定するためには、以下の三つのステップが必要となります。
認証メソッドをcomponents.xml
に設定する。
認証メソッドを記述する。
ユーザーを認証するためのログインフォームを記述する。
Seamのセキュリティ機能ではオンラインのWEBアプリケーションで一般的に提供されている"Remember me"(覚えておいてね)機能をサポートしています。 この機能は、二つの異なったモードをサポートしており、一つはusernameをユーザーのブラウザにクッキーとして保存し、ブラウザからpasswordの入力を促すものです(この場合でも、最近のほとんどのブラウザは、passwordを記憶しています)。
第2のモードはユニークなトークンをクッキーとして記憶しておいて、そのサイトに入ると、passwordの入力をする事なく自動的にユーザーの認証をする機能をサポートするものです。
クライアント側の永続的なクッキーによる自動的な認証(第2のモード)は危険で、ユーザーへの利便性は向上しますが、クロスサイトスクリプティングに対するセキュリティホールの影響を通常より遥かに重大な物としてしまいます。 認証のためのクッキーでなければ、XSSにより攻撃者に盗まれるクッキーは、現在のセッションのユーザーのクッキーという事になります。 これは、ユーザーがセッションを開いていなければ攻撃が有効でないことを意味し、攻撃可能な時間が非常に短い事を意味します。 攻撃者が自動認証をサポートする永続的なRemember meクッキーを盗む可能性があるとすれば、それはたいへん危険なことです。 この機能の利用の危険性は、システムがXSS攻撃に対してどれだけ防御できているのかに依存し、XSS攻撃に対して100%の防御を保証している必要がありますが、入力内容をWEBページに表示するようなサイトにとって、これは簡単ではありません。
ブラウザのベンダーはこの問題を認識しており、最近のブラウザでは新たに導入されたRemember Passwordをサポートしています。 この場合は、ブラウザは特定のドメイン、ウェブサイトに対してのusernameとpasswordを記憶しており、ウェブサイトとのセッションがアクティブでない状態で、ログインフォームのusernameとpasswordを自動的に埋めてゆきます。 ウェブデザイナの立場であれば、ログインのためのショートカットキーを設定しておけば、Remember Meと同様にユーザーの利便性を向上させることができます。 OS-xのSafariなど一部のブラウザでは、OSのキーチェインに暗号化したログインのフォームを記憶させています。 また、ブラウザのクッキーは一般に同期化させることはできませんが、ネットワーク環境ではこのキーチェインはラップトップからデスクトップへと移動させることができます。
まとめ:自動認証をする永続的なRemember Meの使用は一般化してしまっていますが、セキュリティ上、不適切であり使用すべきではありません。 ログイン時のusernameのみを記憶するクッキーを使用することには問題はありません。
デフォルトのRemember me(usernameのみ)機能を使用するためには、特に設定は必要ありません。 下の例のように、ログインフォームにremember meチェックボックスを入れて、これをrememberMe.enabled
とバインドするだけです。
<div>
<h:outputLabel for="name" value="User name"/>
<h:inputText id="name" value="#{credentials.username}"/>
</div>
<div>
<h:outputLabel for="password" value="Password"/>
<h:inputSecret id="password" value="#{credentials.password}" redisplay="true"/>
</div
>
<div class="loginRow">
<h:outputLabel for="rememberMe" value="Remember me"/>
<h:selectBooleanCheckbox id="rememberMe" value="#{rememberMe.enabled}"/>
</div
>
トークンベースの自動認証機能の remember me を使用するためには、まずトークンの記憶場所を設定する必要があります。 Seamでもサポートしていますが、このトークンの記憶場所としてはデータベースが一般的です。 しかし、org.jboss.seam.security.TokenStore
インタフェースを実装して独自の記憶場所を設定することも可能です。 この章では標準で提供されているJpaTokenStore
実装を使用して認証トークンをデータベーステーブルに記憶させることを前提としています。
まず最初に、トークンを保持する新たなエンティティを作ります。 以下に、一般的なエンティティの構造を示します。
@Entity
public class AuthenticationToken implements Serializable {
private Integer tokenId;
private String username;
private String value;
@Id @GeneratedValue
public Integer getTokenId() {
return tokenId;
}
public void setTokenId(Integer tokenId) {
this.tokenId = tokenId;
}
@TokenUsername
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
@TokenValue
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
このコードから分かるように、エンティティのusernameとトークンのプロパティには@TokenUsername
と@TokenValue
という特別なアノテーションが使われており、これらは認証トークンを含むエンティティに必須です。
次に、このエンティティBeanに対して認証トークンの出し入れをするために、 JpaTokenStore
を設定します。 これは、 components.xml
にtoken-class
属性を指定することにより行います。
<security:jpa-token-store token-class="org.jboss.seam.example.seamspace.AuthenticationToken"/>
ここまでが終了したら、最後はcomponents.xml
にRememberMe
コンポーネントの設定をする事です。 mode
はautoLogin
に設定されていなければなりません。
<security:remember-me mode="autoLogin"/>
これで、remember meをチェックしているユーザーがサイトを再訪した時に、自動的に認証されるようになります。
ユーザーがサイトを再訪した時に確実に自動的に認証される様にするために、components.xml
に以下が含まれている必要があります。
<event type="org.jboss.seam.security.notLoggedIn">
<action execute="#{redirect.captureCurrentView}"/>
<action execute="#{identity.tryLogin()}"/>
</event>
<event type="org.jboss.seam.security.loginSuccessful">
<action execute="#{redirect.returnToCapturedView}"/>
</event
>
セキュリティエラーでユーザーがデフォルトのエラーページを受け取らないようにするために、pages.xml
にセキュリティエラーに対応した、もう少し見栄えのするページにリダイレクトするよう設定する事が推奨されます。 セキュリティAPIの発生させる例外には主として二つのタイプがあります。
NotLoggedInException
- ユーザーがログインすることなく、特定のページ閲覧、或は特定の操作を実行しようとしたときに投げられます。
AuthorizationException
- ユーザーが既にログインしていて、当該ユーザーが許可されていないページの閲覧、或は操作を行おうとしたときに投げられます。
NotLoggedInException
の場合、ユーザーがログインできるよう、ユーザーをログインページかユーザー登録ページへ誘導する事が推奨されます。 一方、AuthorizationException
の場合にはユーザーをエラーページに誘導した方が良いでしょう。 以下の例では、この二つのセキュリティ例外によるリダイレクトを処理しているpages.xml
を示しています。
<pages>
...
<exception class="org.jboss.seam.security.NotLoggedInException">
<redirect view-id="/login.xhtml">
<message
>You must be logged in to perform this action</message>
</redirect>
</exception>
<exception class="org.jboss.seam.security.AuthorizationException">
<end-conversation/>
<redirect view-id="/security_error.xhtml">
<message
>You do not have the necessary security privileges to perform this action.</message>
</redirect>
</exception>
</pages
>
ほとんどのwebアプリケーションでは、より洗練されたログインリダイレクトを必要としますが、Seamではこの様なケースに対応できるような機能も持たせています。
認証されていないユーザーが特定のビュー(或はワイルドカードで指定された複数のビュー)の閲覧をしようとした時に、Seamがユーザーをログイン画面にリダイレクトするようにするためには (pages.xml
に) 下のように記述します。
<pages login-view-id="/login.xhtml">
<page view-id="/members/*" login-required="true"/>
...
</pages
>
これは、前項までの例外処理に比べて少々ぶっきらぼうさを抑えた処理ですが、例外処理によるリダイレクトと組み合わせて使用すると良いでしょう
ユーザーがログインした後で、再度ログインし直したい場合に自動的に最初のページ(ユーザーが入ってきたページ)に戻したいような状況を考えてみましょう。 下の様にイベントリスナーをcomponents.xml
に記述すると、ログインせずに制限されたページの閲覧をした(閲覧に失敗した)ことを記憶させておいて、ユーザーが再ログインして成功したときに、当初の要求時のページパラメータをもと当該ページにリダイレクトさせることができます。
<event type="org.jboss.seam.security.notLoggedIn">
<action execute="#{redirect.captureCurrentView}"/>
</event>
<event type="org.jboss.seam.security.postAuthenticate">
<action execute="#{redirect.returnToCapturedView}"/>
</event
>
ログインリダイレクトは対話スコープで実装されていますので、authenticate()
の中で対話を終了させてはいけません。
推奨されませんが、どうしても必要であれば、Seamは(RFC2617)のHTTPBasicあるいはHTTPDigestメソッドを認証に使用する事ができます。 これらの認証フォームを使用する場合にはcomponents.xmlで authentication-filter
が使用可能に設定されている必要があります。
<web:authentication-filter url-pattern="*.seam" auth-type="basic"/>
ベーシックな認証フィルタを使用する場合、auth-type
にbasic
を設定し、ダイジェスト認証を使用する場合には、digest
を設定します。 ダイジェスト認証を使用する場合には key
と realm
も設定する必要があります。
<web:authentication-filter url-pattern="*.seam" auth-type="digest" key="AA3JK34aSDlkj" realm="My App"/>
key
は任意の文字列です。 realm
はユーザーが認証される時にユーザーに提供される認証レルムです。
ダイジェスト認証を使用する場合はorg.jboss.seam.security.digest.DigestAuthenticator
アブストラクトクラスを拡張して、validatePassword()
メソッドによりユーザーのプレーンテキストのパスワードとダイジェスト要求を照合する必要があります。 以下はコード例です。
public boolean authenticate()
{
try
{
User user = (User) entityManager.createQuery(
"from User where username = :username")
.setParameter("username", identity.getUsername())
.getSingleResult();
return validatePassword(user.getPassword());
}
catch (NoResultException ex)
{
return false;
}
}
ここでは、より高度なセキュリティ要求に応えられる、セキュリティAPIで提供されているさらに高度な機能について紹介します。
もしSeamのセキュリティAPIが提供する簡素化されたJAAS設定を使用したくなければ、components.xml
にjaas-config-name
プロパティを設定する事によりシステムのデフォルトのJAAS設定にすることができます。 例として、JBossASをアプリケーションサーバーとして使用していて other
ポリシー(JBossASの提供する UsersRolesLoginModule
ログインモジュールを使用する)を使用したい場合には、components.xml
に以下のような記述をします。
<security:identity jaas-config-name="other"/>
これは単にSeamのセキュリティに対して設定されたJAASセキュリティポリシーに基づいて認証を行うように指示をしているだけで、Seamアプリケーションが配置されているどのコンテナに対してもユーザーの認証がなされたわけではない事に留意してください。
ID管理は、バックエンドのIDストアの種類(データベース、LDAP等)に依存しない、Seamのユーザーとロールの管理のための標準APIを提供します。 ID管理APIの中心はidentityManager
コンポーネントで、新規ユーザーの作成、変更、削除、ロールの追加、無効化、パスワードの変更、ユーザーアカウントの有効化、無効化、ユーザーの認証、ユーザーとロールの一覧等の機能のためのメソッドを提供します。
使用する前にidentityManager
に一つ以上のIdentityStore
sを設定する必要があります。 これらのコンポーネントが実際にバックにあるデータベース、LDAP、その他のセキュリティプロバイダと協調して仕事をします。
identityManager
コンポーネントに認証と許可について別々のIDストアを設定する事が可能で、例えば、LDAPディレクトリを使用してユーザーの認証をし、RDBからこのユーザーのロール情報を得て使用する事ができます。
SeamはIdentityStore
として二つのIdentityStore
の実装を提供しています。 ひとつはRDBを使用してユーザーとロールの情報を保持するJpaIdentityStore
で、デフォルトのIDストアとして設定されており、identityManager
コンポーネントの設定をすることなく使用する事ができます。 もう一つはLdapIdentityStore
で、LDAPディレクトリを使用してユーザーとロールを保持します。
identityManager
コンポーネントにはidentityStore
と roleIdentityStore
の二つの設定可能なプロパティがあります。これらの値は、IdentityStore
インタフェースを実装したSeamコンポーネントを参照するEL式である必要があります。 既に言及したように、設定がされていない場合にはデフォルトの JpaIdentityStore
が使用され、また、identityStore
のみが設定された場合にはroleIdentityStore
に同じ値が設定されたものとして処理されます。 例えば、components.xml
でLdapIdentityStore
をidentityManager
に使用するように設定した場合には、ユーザーに関するものと、ロールに関するものの両方にidentityManager
が使用されます。
<security:identity-manager identity-store="#{ldapIdentityStore}"/>
下記の例ではユーザーに関してはLdapIdentityStore
を、またロールに関する処理にはJpaIdentityStore
を使用するようidentityManager
を設定しています。
<security:identity-manager
identity-store="#{ldapIdentityStore}"
role-identity-store="#{jpaIdentityStore}"/>
以下の章ではこれらのIDストアのインプリメンテーションの詳細について説明します
このIDストアはユーザーおよびロールをリレーショナルデータベースに保存する事を可能としています。 また、データベースのスキーマ設計にはできる限り制約を作らないように設計されており、使用するテーブルの構造に大幅な自由度を認めています。 これはユーザーおよびロールのレコード用のエンティティBeanに特別のアノテーションを使用する事により実現しています。
JpaIdentityStore
は user-class
属性とrole-class
属性を設定する必要があります。 これらの属性は、それぞれユーザーとロールのレコードを保存するエンティティクラスを参照している必要があります。 下の例では、サンプルソースのSeamSpaceのcomponents.xml
の該当部分を示しています。
<security:jpa-identity-store
user-class="org.jboss.seam.example.seamspace.MemberAccount"
role-class="org.jboss.seam.example.seamspace.MemberRole"/>
先に述べたように、特定のアノテーションを使用してユーザーとロールを保持するエンティティBeanを設定します。 下の表に、これらのアノテーションとその詳細な説明について示します。
表 15.1. ユーザーエンティティアノテーション
アノテーション |
状態 |
詳細 |
---|---|---|
|
要求条件 |
このアノテーションでユーザーのusernameを保持しているフィールドあるいはメソッドをマークします。 |
|
要求条件 |
このアノテーションは、アノテートされたフィールドあるいはメソッドにユーザーのpasswordがある事を示しています。 passwordのハッシュアルゴリズムを @UserPassword(hash = "md5") Seamが標準でサポートしていないハッシュアルゴリズムを使用する場合には、 |
|
オプション |
ユーザーのファーストネームを保持しているフィールドあるいはメソッドをマークします。 |
|
オプション |
ユーザーのラストネームを保持しているフィールドあるいはメソッドをマークします。 |
|
オプション |
このアノテーションは、アノテートされたフィールドあるいはメソッドがユーザーが不活化されているか否かを示していることを示しています。 ここでアノテートされるフィールドあるいはメソッドの属性はbooleanでなければなりません。 また、もしこのアノテーションが無ければ、すべてのユーザーが不活化されていないことになります。 |
|
要求条件 |
このアノテーションは、アノテートされたフィールドあるいはメソッドにユーザーのロールがある事を示しています。 この属性については、以下により詳細に記述します。 |
表 15.2. エンティティのロールのアノテーション
アノテーション |
状態 |
詳細 |
---|---|---|
|
要求条件 |
ユーザーのロール名を保持しているフィールドあるいはメソッドをマークします。 |
|
オプション |
ロールのグループメンバーを保持しているフィールドあるいはメソッドをマークします。 |
|
オプション |
ロールが条件付きか否かを示すフィールドあるいはメソッドをマークします。 |
既に示したようにJpaIdentityStore
はデータベースのユーザーとロールに関するテーブルのスキーマのデザインができるだけ自由にできるように設計されています。 ここでは、ユーザーとロールを保持するいくつかのデータベースのスキーマについてみてゆきます。
この単純な例では、クロス参照テーブルUserRoles
を通じてmany-to-many関連でリンクされているuserとroleのテーブルで構成されています。
@Entity
public class User {
private Integer userId;
private String username;
private String passwordHash;
private Set<Role
> roles;
@Id @GeneratedValue
public Integer getUserId() { return userId; }
public void setUserId(Integer userId) { this.userId = userId; }
@UserPrincipal
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
@UserPassword(hash = "md5")
public String getPasswordHash() { return passwordHash; }
public void setPasswordHash(String passwordHash) { this.passwordHash = passwordHash; }
@UserRoles
@ManyToMany(targetEntity = Role.class)
@JoinTable(name = "UserRoles",
joinColumns = @JoinColumn(name = "UserId"),
inverseJoinColumns = @JoinColumn(name = "RoleId"))
public Set<Role
> getRoles() { return roles; }
public void setRoles(Set<Role
> roles) { this.roles = roles; }
}
@Entity public class Role { private Integer roleId; private String rolename; @Id @Generated public Integer getRoleId() { return roleId; } public void setRoleId(Integer roleId) { this.roleId = roleId; } @RoleName public String getRolename() { return rolename; } public void setRolename(String rolename) { this.rolename = rolename; } }
この例では、前の最少機能の例にすべてのオプションフィールドと、ロールにグループメンバーを許可する機能を追加しています。
@Entity
public class User {
private Integer userId;
private String username;
private String passwordHash;
private Set<Role
> roles;
private String firstname;
private String lastname;
private boolean enabled;
@Id @GeneratedValue
public Integer getUserId() { return userId; }
public void setUserId(Integer userId) { this.userId = userId; }
@UserPrincipal
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
@UserPassword(hash = "md5")
public String getPasswordHash() { return passwordHash; }
public void setPasswordHash(String passwordHash) { this.passwordHash = passwordHash; }
@UserFirstName
public String getFirstname() { return firstname; }
public void setFirstname(String firstname) { this.firstname = firstname; }
@UserLastName
public String getLastname() { return lastname; }
public void setLastname(String lastname) { this.lastname = lastname; }
@UserEnabled
public boolean isEnabled() { return enabled; }
public void setEnabled(boolean enabled) { this.enabled = enabled; }
@UserRoles
@ManyToMany(targetEntity = Role.class)
@JoinTable(name = "UserRoles",
joinColumns = @JoinColumn(name = "UserId"),
inverseJoinColumns = @JoinColumn(name = "RoleId"))
public Set<Role
> getRoles() { return roles; }
public void setRoles(Set<Role
> roles) { this.roles = roles; }
}
@Entity public class Role { private Integer roleId; private String rolename; private boolean conditional; @Id @Generated public Integer getRoleId() { return roleId; } public void setRoleId(Integer roleId) { this.roleId = roleId; } @RoleName public String getRolename() { return rolename; } public void setRolename(String rolename) { this.rolename = rolename; } @RoleConditional public boolean isConditional() { return conditional; } public void setConditional(boolean conditional) { this.conditional = conditional; } @RoleGroups @ManyToMany(targetEntity = Role.class) @JoinTable(name = "RoleGroups", joinColumns = @JoinColumn(name = "RoleId"), inverseJoinColumns = @JoinColumn(name = "GroupId")) public Set<Role > getGroups() { return groups; } public void setGroups(Set<Role > groups) { this.groups = groups; } }
IdentityManager
のIDストアの実装としてJpaIdentityStore
を使用する場合、特定の IdentityManager
メソッドを起動するとイベントが発生します。
このイベントはIdentityManager.createUser()
を呼ぶと発生します。 ユーザーエンティティがデータベースに保持される直前にエンティティインスタンスをイベントパラメータとしたイベントが発生します。 このエンティティはJpaIdentityStore
に設定した user-class
のインスタンスです。
エンティティに標準のcreateUser()
で設定されない追加的なフィールド値を設定するためにこのイベントに対応するオブザーバを利用する事ができます。
このイベントはIdentityManager.createUser()
を呼ぶことによっても発生します。 しかし、このイベントはユーザーエンティティの内容がデータベースに保持された後に発生します。 このイベントもEVENT_PRE_PERSIST_USER
と同様、イベントパラメータとしてエンティティのインスタンスを渡します。 ユーザーエンティティを参照する、コンタクトの詳細や当該ユーザー特有のデータのエンティティをデータベースに保存する必要がある場合には、このイベントを観察しておくのが有効です。
このIDストアの実装はLDAPディレクトリをユーザーレコードとして使用するよう設計されています。 この実装は、ユーザーとロールのディレクトリへの保存の方法の設定の自由度が高くなっています。 以下のセクションでは、このIIDストアの設定オプションについて説明し、いくつかのサンプル設定を示します。
以下の表にcomponents.xml
で設定できるLdapIdentityStore
の属性について示します。
表 15.3. LdapIdentityStore設定可能属性
プロパティ |
デフォルト値 |
詳細 |
---|---|---|
|
|
LDAPサーバのアドレス |
|
|
LDAPサーバが使用しているポートの番号 |
|
|
ユーザーレコードを含むコンテキストの識別名(DN) |
|
|
この値がユーザーレコードの位置指定するためにusernameの前に前置されます。 |
|
|
この値がユーザーレコードの位置指定するためにusernameの後ろに追加されます。 |
|
|
ロールレコードを含むコンテキストの識別子(DN) |
|
|
この値がロール名の前に前置され、ロールレコードを位置指定するための識別子として使用されます。 |
|
|
この値がロール名の後ろに追加され、ロールレコードを位置指定するための識別子として使用されます。 |
|
|
LDAPサーバとバインドするために使用するコンテキスト |
|
|
LDAPサーバとバインドするときに使用されるクレデンシャル(パスワード) |
|
|
ユーザーがメンバーであるロールのリストを含んでいるユーザーレコード中の属性の名前 |
|
|
このブール値はユーザーレコード中のロール属性が識別名か否かを示しています。 |
|
|
ユーザーレコードのどの属性がusernameに該当するのかを示しています。 |
|
|
ユーザーレコードのどの属性がpasswordに該当するのかを示しています。 |
|
|
ユーザーレコードのどの属性がfirst nameに該当するのかを示しています。 |
|
|
ユーザーレコードのどの属性がlast nameに該当するのかを示しています。 |
|
|
ユーザーレコードのどの属性がユーザーのフルネームに該当するのかを示しています。 |
|
|
ユーザーレコードのどの属性がユーザーが不活化されていないかを示しています。 |
|
|
ロールレコードのどの属性がロール名に該当するのかを示しています。 |
|
|
ディレクトリ中でオブジェクトのクラスを決定している属性を示しています。 |
|
|
新規のロールレコードの作成のためのオブジェクトクラスの配列 |
|
|
新規のユーザーレコード作成のためのオブジェクトクラスの配列 |
下の設定例では、擬似ホスト directory.mycompany.com
上で動作しているLDAPディレクトリに対応するLdapIdentityStore
の設定を示しています。 ユーザーは、このディレクトリ配下にou=Person,dc=mycompany,dc=com
というコンテキストで保持され、usernameに対応するuid
属性により識別されます。 ロールはロール用のコンテキストou=Roles,dc=mycompany,dc=com
に保持され、ユーザーのエントリからroles
属性を通じて参照されます。 ロールのエントリはロールの名前に対応するロールの一般名(cn
属性)により識別されます。 この例では、ユーザーはenabled
属性をfalseにする事により、使用不可にする事ができます。
<security:ldap-identity-store
server-address="directory.mycompany.com"
bind-DN="cn=Manager,dc=mycompany,dc=com"
bind-credentials="secret"
user-DN-prefix="uid="
user-DN-suffix=",ou=Person,dc=mycompany,dc=com"
role-DN-prefix="cn="
role-DN-suffix=",ou=Roles,dc=mycompany,dc=com"
user-context-DN="ou=Person,dc=mycompany,dc=com"
role-context-DN="ou=Roles,dc=mycompany,dc=com"
user-role-attribute="roles"
role-name-attribute="cn"
user-object-classes="person,uidObject"
enabled-attribute="enabled"
/>
Seamによって標準でサポートされていないセキュリティプロバイダを使った認証やID管理を行う場合には、org.jboss.seam.security.management.IdentityStore
を実装する、一つのクラスの実装を記述するだけ実現できます。
IdentityStore
の実装するメソッドの詳細については該当するJavaDocを参照してください。
SeamアプリケーションでID管理機能を使っている場合には、認証コンポーネント(認証の項参照)による認証を行う必要はありません。components.xml
のidentity
設定からauthenticator-method
を削除してください。 これで、特別な設定をすることなくSeamLoginModule
はデフォルトのIdentityManager
を使用してユーザーの認証を行うようになります。
IdentityManager
にアクセスできるようにするには、下のようにSeamコンポーネントにインジェクトします。
@In IdentityManager identityManager;
あるいは、静的なinstance()
メソッド経由でアクセスします。
IdentityManager identityManager = IdentityManager.instance();
下のテーブルにIdentityManager
のAPIのメソッドを示します。
表 15.4. ID管理のAPI
メソッド |
戻り値 |
詳細 |
---|---|---|
|
|
指定されたusernameとpasswordで新規ユーザーのアカウントを作成します。 もし作成が成功すれば |
|
|
指定された名前のユーザーを削除します。 もし成功すれば |
|
|
指定された名前で新規のロールを作成します。 もし作成が成功すれば |
|
|
指定された名前のロールを削除します。 もし作成が成功すれば |
|
|
指定された名前のユーザーアカウントを活性化します。 活性化されていないアカウントは認証の対象とはなりません。もし成功すれば |
|
|
指定された名前のユーザーアカウントを不活化します。 もし成功すれば |
|
|
指定された名前のユーザーアカウントのpasswordの変更をします。 もし成功すれば |
|
|
もし、指定されたユーザーのアカウントが活性化されていれば |
|
|
特定のロールをユーザーやロールに権限を付与します。 ロールは既に存在していることが必要です。 ロールの付与が成功した場合には |
|
|
特定のユーザーあるいはロールから指定したロールを取り消します。 ユーザーが当該のロールのメンバーであり、かつ取り消しが成功した場合には |
|
|
もし、当該のユーザーが存在すれば |
|
|
ABC順にソートされたすべてのユーザー名の一覧を返します。 |
|
|
指定されたパラメータでフィルタしたユーザー名のリストをABC順にソートして返します |
|
|
すべてのロール名の一覧を返します |
|
|
指定されたユーザーに明示的に認められたロール名の一覧を返します |
|
|
指定されたユーザー名に対して暗示的に付与されているすべてのロール名のリストを返します。 暗示的に付与されているロールとは、ユーザーに直接付与されているロールではなく、ユーザーが所属するロールに対して付与されているロールを言います。 例えば、 |
|
|
設定されたIDストアを使ってusernameとpasswordを認証します。 認証が成功すれば |
|
|
特定のロールを指定したグループのメンバーに追加します。 操作が成功した場合にtrueを返します。 |
|
|
指定されたロールを指定されたグループから削除します。 もし成功すればtrueを返します。 |
|
|
すべてのロール名のリスト |
ID管理APIを使うためには、ユーザーはそのメソッドを呼び出す適切な権限を持っている必要があります。 以下の表にIdentityManager
にある個々のメソッドの起動に必要な権限の一覧を示します。 権限はリテラル文字列で指定します。
表 15.5. ID管理 セキュリティパーミッション
メソッド |
パーミッションの対象 |
パーミッションのアクション |
---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
下の例では、admin
ロールのメンバーすべてが、すべてのID管理関連のメソッドへのアクセス権を付与されているセキュリティルールを示しています。
rule ManageUsers no-loop activation-group "permissions" when check: PermissionCheck(name == "seam.user", granted == false) Role(name == "admin") then check.grant(); end rule ManageRoles no-loop activation-group "permissions" when check: PermissionCheck(name == "seam.role", granted == false) Role(name == "admin") then check.grant(); end
セキュリティAPIはセキュリティ関連イベントに対応するいくつかのデフォルトのフェースメッセージを発生します。 以下の表には、リソースファイルmessage.properties
で、これらを上書きするためのメッセージキーを一覧にしています。 もし、これらのメッセージを出さないようにするのであれば、リソースファイルで対応するキーの値をブランクにしてください。
表 15.6. セキュリティメッセージキー
メッセージキー |
詳細 |
---|---|
|
セキュリティAPIを通して、無事ログインできたときに生成されます。 |
|
ユーザーネーム、パスワードの組み合わせ、或は何らかの認証のエラーにより、ユーザーがログインに失敗したときに生成されます。 |
|
ユーザーが認証されずにセキュリティチェックが必要な操作、あるいはページへのアクセスを試みたときに生成されます。 |
|
このメッセージは既に認証されたユーザーが再度ログインを試みた時に生成されます。 |
SeamのセキュリティAPIは、コンポーネント、コンポーネントのメソッド、それにページに対して多くの認可機能を提供します。 ここでは、それぞれの機能について説明します。 ここで説明するような高度なセキュリティ機能(ルールベースの認可のような)を使用する場合にはcomponents.xml
に前述のような設定を記述しておかなければならない、ということに留意してください。
Seamのセキュリティは必要なセキュリティ権限を持たないユーザーの操作を行わせないように、ロールとパーミッションによりユーザーの操作を制限する事を前提として設計されています。 SeamセキュリティAPIの提供する認可メカニズムは、ロールとパーミッションによるユーザー管理の概念に基づいて作られており、複数のアプリケーションリソース保護の方法を提供する拡張可能なフレームワークを提供しています。
ロールとは、アプリケーションの特定の操作を実施する特権を付与されてるユーザーのグループあるいはタイプを言い、"admin"、 "user"、 "customer"等の名前により構成されています。 これらのロールはユーザーに対して(あるいは場合により他のロールに対し)付与され、特定のアプリケーションの操作に対する特権を持つユーザーの論理的なグループを作成する事を容易にしています。
パーミッションとは、一つの特定の操作を実行するための特権(時として1回限りの)を言います。 パーミッションのみを使ってアプリケーションを組むことも可能ですが、ユーザーのグループに対して特定の特権を付与する事により、セキュリティ管理をより容易にすることができます。 これらはロールよりも、構造上若干複雑で、 ”対象(target)”、”操作(action)” と”受益者(receipient)”の三つの要素から構成されます。 パーミッションの対象は特定の受益者(ユーザー)により特定の操作が行われるオブジェクト(あるいは、任意の名前、クラス)です。 例として、ユーザーBobは顧客オブジェクトを削除するパーミッションを持つ、場合を考えてみると、 パーミッションの対象は「顧客」、パーミッションの操作は「削除」、そして受益者はBobという事になります。
このドキュメント中では権限は、実際には常に必要な受益者を省略してtarget:action
という形式で表示されています。
それでは、もっとも簡単な形式の認可、コンポーネントのセキュリティについて@Restrict
アノテーションから見てゆきましょう。
@Restrict
アノテーションを使うとEL式をサポートしていることもあり、コンポーネントのメソッドに対して強力かつフレキシブルなセキュリティを付与する事ができますが、コンパイル時の安全性等から、タイプセーフな同様の方法(後述)が推奨されます。
@Restrict
アノテーションにより、Seamのコンポーネントにはクラスあるいはメソッドレベルでのセキュリティを付与する事ができます。 もし、クラスとその中のメソッドの両方に@Restrict
アノテーションがあった場合には、メソッドレベルの制限が優先され、クラスレベルの制限は結果として適用されません。 もし、メソッドの起動がセキュリティチェックで失敗した場合には、Identity.checkRestriction()
単位で例外が発生します。 コンポーネントレベルでの@Restrict
アノテーションは、そのコンポーネントのすべてのメソッドに@Restrict
をアノテートしたのと同じことになります。
空の@Restrict
はcomponent:methodName
を意味します。 下のようなコンポーネントの例を見てみましょう。
@Name("account")
public class AccountAction {
@Restrict public void delete() {
...
}
}
この例では、delete()
を呼び出すためにはaccount:delete
という権限が必要な事を暗黙的に示しています。 同様の内容は@Restrict("#{s:hasPermission('account','delete')}")
と記述する事もできます。他の例についても見てゆきましょう。
@Restrict @Name("account")
public class AccountAction {
public void insert() {
...
}
@Restrict("#{s:hasRole('admin')}")
public void delete() {
...
}
}
ここでは、コンポーネントクラスに@Restrict
とアノテーションが付記されています。これは、`Restrictがオーバーライドされない限り、パーミッションのチェックが暗示的に要求されることを示しています。この例の場合、insert()
はaccount:insert
のパーミッションを必要とし、delete()
はユーザーがadmin
ロールに属していることが必要な事を示しています。
先に進む前に、上の例で見た #{s:hasRole()}
式について見てみましょう。 s:hasRole
も s:hasPermission
もEL式であり、 Identity
クラスの同様の名前のメソッドに対応します。 これらセキュリティAPIのすべてについてEL式の中で使う事ができます。
EL式とすることで、@Restrict
アノテーションは、Seamコンテキスト中のどのようなオブジェクトの値でも参照することができるようになります。 これは、特定のオブジェクトのインスタンスをチェックしてパーミッションを決定する場合に非常に有効な方法です。下の例を見てみましょう。
@Name("account")
public class AccountAction {
@In Account selectedAccount;
@Restrict("#{s:hasPermission(selectedAccount,'modify')}")
public void modify() {
selectedAccount.modify();
}
}
ここで興味深いのは、hasPermission()
というファンクション中でselectedAccout
を参照している事です。 この変数の値はSeamのコンテキスト中で検索され、Identity
のhasPermission()
に渡され、この例の場合、特定のAccount
のオブジェクトに対する変更許可を持っているかを決定しています。
時として、@Restrict
アノテーションを使わずに、コードでセキュリティチェックを行いたい場合があるかもしれません。この様な場合には、下のようにIdentity.checkRestriction()
を使って、セキュリティ式を評価することができます。
public void deleteCustomer() {
Identity.instance().checkRestriction("#{s:hasPermission(selectedCustomer,'delete')}");
}
もし式がtrue
と評価されなかった場合には、
ユーザーがログインしていなかったのであれば、NotLoggedInExceptionが投げられ、
ユーザーがログインしていた場合には、AuthorizationExceptionが投げられます。
また、下のようにJavaコードから直接hasRole()
やhasPermission()
メソッドを呼ぶこともできます。
if (!Identity.instance().hasRole("admin"))
throw new AuthorizationException("Must be admin to perform this action");
if (!Identity.instance().hasPermission("customer", "create"))
throw new AuthorizationException("You may not create new customers");
適切なユーザーインタフェースのデザインの一つとして、ユーザーが使用する権限を有しないオプションの表示をしないようにすることがあります。 Seamのセキュリティはユーザーの権限に応じて、コンポーネントのセキュリティで使用したのと同様にEL式を使用する事により1)ページ単位 2)個々のコントロール単位 で描画を管理する事ができます。
インタフェースのセキュリティの例について見てゆきましょう。 まず最初に、ログインしていないユーザーの時だけ表示されるログインフォームについて考えてみましょう。 identity.isLoggedIn()
属性を使えば下のように記述できます。
<h:form class="loginForm" rendered="#{not identity.loggedIn}"
>
もしユーザーがログインしていなければ、ログインフォームが表示されます(実に単純ですね)。 次に、manager
ロールを持っている人達だけがアクセス可能なメニューが必要だと仮定しましょう。 このような場合の一つの方法として、下に例を示したあります。
<h:outputLink action="#{reports.listManagerReports}" rendered="#{s:hasRole('manager')}">
Manager Reports
</h:outputLink
>
これも、たいへんシンプルで、ユーザーがmanager
ロールを持っていなければ、outputLinkは描画されません。rendered
属性は一般に制御そのものに使われたり、<s:div>
や <s:span>
の中で制御の目的に使われます。
次にもう少し複雑な例: h:dataTable
の制御用のアクションリンクの表示非表示をユーザーの権限により制御する事を考えます。 EL式s:hasPermission
により、ユーザーが必要な権限を持っているか否かを決定するために必要なオブジェクトをパラメータとして渡すことができます。 以下に、セキュリティを向上させたリンクを持たせたh:dataTable
の例を示します。
<h:dataTable value="#{clients}" var="cl">
<h:column>
<f:facet name="header"
>Name</f:facet>
#{cl.name}
</h:column>
<h:column>
<f:facet name="header"
>City</f:facet>
#{cl.city}
</h:column>
<h:column>
<f:facet name="header"
>Action</f:facet>
<s:link value="Modify Client" action="#{clientAction.modify}"
rendered="#{s:hasPermission(cl,'modify')"/>
<s:link value="Delete Client" action="#{clientAction.delete}"
rendered="#{s:hasPermission(cl,'delete')"/>
</h:column>
</h:dataTable
>
ページレベルのセキュリティはアプリケーションがpages.xml
を使用していることが必要ですが、設定自身は非常に簡単です。単に保護したいページの page
エレメントに<restrict/>
を追加するだけです。 明示的にrestrict
で制限をしない場合、 当該ページに対してGET要求でアクセスが試みられると /viewId.xhtml:render
がチェックされ、またJSFポストバック(フォームのサブミッション)に対しては /viewId.xhtml:restore
権限がチェックされます。 これ以外の場合には、指定した制限について通常のセキュリティ式評価が行われます。 以下にいくつかの例を示します。
<page view-id="/settings.xhtml">
<restrict/>
</page
>
このページは暗黙的に、GET要求に対して/settings.xhtml:render
権限を要求し、フェース要求に対しては/settings.xhtml:restore
権限を要求しています。
<page view-id="/reports.xhtml">
<restrict
>#{s:hasRole('admin')}</restrict>
</page
>
このページに対するfacesあるいはnon-facesな要求はユーザーがadmin
ロールのメンバーである事が必要です。
Seamのセキュリティは、エンティティ単位でのread,insert,updateおよびdelete操作に対してのセキュリティ制約をかけることを可能にしています。
エンティティクラスのアクション全部に対してセキュリティをかけたいのであれば、下のようにクラスに@Restrict
アノテーションを付記します。
@Entity
@Name("customer")
@Restrict
public class Customer {
...
}
もし、@Restrict
が評価式無しで付記されていれば、デフォルトとしてentity:action
のパーミッションがチェックされます。 ここで、パーミッションの対象はエンティティのインスタンスで、action
は read
, insert
, update
あるいは delete
のいずれかです。
また、下のようにエンティティのライフサイクルに@Restrict
アノテーションを付記することにより、特定の操作だけに制約を課すことができます。
@PostLoad
- エンティティのインスタンスがデータベースからロードされた後に呼び出されます。このメソッドはread
パーミッションの設定に使用してください。
@PrePersist
- エンティティの新規のインスタンスが (データベースに)挿入される前に呼び出されます。 このメソッドはinsert
パーミッションの設定に使用してください。
@PreUpdate
- エンティティが更新される前に呼び出されます。 このメソッドはupdate
パーミッションの設定に使用してください。
@PreRemove
- エンティティが削除される前に呼び出されます。 このメソッドはdelete
パーミッションの設定に使用してください。
ここではinsert
操作に対してのセキュリティチェックをするためのエンティティの設定方法を示しています。 ここで注意していただきたいのは、メソッドの内容はセキュリティと関係なく、アノテーションの仕方が重要な事です。
@PrePersist @Restrict
public void prePersist() {}
/META-INF/orm.xml
の使用/META-INF/orm.xml
にコールバックメソッドを指定する事もできます:
<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings xmlns="http://java.sun.com/xml/ns/persistence/orm"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence/orm http://java.sun.com/xml/ns/persistence/orm_1_0.xsd"
version="1.0">
<entity class="Customer">
<pre-persist method-name="prePersist" />
</entity>
</entity-mappings
>
もちろん この場合もCustomer
のprePersist()
メソッドに@Restrict
アノテーションは必要です。
これは、認証されているユーザーが新規にMemberBlog
レコードを追加する事ができるか否かをチェックする、エンティティ権限ルールの例(サンプルソースのseamspaceのコードから)です。 セキュリティチェックの対象となるエンティティは自動的にワーキングメモリー(この場合、MemberBlog
)に挿入されます。
rule InsertMemberBlog no-loop activation-group "permissions" when principal: Principal() memberBlog: MemberBlog(member : member -> (member.getUsername().equals(principal.getName()))) check: PermissionCheck(target == memberBlog, action == "insert", granted == false) then check.grant(); end;
このルールはPrincipal
ファクトで示される現在の認証ユーザーがブログのエントリを作成したメンバーと同じ名前であればmemberBlog:insert
パーミッションを付与します。 例示したコード中にある、構造体 "principal: Principal()
" は認証の過程で挿入された ワーキングメモリ中のPrincipal
オブジェクトへの変数結合で、変数principal
と命名されています。 変数結合にする事により、他の場所で値が参照可能となり、下のようにPrincipal
名とメンバーのユーザー名を比較する事ができます。 詳細は、JBoss Rules ドキュメントを参照してください。
最後に、JPAプロバイダをSeamセキュリティと統合するために、リスナークラスをインストールします。
EJB3エンティティBeanのセキュリティチェックはEntityListener
により行われ、下記のようなMETA-INF/orm.xml
の設定でリスナーをインストールすることができます。
<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings xmlns="http://java.sun.com/xml/ns/persistence/orm"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence/orm http://java.sun.com/xml/ns/persistence/orm_1_0.xsd"
version="1.0">
<persistence-unit-metadata>
<persistence-unit-defaults>
<entity-listeners>
<entity-listener class="org.jboss.seam.security.EntitySecurityListener"/>
</entity-listeners>
</persistence-unit-defaults>
</persistence-unit-metadata>
</entity-mappings
>
Seamは@Restrict
に代わるアノテーションをいくつか持っており、これらを使う事により@Restrict
とは異なりEL式の評価を行わないので、コンパイル時の安全性を提供します。
Seamには標準のCRUD操作に関するパーミッション用のアノテーションが提供されていますが、独自のアノテーションを作成する事もできます。 以下はorg.jboss.seam.annotations.security
パッケージで配布されているアノテーションです。
@Insert
@Read
@Update
@Delete
これらのアノテーションを使うには、単にセキュリティチェックを行いたいメソッドやパラメータをアノテートするだけです。 メソッドがアノテートされた場合には、アクセス権のチェックの対象となるターゲットクラスも指定する必要があります。 以下の例を見てください。
@Insert(Customer.class) public void createCustomer() { ... }
この例ではユーザーが新規のCustomer
オブジェクトを作成する権限があるか否かパーミッションチェックを行います。 パーミッションチェックの対象はCustomer.class
(java.lang.Class
インスタンスそのもの)で、アクションはアノテーション名の小文字に変換されたもの、ここではinsert
、となります。
同様にコンポーネントのメソッドのパラメータに対してもアノテートする事ができます。 これを行った場合には、パラメータの値がアクセス権チェックの対象ですから、アクセス権のターゲットを指定する必要はありません。
public void updateCustomer(@Update Customer customer) { ... }
下のように、独自のセキュリティアノテーションを作る場合には、単に@PermissionCheck
とアノテートするだけです。
@Target({METHOD, PARAMETER})
@Documented
@Retention(RUNTIME)
@Inherited
@PermissionCheck
public @interface Promote {
Class value() default void.class;
}
もしデフォルトのアクセス権アクション名(アノテーション名の小文字版)を他の値で上書きする必要があれば、@PermissionCheck
アノテーション内にその値を指定することができます。
@PermissionCheck("upgrade")
タイプセーフなパーミッションのアノテーションをサポートするのに加えて、Seamセキュリティはタイプセーフなロールのアノテーションを提供し、認証されているユーザーがどのロールのメンバーに属しているかに基づいてコンポーネントのメソッドへのアクセスを制限する事を可能にしています。 Seamはこのようなアノテーションとして、admin
ロール(アプリケーションにこのロールが設定されていれば)のメンバーに属しているユーザーのみにアクセスを制限するorg.jboss.seam.annotations.security.Admin
を提供しています。 独自のロールのアノテーションを作成するためには、下の例のようにorg.jboss.seam.annotations.security.RoleCheck
でメタアノテートします。
@Target({METHOD}) @Documented @Retention(RUNTIME) @Inherited @RoleCheck public @interface User { }
上の例に示されているような@User
アノテーションを持つメソッドは、呼ばれる度に自動的にインタセプトされ対応するロール名(アノテーション名を小文字に書き換えた名前ーこの場合はuser
)のメンバーにユーザーが含まれるかチェックされます。
Seamセキュリティはアプリケーションに対するパーミッションの決定に対して拡張可能なフレームワークを提供します。 下のクラスダイアグラム図にはSeamの提供するパーミッションフレームワークの主要コンポーネントについて示しています。
関連するクラスについての詳細を以下のセクションに示します
実際には、これは個々のオブジェクトのアクセス権を決定するメソッドを提供するインタフェースです。 Seamは以下のPermissionResolver
を内蔵しています。 それぞれの詳細はこの章の後半で説明します。
RuleBasedPermissionResolver
- このパーミッションリゾルバーはDroolsを使ってルールベースのパーミッションチェックを行います
PersistentPermissionResolver
- このパーミッションリゾルバーはデータベース等にパーミッションオブジェクトを保持します。
独自のパーミッションリゾルバを作成するためには、下の表にあるPermissionResolver
インタフェースに定義されている二つのメソッドを実装します。 独自のPermissionResolver
実装をSeamプロジェクトにデプロイする事により、プロジェクトがデプロイされる時(立ち上がり時)に自動的にスキャンされResolverChain
に組み込まれます。
表 15.7. パーミッションリゾルバーインタフェース
戻り値の型 |
メソッド |
詳細 |
---|---|---|
|
|
このメソッドは |
|
|
This method should remove any objects from the specified set, that would return |
As they are cached in the user's session, any custom PermissionResolver
implementations must adhere to a couple of restrictions. Firstly, they may not contain any state that is finer-grained than session scope (and the scope of the component itself should either be application or session). Secondly, they must not use dependency injection as they may be accessed from multiple threads simultaneously. In fact, for performance reasons it is recommended that they are annotated with @BypassInterceptors
to bypass Seam's interceptor stack altogether.
ResolverChain
はPermissionResolver
sを順番に並べたリストを持っており、このリストに従い、特定のオブジェクトクラス、あるいはパーミッション対象についてのパーミッションを解決します。
The default ResolverChain
consists of all permission resolvers discovered during application deployment. The org.jboss.seam.security.defaultResolverChainCreated
event is raised (and the ResolverChain
instance passed as an event parameter) when the default ResolverChain
is created. This allows additional resolvers that for some reason were not discovered during deployment to be added, or for resolvers that are in the chain to be re-ordered or removed.
下のシークエンス図にパーミッションチェック時のパーミッションフレームワーク内のコンポーネント相互の作用を示します。 パーミッションチェックは、例えば、セキュリティインタセプタ、EL式s:hasPermission
、あるいはAPIIdentity.checkPermission
を呼び出す等、複数の方法により呼び出されます。
1 パーミッションチェックはコードあるいはEL式評価によりIdentity.hasPermission()
が呼び出されることにより実行されます。
1.1. Identity
は解決対象のパーミッションをPermissionMapper.resolvePermission()
を渡して呼び出します。
1.1.1. PermissionMapper
はクラスによりキー付けされたResolverChain
インスタンスのMap
を維持しており、パーミッションの対象オブジェクトに対応して適切なResolverChain
を選択するように管理しています。 適切なResolverChain
を見つければ、ResolverChain.getResolvers()
を呼び出し、管理しているPermissionResolver
sを読み込みます。
1.1.2. ResolverChain
中の個々のPermissionResolver
についてPermissionMapper
はパーミッションチェックの対象をパラメータとして渡してhasPermission()
メソッドを呼び出します。 いずれかのPermissionResolver
s がtrue
を返せば、パーミッションチェックが成功したと見なしPermissionMapper
がIdentity
に対してtrue
を返します。 いずれのPermissionResolver
sもtrue
を返さなければ、 パーミッションチェックは失敗したことになります。
Seamに内蔵されているパーミッションリゾルバーの一つRuleBasedPermissionResolver
は、Drools(JBoss Rules)によるセキュリティルールに基づいたパーミッションの評価を受け付けます。 ルールエンジンを使う事の利点は; 1)ユーザーパーミッションの評価に使用されるビジネスロジックを一か所にまとめることができる 2)スピードーDroolは効率の良いアルゴリズムを使用し、多くの条件の元に多くの複雑なルールを評価することが可能になっています。
Seamセキュリティの提供するルールベースのアクセス権を使用する場合には、Droolに必要な下記のjarファイルをディストリビューション含める必要があります。
drools-compiler.jar
drools-core.jar
janino.jar
antlr-runtime.jar
mvel14.jar
RuleBasedPermissionResolver
を設定するためには、components.xml
にDroolのルールベースが設定されている必要があります。 このルールベースは下の例のように、デフォルトでsecurityRules
と命名されていることを仮定しています。
<components xmlns="http://jboss.com/products/seam/components"
xmlns:core="http://jboss.com/products/seam/core"
xmlns:security="http://jboss.com/products/seam/security"
xmlns:drools="http://jboss.com/products/seam/drools"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation=
"http://jboss.com/products/seam/core http://jboss.com/products/seam/core-2.1.xsd
http://jboss.com/products/seam/components http://jboss.com/products/seam/components-2.1.xsd
http://jboss.com/products/seam/drools http://jboss.com/products/seam/drools-2.1.xsd"
http://jboss.com/products/seam/security http://jboss.com/products/seam/security-2.1.xsd">
<drools:rule-base name="securityRules">
<drools:rule-files>
<value
>/META-INF/security.drl</value>
</drools:rule-files>
</drools:rule-base>
</components
>
デフォルトのルールベースの名前はRuleBasedPermissionResolver
のsecurity-rules
属性で上書きする事ができます。
<security:rule-based-permission-resolver security-rules="#{prodSecurityRules}"/>
RuleBase
コンポーネントを設定したら、次にセキュリティルールの記述をします。
セキュリティルールを作成するためには、まずアプリケーションのjarファイルの/META-INF
ディレクトリ下に新規のルールファイルを作ります。 通常このファイルはsecurity.drl
のように命名されますが、components.xml
に別途指定しておけば、どのようにでも命名する事ができます。
セキュリティルールの内容は? この段階ではDroolsのドキュメントから適当に拝借してくるのが良いかもしれませんが、ここでは非常に単純な例から始めてみる事にしましょう
package MyApplicationPermissions; import org.jboss.seam.security.permission.PermissionCheck; import org.jboss.seam.security.Role; rule CanUserDeleteCustomers when c: PermissionCheck(target == "customer", action == "delete") Role(name == "admin") then c.grant(); end
では、一つづつ見てゆきましょう。 最初はパッケージ宣言です。 Droolのパッケージはルールの集まりで、ルールベースの範囲外とは何ら関わりが無いので、パッケージの名前は任意で構いません。
つぎに、PermissionCheck
クラスとRole
クラスに関するいくつかのインポート文があります。 これらのインポート文は、これから使うルールでこれらのクラスを参照する事をルールエンジンに対して伝えています。
そして、ルールの記述コード。それぞれのルールは、ルールごとにユニークな名前が与えられている必要があります (通常は、ルールの目的をルールの名前にします。) この例の場合、CanUserDeleteCustomers
がルールの名前で、読んで字の如く、顧客レコードの削除をできるかできないかのチェックに使用します。
ルールの記述が二つの部分から成っている事がわかります。ルールは左部分 (LHS) と右部分 (RHS) として知られている部分から成り立っています。LHSはルールの条件部分 (即ち、ルールが実行されるために満たさなければならない条件のリスト) を規定しています。 LHSはwhen
で表されるセクションにあり、また、RHSはLHSが満たされた場合に実行されるアクション、あるいは結果を記述しています。 RHSはthen
以降の部分に記述します。 また、ルールの最後はend
で終了します。
ルールについてLHSを見ると、二つの条件がある事が分かります。 まず、最初の条件を見てみましょう。
c: PermissionCheck(target == "customer", action == "delete")
この条件は、簡単な英語で「ワーキングメモリ中に、target
属性として"customer"を持ち、target
属性として"delete"を持つPermissionCheck
オブジェクトが存在しなければならない」と示しています。
ワーキングメモリって何? Droolsの技術用語で、ルールエンジンがパーミッションチェックをするために必要なコンテキスト情報を保持しているセッションスコープのオブジェクトの事を「ワーキングメモリ」と呼びます。 hasPermission()
メソッドが呼ばれる都度、仮のPermissionCheck
オブジェクト、あるいはファクト(Fact)がワーキングメモリに挿入されます。 このPermissionCheck
は今からチェックされるパーミッションに対応しており、例えばhasPermission("account", "create")
を呼び出すと、target
属性が "account"でaction
属性が"create"であるPermissionCheck
オブジェクトがワーキングメモリに挿入され、パーミッションチェックが終了するまで存在します。
PermissionCheck
ファクトの他に、認証されたユーザーが所属するロールのorg.jboss.seam.security.Role
ファクトがあります。 これらの Role
ファクトはパーミッションチェックの開始の都度ユーザーの認証されたロールと同期されます。 従って、パーミッションチェックに使用されたワーキングメモリ中のRole
オブジェクトは、もし認証されたユーザーがそのロールに所属していなければ、次回のパーミッションチェックの前に削除されます。 ワーキングメモリにはPermissionCheck
と Role
ファクトの他に認証の過程で作成されたjava.security.Principal
オブジェクトが保持されています。
これ以外にパラメータとしてオブジェクトを渡しRuleBasedPermissionResolver.instance().getSecurityContext().insert()
を呼び出すことにより、追加でワーキングメモリ中に長期に生存するファクトを挿入する事ができます。 例外として、先に説明したようにRole
オブジェクトはパーミッションチェックの都度同期されるために、はワーキングメモリ中に長期に生存するファクトとする事はできません。
先の例に戻り、LHSがc:
で始まっていることに気がつくと思います。 これは、変数結合を表しており、条件のマッチングに利用されるオブジェクトへの参照を意味しています(この例の場合はPermissionCheck
)。 LHSの2行目には下の記述があります。
Role(name == "admin")
この条件はワーキングメモリ中に"admin"というname
のRole
オブジェクトが存在しなければならない事を示しています。 先述したように、ユーザーのロールはパーミッションチェックの開始の都度ワーキングメモリに挿入されますので、この条件は結果として「admin
ロールに所属するユーザーでcustomer:delete
の許可を求めているのであれば、これを認めます」という事を示しています。
ルールが適用されると、何が起こるのでしょうか? 次にルールのRHS側を見てみましょう。
c.grant()
RHSはJavaコードから成っており、この例の場合はc
というオブジェクト (既に述べたように、PermissionCheck
オブジェクトへの変数結合) のgrant()
メソッドが起動されます。PermissionCheck
オブジェクトのname
とaction
プロパティ以外にfalse
に初期設定されたgranted
プロパティが存在します。PermissionCheck
のgrant()
を呼ぶことにより、granted
プロパティはtrue
にセットされ、パーミッションのチェックが成功し、ユーザーはパーミッションで決められたアクションについて実行することができるようになります。
ここまで文字列型のパーミッションターゲットのチェックについて見てきました。 しかし、もっと複雑なパーミッションターゲットのセキュリティルールを記述することも可能です。 例えば、ユーザーがブログにコメントを作成する事を可能にするセキュリティルールを記述する場合を考えてみましょう。 これは以下のように、パーミッションチェックの対象がMemberBlog
インスタンスで、現在の認証されたユーザーがuser
ロールのメンバーであることが必要である、と表現されます。
rule CanCreateBlogComment no-loop activation-group "permissions" when blog: MemberBlog() check: PermissionCheck(target == blog, action == "create", granted == false) Role(name == "user") then check.grant(); end
ワイルドカードを使ってパーミッションチェックを設定することも可能で、これはあるパーミッションに対してすべての操作を許可します。下のようにルールのPermissionCheck
のaction
制約を省略することにより、実装できます。
rule CanDoAnythingToCustomersIfYouAreAnAdmin when c: PermissionCheck(target == "customer") Role(name == "admin") then c.grant(); end;
上記のルールでは、admin
ロールを持つユーザーは、どのcustomer
に対しても、任意の操作が可能なパーミッションチェックになっています。
Seamに内蔵されているパーミッションリゾルバーにはこれ以外にPersistentPermissionResolver
があり、これはリレーショナルデータベースのような永続的保存場所からパーミッションを読み込むことが可能で、ACLスタイルのインスタンスベースのセキュリティを提供しており、個別のユーザーとロールに対して特定のパーミッションを指定する事ができます。 また、任意の名前のパーミッションターゲットを指定して保存する事が可能です。
使用するためにはcomponents.xml
に、有効なPersistentPermissionResolver
を設定したPermissionStore
を記述する必要があります。 設定していない場合、デフォルトのパーミッションストアJpaIdentityStore
の使用を試みます。 デフォルト以外のパーミッションストアを使用する場合にはpermission-store
属性を下のように記述します。
<security:persistent-permission-resolver permission-store="#{myCustomPermissionStore}"/>
PersistentPermissionResolver
は、パーミッションを保存しているバックエンドの保存場所との接続のためにパーミッションストアを必要とします。 Seamは標準で一つのPermissionStore
実装JpaPermissionStore
を提供しており、リレーショナルデータベースにパーミッションを保存します。 下記のメソッドを定義しているPermissionStore
インタフェースを実装することにより、独自のパーミッションストアを作成する事も可能です。
表 15.8. パーミッションストアのインタフェース
戻り値の型 |
メソッド |
詳細 |
---|---|---|
|
|
このメソッドは対象のオブジェクトに付与されているすべての権限を表す |
|
|
このメソッドは対象のオブジェクトに付与されている特定のアクションに対するすべての権限を表す |
|
|
このメソッドは対象の一連のオブジェクトに付与されている特定のアクションに対するすべての権限を表す |
|
|
このメソッドは特定の |
|
|
このメソッドは指定された |
|
|
このメソッドは指定された |
|
|
このメソッドは指定されたリストにあるすべての |
|
|
このメソッドは指定された対象オブジェクトクラスに対して可能なアクション(文字列型)のリストを返します。 特定のクラスのパーミッションを付与するためのユーザーインタフェースを作成するためにパーミッション管理と共に使用されます。 |
これはデフォルトの(また、Seamが提供する唯一の)PermissionStore
の実装で、パーミッションの保存にリレーショナルデータベースを使用しています。 使用するためにはユーザーとロールのパーミッションの保存に係る一つないし二つのエンティティクラスの設定が必要になります。 これらのエンティティクラスは保存されているレコードとエンティティの属性がパーミッションのどれに対応するのかを設定するための特別なセキュリティに関するアノテーションでアノテートされている必要があります。
もしユーザーパーミッションとロールパーミッションに同一のエンティティ(一つのDBテーブル)を使うのであれば、user-permission-class
属性を設定します。 ユーザーパーミッションとロールパーミッションを別々のテーブルに保持するのであれば、user-permission-class
属性に加えてrole-permission-class
属性を設定する必要があります。
例えば、ユーザーとロールのパーミッションを一つのエンティティクラスに保存するよう設定する場合は次のようになります。
<security:jpa-permission-store user-permission-class="com.acme.model.AccountPermission"/>
ユーザーパーミッションとロールパーミッションを別のエンティティクラスに保存する場合の設定は次のようになります。
<security:jpa-permission-store user-permission-class="com.acme.model.UserPermission"
role-permission-class="com.acme.model.RolePermission"/>
先述のように、ユーザーとロールのパーミッションを保持するエンティティクラスはorg.jboss.seam.annotations.security.permission
パッケージにある特別なアノテーションを設定されている必要があります。 下の表にこれらのアノテーションと、その使用方法の説明を示します。
表 15.9. エンティティ パーミッション アノテーション
アノテーション |
ターゲット |
詳細 |
---|---|---|
|
|
このアノテーションはパーミッションの対象を含んでいるエンティティの属性を示します。 この属性は |
|
|
このアノテーションはパーミッションアクションを含んでいるエンティティの属性を示します。 この属性は |
|
|
このアノテーションはパーミッションの受益ユーザーを含んでいるエンティティの属性を示します。 この属性は |
|
|
このアノテーションはパーミッションの受益ロールを含んでいるエンティティの属性を示します。 この属性は |
|
|
このアノテーションはユーザーとロールパーミッションを同じエンティティ(テーブル)に保存する場合に使用します。 エンティティのユーザーとロールパーミッション属性の識別のために使用します。 デフォルトで、 @PermissionDiscriminator(userValue = "u", roleValue = "r") |
この例ではユーザーとロールパーミッションが一つのエンティティクラスに保持されています。 下に示したクラスはサンプルのSeamSpaceからのものです。
@Entity
@Name("message")
@Scope(EVENT)
public class Message implements Serializable
{
private Long id;
private String title;
private String text;
private boolean read;
private Date datetime;
@Id @GeneratedValue
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
@NotNull @Length(max=100)
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
@NotNull @Lob
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
@NotNull
public boolean isRead() {
return read;
}
public void setRead(boolean read) {
this.read = read;
}
@NotNull
@Basic @Temporal(TemporalType.TIMESTAMP)
public Date getDatetime() {
return datetime;
}
public void setDatetime(Date datetime) {
this.datetime = datetime;
}
}
上の例に見るように、getDiscriminator()
メソッドが @PermissionDiscriminator
でアノテートされ、どのレコードがユーザーのパーミッションを示し、どのレコードがロールパーミッションを示しているかをJpaPermissionStore
に示しています。 さらに、getRecipient()
メソッドが@PermissionUser
と@PermissionRole
でアノテートされています。 これは、間違いではなく discriminator
属性の値により、エンティティのrecipient
属性の内容をユーザーの名前、あるいはロールの名前として処理する事を示しています。
これ以外のクラス特有のアノテーションを使用して対象クラスに対して特定のパーミッションを設定する事ができます。 これらのパーミッションはorg.jboss.seam.annotation.security.permission
パッケージにあります。
表 15.10. クラス パーミッション アノテーション
アノテーション |
ターゲット |
詳細 |
---|---|---|
|
|
コンテナのアノテーション、 このアノテーションは |
|
|
このアノテーションでは対象クラスに対して一つのパーミッションアクションを認めています。 |
この例では、上のアノテーションを使っています。 下のクラスはサンプルのSeamSpaceにもあります。
@Permissions({
@Permission(action = "view"),
@Permission(action = "comment")
})
@Entity
public class MemberImage implements Serializable {
この例ではview
とcomment
の二つのパーミッションアクションをMemberImage
エンティティクラスに対して宣言する方法を示しています。
デフォルトでは、一つの対象オブジェクトと受益者に対する複数のパーミッションは、一つのデータベースレコードとしてaction
属性(DBではカラム)に複数のアクションをコンマで区切って記述され、保持されます。 大量のパーミッション情報を、物理的な保存領域を抑えてデータベースに保存するために、パーミッションアクションにコンマ区切りの文字列リストの代わりに、整数のビットマスク値を使用する事ができます。
例えば、受益者 "Bob"が特定のMemberImage
(エンティティBean)インスタンスに対して view
とcomment
のパーミッションが付与されていた場合、パーミッションエンティティのaction
属性は、二つのパーミッションアクションを付与されていることを示し"view,comment
"を含んでいます。 代わりに、ビットマスクをパーミッションアクションに使用すると下のようになります。
@Permissions({
@Permission(action = "view", mask = 1),
@Permission(action = "comment", mask = 2)
})
@Entity
public class MemberImage implements Serializable {
action
属性は、この場合単に"3"(bit 1 と 2 がonの状態)となります。 特定の対象クラスに対する大量のアクション許可を記述する場合には、アクションにビットマスクを使用する事により、明らかにパーミッションレコードの保存に必要な容量を圧縮する事ができます。
mask
の値が2のn乗になっている事は明らかに重要です。
JpaPermissionStore
は、パーミッションを保存したり、参照したりするときに対象のインスタンスのパーミッションについて効果的に操作を行うために、対象インスタンスを一意に特定できる必要があります。 これを実現するために、ユニークなIDを生成するよう個々の対象となるクラスに対してidentifier strategyがアサインされます。 それぞれのID戦略実装により、クラスのタイプに応じてユニークなIDの生成が行われます。
IdentifierStrategy
インタフェースはたいへんに単純で、二つのメソッドを宣言しているだけです。
public interface IdentifierStrategy {
boolean canIdentify(Class targetClass);
String getIdentifier(Object target);
}
最初のメソッドcanIdentify()
は、識別子ストラテジーが指定されたターゲットクラスに対してユニークな識別子を生成可能な場合にtrue
を返します。 第2のメソッドgetIdentifier()
は指定されたターゲットオブジェクトに対してユニークな識別子の値を返します。
Seamは二つのIdentifierStrategy
実装、ClassIdentifierStrategy
とEntityIdentifierStrategy
を提供しています(詳細は次のセクション)。
特定のクラスに対して、特別のID戦略を使用するよう明示的に設定するには、org.jboss.seam.annotations.security.permission.Identifier
アノテーションがされ、IdentifierStrategy
インタフェースの実装に値が設定されている必要があります。 オプションとしてname
属性を指定する事も可能ですが、この指定が及ぼす効果は IdentifierStrategy
の実際の実装に依存します。
ID戦略はクラスに対してユニークなIDを生成するために使用し、指定してあれば@Identifier
アノテーション中の name
の値が使用されます。 もし、name
属性が指定されていない場合には、Seamのコンポーネントであれば対象のクラスのコンポーネント名を使用するか、さもなくばパッケージ名を除くクラスの名前を使用します。 下の例にあるクラスのIDは"customer
"となります。
@Identifier(name = "customer")
public class Customer {
以下のクラスの識別子は"customerAction
"となります:
@Name("customerAction")
public class CustomerAction {
最終的に、以下のクラスの識別子は "Customer
"となります:
public class Customer {
このID戦略はエンティティBean毎にユニークなIDを割り当てる方法で、エンティティのプライマリキーを示す文字列とエンティティの名前をつなぎ合わせて、IDを生成しています。 IDの名前セクションの生成ルールはClassIdentifierStrategy
と同様です。 プライマリキー値 (即ち、エンティティのid ) は PersistenceProvider
コンポーネントを使って取得する事ができ、アプリケーションでどの永続性実装を使用しているかに依存せずに値を決める事ができます。 @Entity
でアノテートされていないエンティティについては, エンティティクラス自身に下のように明示的にID戦略を設定する事が必要です。
@Identifier(value = EntityIdentifierStrategy.class)
public class Customer {
生成される識別子の例として、下のようなエンティティクラスを考えてみましょう。
@Entity
public class Customer {
private Integer id;
private String firstName;
private String lastName;
@Id
public Integer getId() { return id; }
public void setId(Integer id) { this.id = id; }
public String getFirstName() { return firstName; }
public void setFirstName(String firstName) { this.firstName = firstName; }
public String getLastName() { return lastName; }
public void setLastName(String lastName) { this.lastName = lastName; }
}
id
が1
のCustomer
のインスタンスに対する識別子は"Customer:1
"となります。 もし、エンティティクラスに明示的な識別子名のアノテーションがあれば、
@Entity
@Identifier(name = "cust")
public class Customer {
結果として、id
が123
のCustomer
は "cust:123
"という識別子を持つことになります。
Seamセキュリティでユーザーとロールの管理のために提供しているID管理APIと同様に、永続的なユーザーのアクセス権の管理のためのパーミッションマネージメントAPIを PermissionManager
により提供しています。
PermissionManager
コンポーネントはパーミッションを管理するための多くのメソッドを持つアプリケーションスコープのSeamのコンポーネントです。 使用する前にパーミッションストアを設定する必要があります(デフォルトで、JpaPermissionStore
が存在すれば、これを使うようになります)。 明示的に別のパーミッションストアを設定する場合には、components.xml
にpermission-store
を設定します。
<security:permission-manager permission-store="#{ldapPermissionStore}"/>
以下の表にPermissionManager
の提供するメソッドの詳細を示します。
表 15.11. パーミッションマネージャAPIのメソッド
戻り値の型 |
メソッド |
詳細 |
---|---|---|
|
|
指定されたターゲットとアクションに対する承認されたすべてのパーミッションを示す |
|
|
指定されたターゲットとアクションに対する承認されたすべてのパーミッションを示す |
|
|
バックエンドのパーミッションストアに指定した |
|
|
バックエンドのパーミッションストアに指定した複数の |
|
|
バックエンドのパーミッションストアから指定した |
|
|
バックエンドのパーミッションストアから指定した複数の |
|
|
対象ターゲットに対する適用可能なアクションのリストを返す。 返されるアクションはターゲットオブジェクトクラスに設定されている |
PermissionManager
メソッドを起動する場合には現在の認証ユーザーが当該管理操作をするために必要な適切なパーミッションを持っている必要があります。 下の表に、現在のユーザーが持っていなければならないパーミッションの一覧を示します。
表 15.12. パーミッション管理 セキュリティパーミッション
メソッド |
パーミッションの対象 |
パーミッションのアクション |
---|---|---|
|
特定の |
|
|
ターゲットの特定の |
|
|
ターゲットの特定の |
|
|
それぞれのターゲットの特定の |
|
|
ターゲットの特定の |
|
|
それぞれのターゲットの特定の |
|
SeamはHTTPSプロトコルによるpageのセキュリティを基本的な部分についてサポートしています。 この機能は、pages.xml
で必要なページについてscheme
を指定することにより簡単に設定することができます。 下の例では/login.xhtml
でHTTPSを使う様に設定しています。
<page view-id="/login.xhtml" scheme="https"/>
また、この設定は自動的にJSFのs:link
やs:button
にも引き継がれ (view
で指定した場合) 、リンクも正しいプロトコルで描画されます。前述の例の場合、下のようなリンクも/login.xhtml
がHTTPSを使うように設定されているために、s:link
先のlogin.xhmtl
にもHTTPSがプロトコルとして使用されます。
<s:link view="/login.xhtml" value="Login"/>
指定されたプロトコル以外 (正しくないプロトコル) を使って、ページを見ようとすると、正しいプロトコルを使って、指定のページへリダイレクトされます。 schema="https"
が指定されているページにhttpでアクセスしようとすると、そのページにhttpsを使ってリダイレクトされます。
すべてのページに対してデフォルトのschemeを設定することも可能で、一部のページに対してHTTPSを使用したい場合などに有効です。 デフォルトスキーマが設定されていない場合には、現在のスキーマを継承します。 従って、ユーザーがHTTPSを必要とするページにアクセスすると、それ以降はHTTPSを必要としないページに対してもHTTPSを使ったアクセスとなります(これは、セキュリティ上は良いのですが、パフォーマンス上は問題があります)。 HTTPをデフォルトのscheme
として指定する場合には次の一行をpages.xml
に追加してください。
<page view-id="*" scheme="http" />
もちろん、HTTPSを使う必要がなければ、デフォルトのschemaを指定する必要もありません。
以下のように、components.xml
に設定することにより、スキーマが変さらになるたびに現在のHTTPセッションを自動的に無効にする事ができます。
<web:session invalidate-on-scheme-change="true"/>
このオプションはHTTPSのページから、HTTPのページへの重要データの漏れや、セッションIDの盗聴に対する脆弱性を減少させます。
もし個別にHTTPとHTTPSの使用を設定する必要があるのであれば、pages.xml
のpages
エレメントにhttp-port
あるいは https-port
を設定することにより行えます。
<pages xmlns="http://jboss.com/products/seam/pages"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://jboss.com/products/seam/pages http://jboss.com/products/seam/pages-2.1.xsd"
no-conversation-view-id="/home.xhtml"
login-view-id="/login.xhtml"
http-port="8080"
https-port="8443"
>
厳密にはセキュリティAPIの一部ではありませんが、SeamはCAPCHA(Completely Automated Public Turing test to tell Computers and Humans Apart)アルゴリズムを内蔵しており、ウェブ上の自動処理プログラムによりアプリケーションが動作しないようにする事を可能にしています。
キャプチャを起動して走らせるためには、Seamのリソースサーブレットを下のように、web.xml
に設定する必要があります。これにより、アプリケーションのページにキャプチャチャレンジのイメージを提供するようになります。
<servlet>
<servlet-name
>Seam Resource Servlet</servlet-name>
<servlet-class
>org.jboss.seam.servlet.SeamResourceServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name
>Seam Resource Servlet</servlet-name>
<url-pattern
>/seam/resource/*</url-pattern>
</servlet-mapping
>
キャプチャチャレンジをフォームに追加するのは以下のようにいたって簡単です:
<h:graphicImage value="/seam/resource/captcha"/>
<h:inputText id="verifyCaptcha" value="#{captcha.response}" required="true">
<s:validate />
</h:inputText>
<h:message for="verifyCaptcha"/>
これですべてです。 graphicImage
がキャプチャチャレンジの表示を制御し、inputText
がユーザーからの入力を受け付けます。 ユーザーの入力はフォームが送信された時に自動的にキャプチャと照合されます。
内蔵コンポーネントをオーバーライドする事により、キャプチャのアルゴリズムをカスタマイズすることができます。
@Name("org.jboss.seam.captcha.captcha")
@Scope(SESSION)
public class HitchhikersCaptcha extends Captcha
{
@Override @Create
public void init()
{
setChallenge("What is the answer to life, the universe and everything?");
setCorrectResponse("42");
}
@Override
public BufferedImage renderChallenge()
{
BufferedImage img = super.renderChallenge();
img.getGraphics().drawOval(5, 3, 60, 14); //add an obscuring decoration
return img;
}
}
以下の表に、セキュリティ関連のイベントによりSeamのセキュリティで発生するイベント(章 6. イベント、インタセプタ、例外処理)を一覧にしました。
表 15.13. セキュリティ イベント
イベントキー |
詳細 |
---|---|
|
ログインに成功した時に発生 |
|
ログインに失敗した時に発生 |
|
ユーザーがすでに認証されていて再度ログインした時に発生 |
|
ユーザーがログインしていないためにセキュリティチェックに失敗した時に発生 |
|
ユーザーがログインできても十分な特権が無くセキュリティチェックに失敗した時に発生 |
|
ユーザーが認証される直前に発生 |
|
ユーザーが認証された直後に発生 |
|
ユーザーがログアウトした後に発生 |
|
ユーザーのクレデンシャル(信用情報)が変更された時に発生 |
|
IdentityのrememberMeプロパティが変更された時に発生 |
場合により、上位権限で処理することが必要な場合があります(たとえば、認証されていないユーザーが、新しいユーザーアカウントを作成する場合)。 Seamはこのような機能をRunAsOperation
クラスで提供しています。 このクラスは、限定された一組の操作に対して Principal
、Subject
、あるいはユーザーのロールを一時的に上書きする事を可能にします。
以下のコード例ではRunAsOperation
の使われ方について、addRole()
メソッドを呼び出して操作終了まで特定のロールを付与する方法を示します。 execute()
メソッドはより上位の特権で実行するためのコードを持っています。
new RunAsOperation() {
public void execute() {
executePrivilegedOperation();
}
}.addRole("admin")
.run();
同様にgetPrincipal()
や getSubject()
メソッドはPrincipal
インスタンスおよび Subject
インスタンスを実行中に上書きする事ができます。 最終的にRunAsOperation
を実行するためにrun()
メソッドを使用します。
アプリケーションが特別なセキュリティを要求する場合には、Identityコンポーネントを拡張する必要がある場合があります。以下の例(説明のためのもので、通常、クレデンシャルはCredentials
コンポーネントにより処理されます)ではcompanyCode
フィールドを追加した拡張Identityコンポーネントを示しています。APPLICATION
により、拡張Identityが内蔵Identityよりも優先されてインストールされることを保証しています。
@Name("org.jboss.seam.security.identity")
@Scope(SESSION)
@Install(precedence = APPLICATION)
@BypassInterceptors
@Startup
public class CustomIdentity extends Identity
{
private static final LogProvider log = Logging.getLogProvider(CustomIdentity.class);
private String companyCode;
public String getCompanyCode()
{
return companyCode;
}
public void setCompanyCode(String companyCode)
{
this.companyCode = companyCode;
}
@Override
public String login()
{
log.info("###### CUSTOM LOGIN CALLED ######");
return super.login();
}
}
SESSION
コンテキストが開始されると同時に利用可能とするために、Identity
コンポーネントは@Startup
とアノテートされている必要があることに留意してください。 これが行われていないと、Seamのいくつかの機能が動作しないことがあります。
OpenID is a community standard for external web-based authentication. The basic idea is that any web application can supplement (or replace) its local handling of authentication by delegating responsibility to an external OpenID server of the user's chosing. This benefits the user, who no longer has to remember a name and password for every web application he uses, and the developer, who is relieved of some of the burden of maintaining a complex authentication system.
When using OpenID, the user selects an OpenID provider, and the provider assigns the user an OpenID. The id will take the form of a URL, for example http://maximoburrito.myopenid.com
however, it's acceptable to leave off the http://
part of the identifier when logging into a site. The web application (known as a relying party in OpenID-speak) determines which OpenID server to contact and redirects the user to the remote site for authentication. Upon successful authentication the user is given the (cryptographically secure) token proving his identity and is redirected back to the original web application.The local web application can then be sure the user accessing the application controls the OpenID he presented.
It's important to realize at this point that authentication does not imply authorization. The web application still needs to make a determination of how to use that information. The web application could treat the user as instantly logged in and give full access to the system or it could try and map the presented OpenID to a local user account, prompting the user to register if he hasn't already. The choice of how to handle the OpenID is left as a design decision for the local application.
Seam uses the openid4java package and requires four additional JARs to make use of the Seam integration. These are: htmlparser.jar
, openid4java.jar
, openxri-client.jar
and openxri-syntax.jar
.
OpenID processing requires the use of the OpenIdPhaseListener
, which should be added to your faces-config.xml
file. The phase listener processes the callback from the OpenID provider, allowing re-entry into the local application.
<lifecycle>
<phase-listener>org.jboss.seam.security.openid.OpenIdPhaseListener</phase-listener>
</lifecycle>
With this configuration, OpenID support is available to your application. The OpenID support component, org.jboss.seam.security.openid.openid
, is installed automatically if the openid4java classes are on the classpath.
To initiate an OpenID login, you can present a simply form to the user asking for the user's OpenID. The #{openid.id}
value accepts the user's OpenID and the #{openid.login}
action initiates an authentication request.
<h:form>
<h:inputText value="#{openid.id}" />
<h:commandButton action="#{openid.login}" value="OpenID Login"/>
</h:form>
When the user submits the login form, he will be redirected to his OpenID provider. The user will eventually return to your application through the Seam pseudo-view /openid.xhtml
, which is provided by the OpenIdPhaseListener
. Your application can handle the OpenID response by means of a pages.xml
navigation from that view, just as if the user had never left your application.
The simplest strategy is to simply login the user immediately. The following navigation rule shows how to handle this using the #{openid.loginImmediately()}
action.
<page view-id="/openid.xhtml">
<navigation evaluate="#{openid.loginImmediately()}">
<rule if-outcome="true">
<redirect view-id="/main.xhtml">
<message>OpenID login successful...</message>
</redirect>
</rule>
<rule if-outcome="false">
<redirect view-id="/main.xhtml">
<message>OpenID login rejected...</message>
</redirect>
</rule>
</navigation>
</page>
Thie loginImmediately()
action checks to see if the OpenID is valid. If it is valid, it adds an OpenIDPrincipal to the identity component, marks the user as logged in (i.e. #{identity.loggedIn}
will be true) and returns true. If the OpenID was not validated, the method returns false, and the user re-enters the application un-authenticated. If the user's OpenID is valid, it will be accessible using the expression #{openid.validatedId}
and #{openid.valid}
will be true.
You may not want the user to be immediately logged in to your application. In that case, your navigation should check the #{openid.valid}
property and redirect the user to a local registration or processing page. Actions you might take would be asking for more information and creating a local user account or presenting a captcha to avoid programmatic registrations. When you are done processing, if you want to log the user in, you can call the loginImmediately
method, either through EL as shown previously or by directly interaction with the org.jboss.seam.security.openid.OpenId
component. Of course, nothing prevents you from writing custom code to interact with the Seam identity component on your own for even more customized behaviour.
Logging out (forgetting an OpenID association) is done by calling #{openid.logout}
. If you are not using Seam security, you can call this method directly. If you are using Seam security, you should continue to use #{identity.logout}
and install an event handler to capture the logout event, calling the OpenID logout method.
<event type="org.jboss.seam.security.loggedOut">
<action execute="#{openid.logout}" />
</event>
It's important that you do not leave this out or the user will not be able to login again in the same session.