GAE/Jの認証を経た後、YUI2.7.0+Struts1.3をつかってエンティティを表示する

カスタムログインを作ろうの第3回。目標は、

  • GAE/Jの認証を使って、ログインする。
  • ログインしたユーザー(Email)が、アプリケーションで許可した者(アプリケーションでリストを用意)でなければ、認証失敗。

である。
前回は、カスタムログインに使うユーザー・リストをエンティティーとして登録するところ。今回は、まず、Struts1.3ベースでGAE/Jのログインを使う部分を実装してみる。

サンプル

サンプルにアクセスすると、以下のようにログイン画面が現れる。


ログインをすると、前回のサンプルと同じ「エンティティーをリスト表示する」画面に遷移する。


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

プロジェクトの作成

前回のサンプルをコピーして新規のWeb Applicationプロジェクトを作成する。この際、必要に応じてバージョンやアプリケーション名を変える。

Actionクラスの作成

template methodパターンを適用した規定クラスを用意した。このtemplateを使うサブクラスは、execメソッドを(通常のActionクラスのexecuteメソッドの代わりに)使用する。読んでもらえれば分かるが、exec内で、initを必ず実行するようになっていて、initの中にGAE/Jでの認証処理を埋め込んでいる。sendRedirectでStrutsの処理フローを分断するやり方になっているのでスマートとはいいがたいなぁ

BaseAction.java

package test;

import java.io.IOException;

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

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 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();

    	if (request.getUserPrincipal() != null) {
    	} else {
    		response.sendRedirect(userService.createLoginURL(thisURL));
    	}
    }
    
    abstract public ActionForward exec(
        	ActionMapping mapping,
            ActionForm form,
            HttpServletRequest request,
            HttpServletResponse response);
}


次は、エンティティーからデータを取得してクライアントに戻すコード。これは、クライアントのYUI2.7から非同期で呼び出される。前回のサンプルとほぼ同じコードとなるが、上の規定クラスのサブクラスとする。

UserListForwardAction.java

package test;

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

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

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

import test.entities.AppUser;

public class UserListForwardAction extends BaseAction {

    public ActionForward exec(ActionMapping mapping,
        ActionForm form,
        HttpServletRequest request,
        HttpServletResponse response)
    {
    	// AppUserのDAOを生成する
        AppUserDao pdao = new AppUserDao();
    	// AppUserのListを取得する
        List<AppUser> usrList = pdao.getAll();

        StringBuffer sb = new StringBuffer();
        byte[] bStr;

    	// AppUserのListをCSV形式に整形する
        for(AppUser usr : usrList){
            sb.append("\n");
            sb.append(usr.getEmail());
            sb.append(",");
            sb.append(usr.getName());
            sb.append(",");
            sb.append(usr.getAddress());
            sb.append(",");
            sb.append(usr.getRole());
            sb.append("\n");
        }

        try{
        	// CSV形式に整形したListをクライアントにレスポンスする。
        	bStr = sb.toString().getBytes("UTF-8");
            response.setContentType("text/html; charset=UTF-8"); 
            ServletOutputStream outputStream;
            outputStream = response.getOutputStream();
            outputStream.write(bStr);
            outputStream.flush();

        } catch (UnsupportedEncodingException e){
			e.printStackTrace();
        } catch (IOException e) {
			e.printStackTrace();
		}
        return null;
    }
}


最後に、ログイン後に画面を表示するため(だけ)のアクションを作成する。URLにアクセスすると、Javascriptで、このアクションにリダイレクトされるようにする。


UserListAction.java

package test;

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

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

public class UserListAction extends BaseAction {

    public ActionForward exec(ActionMapping mapping,
        ActionForm form,
        HttpServletRequest request,
        HttpServletResponse response)
    {
        return (mapping.findForward("success"));
    }
}

struts-config.xmlの修正

war/WEB-INF/struts-config.xmlは以下のようにする。

<?xml version="1.0" encoding="utf-8" ?>
<!--
    Licensed to the Apache Software Foundation (ASF) under one or more
    contributor license agreements.  See the NOTICE file distributed with
    this work for additional information regarding copyright ownership.
    The ASF licenses this file to You under the Apache License, Version 2.0
    (the "License"); you may not use this file except in compliance with
    the License.  You may obtain a copy of the License at
   
         http://www.apache.org/licenses/LICENSE-2.0
   
    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.
-->

