GAE/Jの認証をカスタマイズする

前々回までのサンプルコードを使って、GAE/Jの認証をカスタマイズする(最終回)。

GAE/Jは(今のところ?)認証時に設定できるロール(役割)が

  • 一般ユーザー
  • Admin(管理)ユーザー

のいずれかとなっている。コンシューマー向けのサイトを少人数で開発・運用するのであれば、これで行けると思うのだが、B2Bのサイトであるとか、大人数でメンテナンスをするサイトになると、もう少しきめ細かい認証方式が欲しくなる。GAE上にワークフロー(業務フロー)みたいなものを作る場合にも、必要になることが多いだろう。ディレクトリー・サービスみたいなものが提供されると、こういうことも可能になるのだろうが、「普通のデータとしてユーザー・マスタを持って、それでコントロールする」という「昔ながらの方法」も敷居が低くて捨てがたい。

今回は、前々回までのサンプルをつかって、以下のようなサンプルを作ってみる。

  • GAE/Jに標準で用意されている(簡易な)認証機構をつかう。
  • セッション・タイムアウトで再認証をする。
  • ユーザーマスタをもって、そこに登録されていないユーザーは認証を通過させない。

ユーザーマスタとしては、先のログ「GAE/JでYUI2.7.0+Struts1.3をつかって、データストアからエンティティを取得してDataTableで表示する」で作成した、AppUserエンティティー(AppUser Kind)を使う。

以下の作業は、GAE/JプラグインがインストールされたGanymede(Eclipse3.4)で行った。

プロジェクトの作成

前々回作成したプロジェクトをコピーして新規のプロジェクトを作成する。

AppUserDao.javaの変更

今回のサンプルでは、AppUser KindにGAE/Jの認証を通過してきた、ユーザー(Emailアドレス)が存在するか確認をする。そのためのメソッドgetAppUserByEmail(String email)を追加する。コードの全文を以下に示しておく。

package test;

import java.util.ArrayList;
import java.util.List;

import javax.jdo.PersistenceManager;
import javax.jdo.Query;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.KeyFactory;

import test.entities.AppUser;
import test.entities.RootTO;

/**
 * AppUserエンティティーのData Access Object
 * 
 */
public class AppUserDao{
    private static final Log log = LogFactory.getLog(AppUserDao.class);
    
    /**
     * コンストラクタ
     */
    public AppUserDao() {
    }
    
    @SuppressWarnings("unchecked")
    private List<AppUser> loadAppUser(){

        List<AppUser> users =
            new ArrayList<AppUser>();

        PersistenceManager pm 
                = PMF.get().getPersistenceManager();

        /**
         * AppUserエンティティーに対するクエリの実行
         */
        Query query = pm.newQuery(AppUser.class);
        query.setOrdering("email asc");

        try{
            List<AppUser> list = (List<AppUser>)query.execute();
            for(AppUser usr : list){
                users.add(usr);
            }
        }finally{
            query.closeAll();
            pm.close();
        }
        return users;
    }

    /*
     *  AppUserのリストを返却する。 
     */
    public List<AppUser> getAll() {
        return this.loadAppUser();
    }

    /*
     *  AppUserを登録、更新する。 
     */
    public List<AppUser> setAppUser(AppUser usr) {
        
        log.debug("setAppUser id : " + usr.getNumber());
        
        if(usr.getNumber().intValue() != -1){
            this.updateAppUser(usr);
        }else{
            this.registerAppUser(usr);
        }
        
        return this.loadAppUser();
    }

    /*
     *  AppUserを登録する。 
     */
    private void registerAppUser(AppUser usr) {
        
        PersistenceManager pm 
            = PMF.get().getPersistenceManager();
        long count = 0;
        try{
            pm.currentTransaction().begin();
            RootTO root = pm.getObjectById(RootTO.class, "key");
            count = root.getCount()+1;
            root.setCount(count);
            pm.currentTransaction().commit();
        }finally{
            if(pm.currentTransaction().isActive()){
                // ロールバック
                pm.currentTransaction().rollback();
            }
            pm.close();
        }

        if(count > 0){        
            AppUser storePer =
                new AppUser(usr.getEmail(),
                			usr.getName(),
                			usr.getAddress(),
                			usr.getRole());
         
            KeyFactory.Builder kb = new KeyFactory.Builder(RootTO.class.getSimpleName(), "key");
            kb.addChild(AppUser.class.getSimpleName(), 
                    AppUser.class.getSimpleName()+ new Long(count).toString());
            Key key = kb.getKey();
               
            storePer.setKey(key);
            storePer.setNumber(count);
            
            pm = PMF.get().getPersistenceManager();
            try{
                pm.makePersistent(storePer);
            }finally{
                pm.close();
            }
        }
        return;
    }

    /*
     *  AppUserを更新する。 
     */
    private void updateAppUser(AppUser usr) {
        
        PersistenceManager pm 
            = PMF.get().getPersistenceManager();

        try{
            // キーの生成
            KeyFactory.Builder kb = new KeyFactory.Builder(RootTO.class.getSimpleName(), "key");
            kb.addChild(AppUser.class.getSimpleName(), 
                AppUser.class.getSimpleName()+usr.getNumber().toString());
            Key key = kb.getKey();

            // オブジェクトの取得
            AppUser storePer = pm.getObjectById(AppUser.class, key);
            storePer.setEmail(usr.getEmail());
            storePer.setName(usr.getName());
            storePer.setAddress(usr.getAddress());
            storePer.setRole(usr.getRole());
        }finally{
            pm.close();
        }
        return;
    }

