本連載では、Java Platform, Enterprise Edition 6 (Java EE 6) を実際の開発に活用していただくための情報をお届けしています。今回も前回から引き続き Contexts and Dependency Injection for the Java EE platform(CDI)を取り上げます。前回は、CDIの基本的な機能を単純なHelloプログラムにより確認しました。今回は、CDIの機能を利用して、シンプルなアプリケーションを作成します。単純なDIの利用だけではなく、インターセプターやデコレータ、イベント機能などを利用して、AOP的な認証認可の仕組みなども取り入れてみます。(日本オラクル Fusion Middleware 事業統括本部二條智文) |
作成するアプリケーション
今回作成するアプリケーションは、Longin画面からログインをし、Documentを作成・更新・削除する機能を持つとシンプルなアプリケーションです。まずは、CDIのInjectionの機能を利用し、以下の機能を持ったアプリケーションを作成してみます。- ログイン機能(画面遷移)
- ログアウト機能(画面遷移)
- ドキュメントのロード機能
- ドキュメントの作成、更新、削除機能
なお、今回は、アプリケーション作成の手順を主に説明します。上記アプリケーションは、CDI仕様書の「1.3. Introductory examples」に記述されているコードをアレンジした内容となっています。詳細を知りたい方は、実際に動作するアプリケーションと見比べながら、上記仕様書を参照するとわかりやすいと思います。
今回作成するアプリケーションの完成版は、ここからダウンロード可能です。Step By Stepで開発手順を説明していますが、もしお時間がない場合は、こちらをダウンロードしてソースを見ながらお読みください。
データベースの準備
今回のアプリケーションはDBへアクセスし、ログイン情報や、ドキュメント情報、認可情報などを取得・更新します。そのため、アプリケーションで利用するDBの設定をしておきます。今回は、データベースにOracle Database Express Edition 11g Release 2(以下XE11g)を使用します。XE11gは、次のページ http://www.oracle.com/technetwork/jp/database/express-edition/overview/index.htmlよりダウンロードして、インストールしてください。
インストール後に、今回のアプリケーション用のスキーマと、テーブルおよびデータを定義します。まずは、DBユーザを作成します。コマンドプロンプトで、以下のようにSQL*Plusを実行してください。
> sqlplus / as sysdba
その後、以下のようにユーザを作成してください。
-- cdiスキーマ作成 CREATE USER cdi IDENTIFIED BY cdi; -- cdiユーザへのパーミッション付与 GRANT RESOURCE, CONNECT TO cdi;
次に、いくつかテーブルを作成します。上記のcdiユーザでログインし直してください。
> sqlplus cdi/cdi
次のSQLを実行してテーブルを作成します。
Usersテーブル
create table users ( username varchar2(20) NOT NULL, password varchar2(20) NOT NULL, banned number(1,0) NOT NULL, CONSTRAINT users_pk PRIMARY KEY (username) );
permissionsテーブル
create table permissions ( id varchar2(10) NOT NULL, username varchar2(20) NOT NULL, action varchar2(20) NOT NULL, datatype varchar2(100) NOT NULL, CONSTRAINT permissions_pk PRIMARY KEY (id) );
Documentsテーブル
create table documents ( id varchar2(10) NOT NULL, title varchar2(50) NOT NULL, text varchar2(255) NOT NULL, createdby varchar2(20) NOT NULL, CONSTRAINT documents_pk PRIMARY KEY (id) );
次のようにデータをinsertします。
--Userデータ insert into users values ('duke','welcome1', 0); insert into users values ('candy','welcome1', 0); insert into users values ('oracle','welcome1', 1); -- permissionデータ insert into permissions values ('1', 'duke','save', 'cdisample.entity.Document'); insert into permissions values ('2', 'duke','delete', 'cdisample.entity.Document'); insert into permissions values ('3', 'candy','save', 'cdisample.entity.Document'); commit;
以上で、データベースの設定は終わりました。次は、WebLogic上にデータソースを作成しておきます。”demo/CDI”という名称のデータソースを作成してください。
データソースの作成手順は、[連載] WebLogic Server 12cでJava EE 6を動かしてみよう!(4) JPA 第1回の”WebLogic Server のデータソースの作成”の項を参照してください。
アプリケーション開発の準備
前回と同様に、CDIアプリケーション用のプロジェクトをEclipse上に作成します。前回の記事を参考にして、「CDISimpleApp」という名称の”Dynamic Web Project”を作成してください。また、前回と同様にCDI機能を有効化するために、WEB-INF直下にbeans.xmlファイルを配置します。今回は、JSFおよびJPAを利用するため、プロジェクトのFacetにこの2つを追加します。CDISimpleAppを右クリックし、Properties > Project Facetsを選択し、「Project Facets」画面で、”JavaServer Faces”および、”JPA”にチェックを入れてOKボタンを押してください。
以上でアプリケーション作成の準備は完了です。アプリケーションの作成 - ログイン機能の実装
それでは、まず、ログイン機能を実装していきます。作成するコードは以下になります。- Quarlifier
- Users.java: DBのUsersテーブルへアクセスを表すQuarilier
- Documents.java: DBのDcumentsテーブルへのアクセスを表すQuralifier
- 汎用Class
- Databases.java: JAP EntityManagerのDIを定義するクラス
- エンティティ
- User.java: JPAのUserエンティティ
- モデル
- Login.java: ログイン処理を実装するクラス
- Credential.java: 画面に入力したUsername/Passwordを保持
- 例外
- NotLoggedInException.java: ログインしていない場合用の例外
- 画面
- login.xhtml: ログイン画面
- documents.xhtml : ドキュメント編集画面
まず、CDIで利用するQualifierを作成します。次のようなQualifierを2つ作成してください。
package: cdisample.qualifier
Class: Users.java
Class: Users.java
package cdisample.qualifier; import java.lang.annotation.Retention; import java.lang.annotation.ElementType; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import javax.inject.Qualifier; @Qualifier @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE,ElementType.FIELD,ElementType.METHOD,ElementType.PARAMETER,ElementType.CONSTRUCTOR}) public @interface Users {}
Package: cdisample.qualifier
class: Documents.java
class: Documents.java
package cdisample.qualifier; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import javax.inject.Qualifier; @Qualifier @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE,ElementType.FIELD,ElementType.METHOD,ElementType.PARAMETER,ElementType.CONSTRUCTOR}) public @interface Documents {}
JPAで利用するEntitiyManagerをCDIを利用して上記のQalifierで注入できるように以下のようなクラスを作成します。
Package: cdisample.dao
class: Databases.java
class: Databases.java
package cdisample.dao;
import javax.enterprise.inject.Produces;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.PersistenceContext;
import javax.persistence.PersistenceUnit;
import cdisample.qualifier.Documents;
import cdisample.qualifier.Users;
//EntityManagerをDIするためのクラス、@Producesで定義
public class Databases {
@Produces @PersistenceContext
@Users EntityManager userDatabaseEntityManager;
@Produces @PersistenceUnit
@Users EntityManagerFactory userDatabaseEntityManagerFactory;
@Produces @PersistenceContext
@Documents EntityManager docDatabaseEntityManager;
}
Userエンティティを作成します。以下のようなクラスを作成します。
Package: cdisample.entity
Class: User.java
Class: User.java
package cdisample.entity;
import java.io.Serializable;
import javax.persistence.*;
@Entity
@Table(name="USERS")
public class User implements Serializable {
private static final long serialVersionUID = 1L;
@Id
private String username;
private boolean banned;
private String password;
public User() {}
// getter/setterをこれ以降に記述します
}
Psersistence.xmlを以下のように編集します。すべてを書き換えてください。
Persisntence.xml
<?xml version="1.0" encoding="UTF-8"?><persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"><persistence-unit name="CDISimpleApp"><provider>org.eclipse.persistence.jpa.PersistenceProvider</provider><jta-data-source>demo/CDI</jta-data-source><class>cdisample.entity.User</class><properties><property name="eclipselink.target-server" value="WebLogic"/> </properties></persistence-unit></persistence>Login/Logout処理を実行するModelクラスを作成します。
以下のクラスを作成してください。
Package: cdisample.model
Class:Login.java
Class:Login.java
package cdisample.model; import java.io.Serializable; import java.util.List; import javax.enterprise.context.SessionScoped; import javax.enterprise.event.Event; import javax.enterprise.inject.Model; import javax.enterprise.inject.Produces; import javax.faces.application.FacesMessage; import javax.faces.context.ExternalContext; import javax.faces.context.FacesContext; import javax.inject.Inject; import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; import javax.persistence.Parameter; import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.CriteriaQuery; import javax.persistence.criteria.Root; import cdisample.entity.User; import cdisample.exception.NotLoggedInException; import cdisample.qualifier.LoggedIn; import cdisample.qualifier.Users; @SessionScoped @Model public class Login implements Serializable { @Inject Credentials credentials; // JSFの入力値をDI @Inject @Users EntityManager userDatabase; // EntityManagerをDI//クエリ用 private CriteriaQueryquery; private Parameter usernameParam; private Parameter passwordParam; private User user; @Inject // Injectされてインスタンス化されるタイミングで、以下のメソッドがコールされる void initQuery(@Users EntityManagerFactory emf) { CriteriaBuilder cb = emf.getCriteriaBuilder(); usernameParam = cb.parameter(String.class); passwordParam = cb.parameter(String.class); query = cb.createQuery(User.class); Root u = query.from(User.class); query.select(u); query.where(cb.equal(u.get("username"), usernameParam), cb.equal(u.get("password"), passwordParam)); } public String login() { List results = userDatabase.createQuery(query) .setParameter(usernameParam, credentials.getUsername()) .setParameter(passwordParam, credentials.getPassword()) .getResultList(); if (!results.isEmpty()) { user = results.get(0); } else { FacesMessage msg = new FacesMessage("Invalid Username or Password"); msg.setSeverity(FacesMessage.SEVERITY_ERROR); FacesContext.getCurrentInstance().addMessage(null, msg); return null; } return "documents"; } public String logout() { user = null; ExternalContext context = FacesContext.getCurrentInstance() .getExternalContext(); context.invalidateSession(); return "login"; } public boolean isLoggedIn() { return user != null; } // ログインユーザをDI可能にするために@Producesで定義 @Produces @SessionScoped @LoggedIn public User getCurrentUser() { if (user == null) { throw new NotLoggedInException(); } else { return user; } } }
Package: cdisample.model
Class:Credentials.java
Class:Credentials.java
package cdisample.model;
import javax.enterprise.inject.Model;
@Model
public class Credentials {
private String username;
private String password;// これ以降にGetter/Setterを記述
}
上記2つのクラスで使用している@Modelアノテーションは、CDIの組み込みタイプで、いわゆるMVCモデルのModelを表すアノテーションです。次に例外クラスを1つ作成します。
Package: cdisample.exception/Class:NotLoggedInException.java
package cdisample.exception; public class NotLoggedInException extends RuntimeException { }ここまでで、コンパイルエラーが無い状態になっていれば、問題ありません。
次にJSFを利用してUIを作成します。ここでは、WebContentの直下にlogin.xhtmlとdocuments.xhtmlを作成します。
login.xhml
<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN""http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html xmlns="http://www.w3.org/1999/xhtml" xmlns:f="http://java.sun.com/jsf/core" xmlns:h="http://java.sun.com/jsf/html"><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /><title>CDI Sample App - Login</title></head><body><f:view><h:messages style="color: red" /><h:form><h:panelGrid columns="2" rendered="#{!login.loggedIn}"><h:outputLabel for="username">Username:</h:outputLabel><h:inputText id="username" value="#{credentials.username}" /><h:outputLabel for="password">Password:</h:outputLabel><h:inputSecret id="password" value="#{credentials.password}" /></h:panelGrid><h:commandButton value="Login" action="#{login.login}" rendered="#{!login.loggedIn}" /></h:form></f:view></body></html>
documents.xhtml
<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN""http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html xmlns="http://www.w3.org/1999/xhtml" xmlns:f="http://java.sun.com/jsf/core" xmlns:h="http://java.sun.com/jsf/html"><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /><title>CDI Sample App - Documents</title></head><body><f:view><h:messages style="color: red" /><h:form><h:panelGrid columns="1" rendered="#{login.loggedIn}"><h:outputLabel>signed in as <h:outputText value="#{login.currentUser.username}" /></h:outputLabel></h:panelGrid><h:commandButton value="Logout" action="#{login.logout}" rendered="#{login.loggedIn}" /></h:form></f:view></body></html>
一旦この状態で、実行してみましょう。ログイン・ログアウト機能が利用できます。
Login.xhtmlを右クリックして実行してください。右のようなシンプルなログイン画面が表示されます。
テストデータとして、usernameに”duke”、passwordに”welcome1”を入力して、Loginボタンを押してください。"signed in as duke"という画面が表示されれば正常に動作しています。アプリケーションの作成–ドキュメント編集機能の実装
次に、ドキュメントの編集機能を実装します。ここでは、以下のようなコードを作成します。- エンティティ
- Document.java: JPAのDocumentエンティティ
- データアクセス
- DataAccess.java: DAOを表すインタフェース
- DocumentDataAccess.java: DocumentsテーブルにアクセスするDAOクラス
- モデル
- DocumentEditor.java: 画面からのDocument処理を実行するクラス
- 画面
- documents.xhtml : ドキュメント編集機能の追加
まずは、Documentエンティティを作成します。
Package: cdisample.entity
Class: Document.java
Class: Document.java
package cdisample.entity;
import java.io.Serializable;
import javax.persistence.*;
@Entity
@Table(name="DOCUMENTS")
public class Document implements Serializable {
private static final long serialVersionUID = 1L;
@Id
private String id;
private String createdby;
private String text;
private String title;
public Document() {}
// getter/setterをこれ以降に記述します
}
Persistence.xmlに上記のエンティティを追加します。
persistence.xml
<?xml version="1.0" encoding="UTF-8"?><persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"><persistence-unit name="CDISimpleApp"><provider>org.eclipse.persistence.jpa.PersistenceProvider</provider><jta-data-source>demo/CDI</jta-data-source><class>cdisample.entity.User</class><class>cdisample.entity.Document</class><properties><property name="eclipselink.target-server" value="WebLogic"/></properties></persistence-unit></persistence>
次に、Documentsテーブルのアクセス用にDAOクラスを作成します。まずは、データアクセスを汎用化するインタフェースを以下のように定義します。
Package: cdisample.dao
Class: DataAccess.java
Class: DataAccess.java
package cdisample.dao; public interface DataAccess{ public T load(V id); public void save(T object); public void delete(V object); public Class getDataType(); }
次に、上記を実装したDocumentDataAccessクラスを作成します。
Package: cdisample.dao
Class: DocumentDataAccess.java
Class: DocumentDataAccess.java
package cdisample.dao; import javax.ejb.Stateless; import javax.inject.Inject; import javax.persistence.EntityManager; import cdisample.entity.Document; import cdisample.qualifier.Documents; @Stateless public class DocumentDataAccess implements DataAccess{ @Inject @Documents EntityManager em; public Document load(String id) { return em.find(Document.class, id); } public void save(Document document) { Document currentDocument = em.find(Document.class, document.getId()); if (currentDocument == null) { em.persist(document); } else { em.merge(document); } } public void delete(String id) { Document currentDocument = em.find(Document.class, id); if (currentDocument != null) { em.remove(currentDocument); } } public Class getDataType() { return Document.class; } }
次に、画面から実行されるModelクラスを作成します。
Package: cdisample.model
Class: DocumentEditor.java
Class: DocumentEditor.java
package cdisample.model; import javax.enterprise.context.RequestScoped; import javax.enterprise.inject.Model; import javax.inject.Inject; import cdisample.dao.DataAccess; import cdisample.entity.Document; import cdisample.entity.User; import cdisample.qualifier.LoggedIn; @RequestScoped @Model public class DocumentEditor implements java.io.Serializable { @Inject private DataAccessdocEditor; // DAOをDI private String id; private String title; private String text; @Inject @LoggedIn private User currentUser; // ログインユーザをDI public void load() { Document document = docEditor.load(id); if (document == null) { document = new Document(); } title = document.getTitle(); text = document.getText(); } public void save() { Document document = new Document(); document.setId(id); document.setTitle(title); document.setText(text); document.setCreatedby(currentUser.getUsername()); docEditor.save(document); } public void delete() { docEditor.delete(id); id = null; title = null; text = null; } public String getId() { return id; } public void setId(String id) { this.id = id; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getText() { return text; } public void setText(String text) { this.text = text; } }
最後に、JSFにドキュメント編集UIを追加します。Document.xhtmlを以下のように編集してください。
document.xhtml
<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN""http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html xmlns="http://www.w3.org/1999/xhtml"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:h="http://java.sun.com/jsf/html"><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /><title>CDI Sample App - Documents</title></head><body><f:view><h:messages style="color: red" /><h:form><h:panelGrid columns="1" rendered="#{login.loggedIn}"><h:outputLabel>signed in as <h:outputText
value="#{login.currentUser.username}" /></h:outputLabel></h:panelGrid><h:commandButton value="Logout" action="#{login.logout}"
rendered="#{login.loggedIn}" /></h:form><h:form rendered="#{login.loggedIn}"><h:panelGrid columns="3"><h:outputLabel for="id">id:</h:outputLabel><h:inputText id="id" value="#{documentEditor.id}" /><h:commandButton value="load" action="#{documentEditor.load}" /><h:outputLabel for="Title">Title:</h:outputLabel><h:inputText id="Title" value="#{documentEditor.title}" /><h:outputLabel for="Title"></h:outputLabel><h:outputLabel for="Text">Text:</h:outputLabel><h:inputTextarea id="Text" value="#{documentEditor.text}" /><h:outputLabel for="Title"></h:outputLabel></h:panelGrid><h:panelGrid columns="2"><h:commandButton value="save" action="#{documentEditor.save}" /><h:commandButton value="delete" action="#{documentEditor.delete}" /></h:panelGrid></h:form></f:view></body></html>
この状態で、login.xhtmlを実行してみて下さい。先ほどと同じようにログインすると、右のようなドキュメント編集画面が出ます。Load/save/deleteなどの機能が動作します。
以上、CDIを利用したシンプルなアプリケーションを実装してみました。CDIを利用することで、スコープを意識せずに、@Injectするのみで必要なBeanにアクセスすることができ、コード同士を疎結合な状態にできることを体感できたと思います。
セキュアなアプリケーションの実装
次のステップでは、先ほど作成したアプリケーションに、認可処理を追加して、セキュアなアプリケーションを実装していきます。認可処理には、インターセプター、デコレータなどの機能を利用し、業務ロジックには影響を与えずに、セキュアな実装を追加していきます。
データアクセスの保護
CDIのインターセプター機能を利用して、各種データアクセスを実行する際に、ログインユーザの状態を判断し、ユーザが無効であった場合はデータアクセスをはじくといった、データアクセスをセキュアに実行する機能を実装してみます。まずは、汎用的なログ出力のためのBeanを作成します。
Package: cdisample.util
Class: Loggers.java
Class: Loggers.java
package cdisample.util;
import java.util.logging.Logger;
import javax.enterprise.inject.Produces;
import javax.enterprise.inject.spi.InjectionPoint;
public class Loggers {
// LoggerをDIするための@Produces定義。引数のInjectionPointからクラス情報をセット
@Produces Logger getLogger(InjectionPoint injectionPoint) {
return Logger.getLogger(injectionPoint.getMember().getDeclaringClass().getSimpleName());
}
}
次に、インターセプター用のアノテーションを定義します。
Package: cdisample.qualifier
Class: Secure.java
Class: Secure.java
package cdisample.qualifier; import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.ElementType; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import javax.interceptor.InterceptorBinding; @InterceptorBinding @Inherited @Target({ElementType.TYPE,ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface Secure {}
上記アノテーションを定義したインターセプタークラスを作成します。
Package: cdisample.qualifier
Class: Secure.java
Class: Secure.java
package cdisample.interceptor; import java.io.Serializable; import java.util.logging.Logger; import javax.inject.Inject; import javax.interceptor.AroundInvoke; import javax.interceptor.Interceptor; import javax.interceptor.InvocationContext; import cdisample.entity.User; import cdisample.exception.NotAuthorizedException; import cdisample.qualifier.LoggedIn; import cdisample.qualifier.Secure; @Secure @Interceptor public class AuthorizationInterceptor implements Serializable { @Inject @LoggedIn User user; //ログインユーザを取得 @Inject Logger log; // ログ出力用 @AroundInvoke public Object authorize(InvocationContext ic) throws Exception { if (!user.isBanned()) { // ユーザが有効な場合は、処理を続行 log.info("Authorized: " + user.getUsername()); return ic.proceed(); //インターセプトした処理を実行 } else { // ユーザが無効な場合は、Exceptionを投げる log.info("Not authorized: " + user.getUsername()); throw new NotAuthorizedException(); } } }
例外クラスの作成
Package: cdisample.exception
Class: NotAuthorizedException.java
Class: NotAuthorizedException.java
package cdisample.exception; public class NotAuthorizedException extends RuntimeException { public NotAuthorizedException() { } public NotAuthorizedException(String action) { super(action); } }
最後に、インターセプトしてセキュアにしたいクラスに@Secureアノテーションを付与します。以下のように、DocumentDataAccessクラスに@Secureアノテーションを付与します。
Package: cdisample.dao
Class: DocumentDataAccess.java
Class: DocumentDataAccess.java
package cdisample.dao;…略… import cdisample.qualifier.Secure; @Stateless@Secure public class DocumentDataAccess implements DataAccess{…略… }
最後に、beans.xmlにインターセプターの定義を追記します。
Package: cdisample.exception
Class: NotAuthorizedException.java
Class: NotAuthorizedException.java
package cdisample.exception; public class NotAuthorizedException extends RuntimeException { public NotAuthorizedException() { } public NotAuthorizedException(String action) { super(action); } }
最後に、インターセプトしてセキュアにしたいクラスに@Secureアノテーションを付与します。以下のように、DocumentDataAccessクラスに@Secureアノテーションを付与します。
beans.xml
<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/beans_1_0.xsd"><interceptors><class>cdisample.interceptor.AuthorizationInterceptor</class></interceptors></beans>
この状態で、login.xhtmlを実行してみます。ログインのusername/passwordは”oracle/welcome1”を入力してください。このユーザデータは無効フラグが立っているため、ログイン後にドキュメント編集画面で何らかのボタンを押すと、DocumentDataAccessを実行する処理がインターセプトされて、Exceptionがスローされます。その他のユーザでは、正常に動作することを確認してみましょう。
このようにインターセプターを利用すると、業務ロジックに影響せずに、@Secureアノテーションを定義するのみでセキュアに処理を実行することが可能になります。
データ操作に対する認可機能の追加
インターセプター機能で実装したのは、ログインユーザが無効だった場合にすべてのデータアクセスを禁止するという処理でした。ここでは、デコレータ機能を利用して、データアクセスの更新・削除それぞれの操作ごとに認可する仕組みを入れ込んでみます。権限の無い操作をした場合は、エラーとなるようにします。まず、認可情報を保持するPermissionエンティティを作成します。
Package: cdisample.entity
Class: Document.java
Class: Document.java
package cdisample.entity; import java.io.Serializable; import javax.persistence.*; @Entity @Table(name="PERMISSIONS") public class Permission implements Serializable { private static final long serialVersionUID = 1L; @Id private String id; @Column(name="\"ACTION\"") private String action; private String datatype; private String username; public Permission() { } public Permission(String action, String datatype) { this.action = action; this.datatype = datatype; } // ここにgetter/setterを記述// action/datatypeのみで同値判定するhashCode/equalsを定義 @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((action == null) ? 0 : action.hashCode()); result = prime * result + ((datatype == null) ? 0 : datatype.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; Permission other = (Permission) obj; if (action == null) { if (other.action != null) return false; } else if (!action.equals(other.action)) return false; if (datatype == null) { if (other.datatype != null) return false; } else if (!datatype.equals(other.datatype)) return false; return true; } }
persistence.xmlに上記のエンティティを追加します。
persistence.xml
<?xml version="1.0" encoding="UTF-8"?><persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"><persistence-unit name="CDISimpleApp"><provider>org.eclipse.persistence.jpa.PersistenceProvider</provider><jta-data-source>demo/CDI</jta-data-source><class>cdisample.entity.User</class><class>cdisample.entity.Document</class><class>cdisample.entity.Permission</class><properties><property name="eclipselink.target-server" value="WebLogic"/> </properties></persistence-unit></persistence>
次に、ログイン時に、CDIのイベント機能を利用して、セッションにログインユーザの認可情報を保持する機能を追加します。
Package: cdisample.event
Class: Permissions.java
Class: Permissions.java
package cdisample.event; import java.io.Serializable; import java.util.HashSet; import java.util.Set; import javax.enterprise.context.SessionScoped; import javax.enterprise.event.Observes; import javax.enterprise.inject.Produces; import javax.inject.Inject; import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; import javax.persistence.Parameter; import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.CriteriaQuery; import javax.persistence.criteria.Root; import cdisample.entity.User; import cdisample.entity.Permission; import cdisample.qualifier.LoggedIn; import cdisample.qualifier.Users; @SessionScoped public class Permissions implements Serializable { @Produces // ログインユーザの認可情報をDIするための定義 private Setpermissions = new HashSet (); @Inject @Users EntityManager userDatabase; Parameter usernameParam; CriteriaQuery query; @Inject // DI時にPermissionデータをDBから取得する処理 void initQuery(@Users EntityManagerFactory emf) { CriteriaBuilder cb = emf.getCriteriaBuilder(); usernameParam = cb.parameter(String.class); query = cb.createQuery(Permission.class); Root p = query.from(Permission.class); query.select(p); query.where(cb.equal(p.get("username"), usernameParam)); }// ログイン時のイベント処理により、DBより認可情報を取得する処理 void onLogin(@Observes @LoggedIn User user) { permissions = new HashSet (userDatabase.createQuery(query) .setParameter(usernameParam, user.getUsername()) .getResultList()); } }
ログイン機能のログイン処理に、上記イベントを発火する処理を実装します。
Package: cdisample.event
Class: Permissions.java
Class: Permissions.java
@SessionScoped @Model public class Login implements Serializable {…略…// ログインイベントの注入@Inject @LoggedIn EventuserLoggedInEvent; …略… public String login() { Listresults = userDatabase.createQuery(query) .setParameter(usernameParam, credentials.getUsername()) .setParameter(passwordParam, credentials.getPassword()) .getResultList(); if (!results.isEmpty()) { user = results.get(0);userLoggedInEvent.fire(user); // ログイン時にイベント発火 } else { FacesMessage msg = new FacesMessage("Invalid Username or Password"); msg.setSeverity(FacesMessage.SEVERITY_ERROR); FacesContext.getCurrentInstance().addMessage(null, msg); return null; } return "documents"; }…略…
上記により、ログイン時にイベントが実行され、セッション中に認可情報が保持され、セッション・スコープで認可情報にアクセスできるようになります。
次に、データアクセスに対して認可処理を実行する汎用的なデコレータを作成します。
Package: cdisample.decorator
Class: DataAccessAuthorizationDecorator.java
Class: DataAccessAuthorizationDecorator.java
package cdisample.decorator; import java.io.Serializable; import java.util.Set; import java.util.logging.Logger; import javax.decorator.Decorator; import javax.decorator.Delegate; import javax.inject.Inject; import cdisample.dao.DataAccess; import cdisample.entity.Permission; import cdisample.exception.NotAuthorizedException; @Decorator public abstract class DataAccessAuthorizationDecoratorimplements DataAccess , Serializable { @Inject @Delegate DataAccess delegate; // デコレータ用のdelgate定義 @Inject Logger log; @Inject Set permissions; // セッションに保持している認可情報をDI// save処理の認可処理 public void save(T object) { authorize("save"); delegate.save(object); }// delete処理の認可処理 public void delete(V object) { authorize("delete"); delegate.delete(object); }// 認可処理 private void authorize(String action) { Class type = delegate.getDataType(); if (permissions.contains(new Permission(action, type.getName()))) { log.info("Authorized for " + action); } else { log.info("Not authorized for " + action); throw new NotAuthorizedException(action); } } }
最後に、beans.xmlにデコレータの定義を追記します。
beans.xml
<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/beans_1_0.xsd"><interceptors><class>cdisample.interceptor.AuthorizationInterceptor</class></interceptors><decorators><class>cdisample.decorator.DataAccessAuthorizationDecorator</class></decorators></beans>
これで、DataAccessインタフェースを実装しているすべてのクラスに対して、認可処理を追加することができます。(今回はDocumentDataAccessしか存在していませんが。)
それでは、認可処理が正常に実行されるか試してみましょう。login.xhtmlを実行してみます。
まずは、duke/welcome1でログインしてDocumentのsave/deleteを実行してみてください。この場合は、どちらも実行可能です。次に、一旦ログアウトし、candy/welcome1でログインしてみてください。Candyには、Permissionsテーブルにsaveの権限しか定義していないため、”delete”操作を実行しようとすると、NotAuthorizedExceptionが投げらて、実行されません。
このように、デコレータを利用することで、各操作への認可処理をメソッド単位に実装できます。またその際に、もともとの業務ロジックには影響を与えません。
以上で、CDIを利用したアプリケーションの作成は終わりです。
まとめ
今回はCDIの機能を利用して、簡単なアプリケーションを作成してみました。CDIを利用することで、コード間の依存性を低減し、疎結合なアプリケーションを作成することができました。また、インターセプターやデコレータなどの機能を利用することで、業務ロジックには影響せずに、認可処理を追加し、処理をセキュアにすることができました。このようにCDIを利用することで、DIだけではなく、Contextに紐付いたDI管理や、AOP的なコーディングなど様々なメリットが享受できることがご理解いただけたかと思います。今後の開発において、ぜひCDIを活用してみてください。
連載の終りに
今回を持ちまして、[連載] WebLogic Server 12cでJava EE 6を動かしてみよう!は一旦終了いたします。ここまで読んでくださり、どうもありがとうございました。連載を通じて、Java EE 6の簡単開発を主眼に置いた機能を体験することができたと思います。これを機会に、皆様が携わる開発において、Java EE 6が一層利用されることを望んでおります。さて、Java EE仕様に目を落とすと、Java EE 7という新規仕様がもうじきリリースされます(2013/05現在)。Java EE 7に関しても、また別の機会に紹介していきたいと思いますので、お楽しみに!