<!DOCTYPE struts-config PUBLIC
          "-//Apache Software Foundation//DTD Struts Configuration 1.3//EN"
          "http://struts.apache.org/dtds/struts-config_1_3.dtd">

<struts-config>

<!-- ================================================ Form Bean Definitions -->
<form-beans>
    <form-bean
		name="dummyForm"
		type="org.apache.struts.action.DynaActionForm" >
	</form-bean>
</form-beans>

<!-- ========================================= Global Exception Definitions -->
    <global-exceptions>
    </global-exceptions>

<!-- =========================================== Global Forward Definitions -->
    <global-forwards>
        <!-- Default forward to "Welcome" action -->
        <forward
            name="welcome"
            path="/index.hml"/>
    </global-forwards>

<!-- =========================================== Action Mapping Definitions -->
    <action-mappings>
		<action path="/forwardText"
			type="test.UserListForwardAction"
			name="dummyForm"
			scope="request">
		</action>
		<action path="/userList"
			type="test.UserListAction"
			scope="request">
			<forward name="success" path="/userList.jsp" />
		</action>
    </action-mappings>

<!-- ======================================== Message Resources Definitions -->
    <message-resources parameter="MessageResources" />

<!-- =============================================== Plug Ins Configuration -->

  <!-- ======================================================= Tiles plugin -->

  <!-- =================================================== Validator plugin -->
   
</struts-config>

画面(userList.jsp)の作成

前回のサンプルで作成したindex.htmlを、userList.jspに改名し、以下を先頭に追加する。

<%@ page contentType="text/html;charset=UTF-8" language="java" %>

index.htmlの作成

index.htmlを作成する。/userList.doにリダイレクトされるようにする。

<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<title>Ajax+Strutsのサンプル</title>
<link rel="shortcut icon" href="../images/egp-favicon.ico" >
<script>
document.location="./userList.do";
</script>
</head> 
 
<body>
</body>
</html>

エンティティーとその初期化

前回のサンプルで作成した

はそのまま用いる。

また、この初期化も同様にContextListenerで行う。

web.xml

変更なし。とりあえず載せておく。
GAE/Jでセッションを有効にした場合、その有効期間は(デフォルトで)1日に設定されている。認証の有効期間を変更したい場合、web.xmlでExpireするまでの時間(分)を設定すればいいと思ってしまうのだが、これではうまくいかない(詳しくは、末尾の注記「セッションタイムアウトと再認証」を参照)。

<?xml version="1.0" encoding="utf-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5">

  <display-name>GAE/J Sample Application</display-name>
  
  	<listener>
  		<listener-class>
   		test.listeners.CreateEntitiesContextListner
  		</listener-class>
  	</listener>

  	<!-- Standard Action Servlet Configuration -->
  	<servlet>
    	<servlet-name>action</servlet-name>
    	<servlet-class>org.apache.struts.action.ActionServlet</servlet-class>
    	<init-param>
      	<param-name>config</param-name>
      	<param-value>/WEB-INF/struts-config.xml</param-value>
    	</init-param>
    	<load-on-startup>1</load-on-startup>
  	</servlet>

  	<!-- Standard Action Servlet Mapping -->
  	<servlet-mapping>
    	<servlet-name>action</servlet-name>
    	<url-pattern>*.do</url-pattern>
  	</servlet-mapping>

  	<!-- The Usual Welcome File List -->
  	<welcome-file-list>
    	<welcome-file>index.html</welcome-file>
  	</welcome-file-list>

</web-app>

テストとデプロイ

ローカルサーバーでテストをしたら、GAE/Jにデプロイする。

注記;セッションタイムアウトと再認証

GAE/Jでセッションを利用すると、データストアに_ah_SESSIONというエンティティーが永続化されることが分かる。web.xmlでセッションタイムアウトを設定すると、このExpire Time(Unixタイムで表現された、アウト時間。GMT±0による)にそれが反映される。
だが、セッションタイムアウトの設定だけでは「セッションタイムアウトしたら再認証」は実現できない。
ブラウザーからCookieを見てみると、JSESSIONID(セッションID)の他に、ACSID(アクセスID)というCookieがあることがわかる。

この生存時間は1日に設定される。セッションタイムアウト後、ブラウザー画面を更新すると、GAE/J側がこのACSIDを読んで「認証ユーザーかのチェック」が行う。このため、ACSIDがExpireするまでは、再認証を聞いてこない。ACSIDを削除すると再認証を聞くようになる