    /**
     *  AppUserを削除する。 
     */
    public List<AppUser> deletePerson(AppUser usr) {
        PersistenceManager pm 
        = PMF.get().getPersistenceManager();

        try{
            // キーの生成
            KeyFactory.Builder kb = new KeyFactory.Builder(RootTO.class.getSimpleName(), "key");
            kb.addChild(AppUser.class.getSimpleName(), 
                AppUser.class.getSimpleName()+usr.getNumber().toString());
            Key key = kb.getKey();

            // オブジェクトの取得
            AppUser storePer = pm.getObjectById(AppUser.class, key);
            pm.deletePersistent(storePer);
        }finally{
            pm.close();
        }
        return this.loadAppUser();
    }
    
    /**
     *  Personを一括削除する。 
     */
    @SuppressWarnings("unchecked")
    public void deleteAll() {
        PersistenceManager pm 
        = PMF.get().getPersistenceManager();

        Query query = pm.newQuery(AppUser.class);
        try{
            pm.currentTransaction().begin();
            List<AppUser> list = (List<AppUser>)query.execute();
            pm.deletePersistentAll(list);
            pm.currentTransaction().commit();
        }finally{
            if(pm.currentTransaction().isActive()){
                // ロールバック
                pm.currentTransaction().rollback();
            }
            query.closeAll();
            pm.close();
        }
        return;
    }

    /**
     * emailでAppUserエンティティーを取得する
     */
    @SuppressWarnings("unchecked")
	public List<AppUser> getAppUserByEmail(String email) {

    	List<AppUser> users =
            new ArrayList<AppUser>();

        PersistenceManager pm 
                = PMF.get().getPersistenceManager();

        /**
         * AppUserエンティティーに対するクエリの実行
         */
        Query query = pm.newQuery(AppUser.class);
        query.setFilter("email == requiredEmail");
        query.setOrdering("email asc");
        query.declareParameters("String requiredEmail");

        try{
            List<AppUser> list = (List<AppUser>)query.execute(email);
            for(AppUser usr : list){
                users.add(usr);
            }
        }finally{
            query.closeAll();
            pm.close();
        }
        return users;
    }
    
}

BaseAction.javaの変更

Actionの基底クラスを以下のように変更する。前々回のサンプルでは、ACSID(アクセスID)というCookieを上書きして再認証をさせたが、今回はセッション・タイムアウトで再認証をさせるようにする。また、GAE/Jの認証を通過したら、AppUsersエンティティーを、Emailアドレスで検索して存在確認をする。この際、存在しなかったら、GAEで用意している認証画面に戻してしまう。

package test;

import java.io.IOException;
import java.util.List;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.apache.struts.action.Action;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;

import test.entities.AppUser;

import com.google.appengine.api.users.User;
import com.google.appengine.api.users.UserService;
import com.google.appengine.api.users.UserServiceFactory;

public abstract class BaseAction extends Action {

    public ActionForward execute(
    	ActionMapping mapping,
        ActionForm form,
        HttpServletRequest request,
        HttpServletResponse response) throws IOException
    {
    	init(mapping, form, request, response);
    	return exec(mapping, form, request, response);
    	
    }

    public void init(
    		ActionMapping mapping,
            ActionForm form,
            HttpServletRequest request,
            HttpServletResponse response) throws IOException{
    		
    	UserService userService = UserServiceFactory.getUserService();
    	String thisURL = request.getRequestURI();

        HttpSession session = request.getSession(true);

        if (session.isNew()) {
    		response.sendRedirect(userService.createLoginURL(thisURL));
    	} else{
    		// ログインしたらここに戻る。           	
    		User attemptingUser = userService.getCurrentUser();
           	AppUserDao auo = new AppUserDao();
           	List<AppUser> users = auo.getAppUserByEmail(attemptingUser.getEmail());
           	if(users.size()==0)
           		response.sendRedirect(userService.createLoginURL(thisURL));
    	}
    }
    
    abstract public ActionForward exec(
        	ActionMapping mapping,
            ActionForm form,
            HttpServletRequest request,
            HttpServletResponse response);
}

web.xml

以下を追加して、セッションタイムアウトを設定する。(以下の例では5分でタイムアウト

  	<!-- session configuration -->
 	<session-config>
	  	<session-timeout>5</session-timeout>
	</session-config>

テストとデプロイ

AppUserDao.javaに新しいクエリを作ったので、ローカル環境で実行してIndexを作成する。Indexはwar/WEB-INF/appengine-generated/datastore-indexes-auto.xmlに登録される。そうしたら、GAE/Jにデプロイする。
ブラウザーで、GAE/Jが発行した、JSESSENIDとACSIDの2つのCookieを削除して、ブラウザーを再起動する。URLにアクセスすると、認証を聞いてくるが、メイルの設定などで「ログイン状態を保持する」としていると、ユーザーIDが入った格好で、以下のような画面がでる。

ここで、「別のユーザーとしてログインする」を選ぶと、以下の画面に変わる。AppUserに登録したEmail、しないEmailアドレスでテストする。