2011/04/24

GAE/J の OAuth 認証を試してみた。その2

その1で一応 OAuth 認証は出来ているんだけど 公開鍵を使った認証ができるのでこれを試してみる。

最初、公開鍵を使うと consumer_key と consumer_secret で 2-legged で認証できると思っていたのだがどうやら出来ないならしい。
そもそも API がユーザを取得するようになっているので そういう物なんだろう。

鍵の準備

まず公開鍵を用意する。 現状、使えるのは RSA だけ。

openssl で鍵を作る。

  • http://code.google.com/intl/ja/apis/gdata/docs/auth/authsub.html#Registered
$ openssl req -x509 -nodes -days 365 -newkey rsa:1024 -sha1 \
 -subj '/C=US/ST=CA/L=test/CN=wsjs-gae.appspot.com' \
 -keyout wsjs-gae_pri.pem -out wsjs-gae_pub.pem
subject は適当に修正が必要。

java は pkcs8 形式しか受け付けないので秘密鍵を変換する。

$ openssl pkcs8 -in wsjs-gae_pri.pem -out wsjs-gae_pri.pk8 -topk8 -nocrypt

サーバに公開鍵を登録

管理画面から公開鍵と登録する。

  • https://www.google.com/accounts/UpdateDomain

サーバ側の準備はこれだけ。

クライアント側に秘密鍵を指定

クライアント側で秘密鍵を使うコードは既にその1のコードに入っている。

	protected OAuthAccessor getOAuthAccessor() throws Exception {
		OAuthServiceProvider provider = 
			new OAuthServiceProvider(reqUrl, authUrl, accessUrl);
		OAuthConsumer consumer = 
			new OAuthConsumer(null, domain, consumerSecret, provider);

		String key = props.getProperty(PRIVATE_KEY);
		if (key != null) {
			EncodedKeySpec privKeySpec = new PKCS8EncodedKeySpec(
				OAuthSignatureMethod.decodeBase64(key)
			);
			KeyFactory keyFactory = KeyFactory.getInstance("RSA");
			PrivateKey privateKey = keyFactory.generatePrivate(privKeySpec);
			consumer.setProperty(RSA_SHA1.PRIVATE_KEY, privateKey);
			consumer.setProperty(OAuth.OAUTH_SIGNATURE_METHOD, OAuth.RSA_SHA1);
		}

		return new OAuthAccessor(consumer);
	}

プロパティファイルに private_key を指定すると時動的に RSA_SHA1 モードに切り替わるようになっている。

鍵の準備で作成した pkcs8 の秘密鍵を設定する。

  • oauth.props:
app_id=wsjs-gae
consumer_secret=XXXXXXXXXXXXXXXXXXXXXXX
private_key=\
MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBALFqa/6eR/Eb8Jf8\
ftkuszfdjyybKeZXFBbnJejDjYc40wA9+kaEZbBwrFNCPFcRyF9+CCGbfkVKKvxH\
hpe+81xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxghxtTb\
VnKfhexxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxZfL1C4\
OKEMI/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxJ/Ngts\
EaP0YgxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxBi9jlS\
i6vZN0xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxQG7eqM\
UN4pjJxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxi8zjWZ\
3vt8Epxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx0cEgB0\
8iu3AExxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxR1OYlW\
SmWJbbxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxexkVe+\
GVe9XRTSzVU5zIk70oTocR9qFwUZhMk1jzcNuNvTVZbPr7mPXhNhAkEArFu3d3eK\
csyGYG5+4e6QkYWkjce/xTdmt0YBdOLzyxMPcVZpUPxN75TBBDiLmpNAr2kRkWw5\
0VlVlA/KMbN+qQ==

これで その1 と同じ用に request、承認、access、目的URL と進めば RSA_SHA1 で 3-legged oauth で認証を行う事ができる。

2-legged 出来ないんでセキュリティが強化されただけで あんまり面白く無い。
しかも公開鍵登録しても HMAC-SHA1 でアクセス出来ちゃうし。


2011/04/17

GAE/J の OAuth 認証を試してみた。その1

CUI のクライアントから GAE にアクセスするバッチ的な プログラムの為に OAuth 認証を調べてみた。

GAE 側は極めて単純で

OAuthService oauthService = OAuthServiceFactory.getOAuthService();
User user = oauthService.getCurrentUser();
これだけで OAuth 認証されたユーザが取得できる。

問題はクライアント側で OAuth の 3-legged プロトコルを ちゃんと実装しなければいけない。

とっかかりはこの辺りから...

  • http://code.google.com/intl/ja/appengine/docs/java/oauth/

で、こっちにリンクされてて思いっきりのプロトコルの説明...

  • http://code.google.com/intl/ja/apis/accounts/docs/OAuth_ref.html

最初、このプロトコルを自力で実装してたのだが暗号化の辺りに来て 「こんなの普通ライブラリあるよね?」と気付いて探したら出て来た。

  • http://code.google.com/p/oauth/

こいつがまた使い方が難しくて良く分からんのだが、まぁ部品は揃ったので 進めて見る。



サーバ側の準備

まず最初に必要なのが GAE サーバの登録。
  • https://www.google.com/accounts/ManageDomains
ここにアクセスする段階で OAuth の認証されるユーザでログインが必要。

ドメイン名に .appspot.com を指定して [add domain] をクリックする。 ※普通は https にして置いた方が良いと思う。

するとサイトの所有権を確認される。

今回は meta ダグをトップページに張り付ける方法を選択した。

meta ダグを張り付けた後で [確認] をクリックすると ドメインが追加される。

ドメイン名をクリックするとドメインの管理画面に移動するので URL を入力して [Save] する。

これて OAuth の Key と Secret が登録され OAuth が使えるようになる。

テスト用のサーバ側コードを用意する。

  • https://wsjs-gae.appspot.com/test/oauth.ssjs:
var OAuthServiceFactory = Packages.com.google.appengine.api.oauth.OAuthServiceFactory; function doGet(req, res) { var oauthService = OAuthServiceFactory.getOAuthService(); var text = "user:"+oauthService.getCurrentUser()+"\n" +"admin:"+oauthService.isUserAdmin()+"\n" ; res.writer.write(text); }

これてサーバ側の準備完了。

クライアントの実装

先に見付けたサイトからライブラリの jar を落す。

  • http://code.google.com/p/oauth/downloads/list
の ApacheJMeter_oauth-v2.jar を選択。

これだけではだめなので SVN でソースは一式落したほうが良い。

svn co http://oauth.googlecode.com/svn/code/java/

サンプルコード jmeter/example/command-line を参考にクライアント側 のコードを書いてみる。

  • OAuthTestClient.java:
package org.kotemaru.test; import java.io.*; import java.net.*; import java.util.*; import java.security.KeyFactory; import java.security.PrivateKey; import java.security.PublicKey; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.security.spec.EncodedKeySpec; import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.X509EncodedKeySpec; import net.oauth.client.OAuthClient; import net.oauth.OAuthServiceProvider; import net.oauth.OAuthConsumer; import net.oauth.OAuthAccessor; import net.oauth.OAuth; import net.oauth.ParameterStyle; import net.oauth.OAuthMessage; import net.oauth.OAuthException; import net.oauth.http.HttpClient; import net.oauth.signature.*; import net.oauth.client.httpclient3.HttpClient3; //import net.oauth.client.URLConnectionClient; public class OAuthTestClient { public static final String APP_ID = "app_id"; public static final String CONSUMER_SECRET = "consumer_secret"; public static final String OAUTH_TOKEN = "oauth_token"; public static final String OAUTH_TOKEN_SECRET = "oauth_token_secret"; public static final String PRIVATE_KEY = "private_key"; public static final String APP_NAME = "application_name"; public static final String GET_REQ_TOKEN = "/_ah/OAuthGetRequestToken"; public static final String GET_AUTH_TOKEN = "/_ah/OAuthAuthorizeToken"; public static final String GET_ACCESS_TOKEN = "/_ah/OAuthGetAccessToken"; public static final String HTTPS = "https://"; public static final String APPSPOT_COM = "appspot.com"; private Properties props; private String appId; private String consumerSecret; private String domain; private String reqUrl ; private String authUrl ; private String accessUrl ; private OAuthAccessor accessor; private OAuthClient client; public OAuthTestClient(Properties p) throws Exception { props = p; appId = props.getProperty(APP_ID); consumerSecret = props.getProperty(CONSUMER_SECRET); domain = appId + "." + APPSPOT_COM; reqUrl = HTTPS + domain + GET_REQ_TOKEN; authUrl = HTTPS + domain + GET_AUTH_TOKEN; accessUrl = HTTPS + domain + GET_ACCESS_TOKEN; accessor = getOAuthAccessor(); client = getOAuthClient(); } protected OAuthAccessor getOAuthAccessor() throws Exception { OAuthServiceProvider provider = new OAuthServiceProvider(reqUrl, authUrl, accessUrl); OAuthConsumer consumer = new OAuthConsumer(null, domain, consumerSecret, provider); String key = props.getProperty(PRIVATE_KEY); if (key != null) { EncodedKeySpec privKeySpec = new PKCS8EncodedKeySpec( OAuthSignatureMethod.decodeBase64(key) ); KeyFactory keyFactory = KeyFactory.getInstance("RSA"); PrivateKey privateKey = keyFactory.generatePrivate(privKeySpec); consumer.setProperty(RSA_SHA1.PRIVATE_KEY, privateKey); consumer.setProperty(OAuth.OAUTH_SIGNATURE_METHOD, OAuth.RSA_SHA1); } return new OAuthAccessor(consumer); } protected OAuthClient getOAuthClient() { OAuthClient client = new OAuthClient(new HttpClient3()); client.getHttpParameters().put(HttpClient.FOLLOW_REDIRECTS, Boolean.TRUE); return client; } protected OAuthMessage send(String url, String token) throws Exception { List<OAuth.Parameter> params = new ArrayList<OAuth.Parameter>(); params.add(new OAuth.Parameter(APP_NAME, appId)); params.add(new OAuth.Parameter(OAUTH_TOKEN, token)); OAuthMessage response = client.invoke(accessor, "GET", url, params); return response; } public String getAuthUrl() throws Exception { client.getRequestToken(accessor); OAuthMessage response = send(authUrl, accessor.requestToken); props.setProperty(OAUTH_TOKEN, accessor.requestToken); props.setProperty(OAUTH_TOKEN_SECRET, accessor.tokenSecret); return response.URL; } public void access() throws Exception { accessor.tokenSecret = props.getProperty(OAUTH_TOKEN_SECRET); OAuthMessage response = send(accessUrl, props.getProperty(OAUTH_TOKEN)); props.setProperty(OAUTH_TOKEN, response.getParameter(OAUTH_TOKEN)); props.setProperty(OAUTH_TOKEN_SECRET, response.getParameter(OAUTH_TOKEN_SECRET)); } public OAuthMessage request(String url) throws Exception { accessor.consumer.setProperty(OAuthClient.PARAMETER_STYLE, ParameterStyle.AUTHORIZATION_HEADER); accessor.tokenSecret = props.getProperty(OAUTH_TOKEN_SECRET); List<OAuth.Parameter> params = new ArrayList<OAuth.Parameter>(); params.add(new OAuth.Parameter(APP_NAME, appId)); params.add(new OAuth.Parameter(OAUTH_TOKEN, props.getProperty(OAUTH_TOKEN))); OAuthMessage response = client.invoke(accessor, "GET", url, params); return response; } //------------------------------------------------------------------ public static Properties loadProps(String name) throws IOException { Properties props = new Properties(); InputStream in = new FileInputStream(name); try { props.load(in); } finally { in.close(); } return props; } public static void saveProps(String name, Properties props) throws IOException { OutputStream out = new FileOutputStream(name); try { props.store(out, ""); } finally { out.close(); } } public static void main(String[] args) throws Exception { Properties props = loadProps(args[0]); System.out.println(props); OAuthTestClient test = new OAuthTestClient(props); BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); System.out.print("\n> "); String line = reader.readLine(); while (line != null) { line = line.trim(); if ("request".equals(line)) { String url = test.getAuthUrl(); System.out.println("Auth URL:\n"+url); saveProps(args[0], props); } else if ("access".equals(line)) { test.access(); saveProps(args[0], props); System.out.println("Get access token."); } else { OAuthMessage response = test.request(line); System.out.println(response.readBodyAsString()); } System.out.print("\n> "); line = reader.readLine(); } } }

通常の OAuth クライアントなので GAE としての特別な部分は以下の3行だけ。

public static final String GET_REQ_TOKEN = "/_ah/OAuthGetRequestToken"; public static final String GET_AUTH_TOKEN = "/_ah/OAuthAuthorizeToken"; public static final String GET_ACCESS_TOKEN = "/_ah/OAuthGetAccessToken";

使い方は最初に目的サーバの app-id と OAuth secret を持つ プロパティファイルを作成する。

  • oauth.props:
app_id=<GAEのAPP-ID> consumer_secret=XXXXXXXXXXXXXXXXXXXX

oauth.props を引数に渡して org.kotemaru.test.OAuthTestClient を起動するとプロンプトが出るのでコマンドを入力。

$ java -classpath $CP org.kotemaru.test.OAuthTestClient oauth.props
  • トークン取得:
> request
Auth URL:
https://www.google.com/accounts/OAuthAuthorizeToken?application_name=wsjs-gae&oauth_token=4%2FtdHeN0V_wnRNUBbLJYpgiU0GRUUJ&oauth_consumer_key=wsjs-gae.appspot.com&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1302937883&oauth_nonce=1302937883290438000&oauth_version=1.0&oauth_signature=XXXXXXXXXXXXXXXXXXX%3D&scope=appengine&hd=default

このURLをブラウザで開いて登録ユーザでログインしアクセス許可を行う。

  • アクセストークンの取得:
> access Get access token.

アクセストークンは oauth.props に書き戻される。

  • 目的URLにアクセス:
> https://wsjs-gae.appspot.com/test/oauth.ssjs
user:xxxxx@gmail.com
admin:true

ちゃんと認証できました。

なんだかえらい大変でした。 趣味で使う分には固定パスワードで十分だと思います。 業務用だと管理ツールで必須とか言われるかもねー。

コード一式(Eclipseプロジェクト):
oauth-client.zip

参考にしたページ:
http://blog.smartnetwork.co.jp/staff/node/53


2011/03/06

GAE/Jのspinup

spin-up時間に Servlet と Filter でどれくらい違いがでるのか ちょっと気になったので調べてみた。

いずれも中身は空っぽの NullServlet と NullFilter を用意して 実機で動かした。

結果:

  • NullServlet
    1241ms 1260cpu_ms
    1035ms 1306cpu_ms
    1035ms 1306cpu_ms
    1032ms 1516cpu_ms
    1253ms 1400cpu_ms
    1200ms 1540cpu_ms
    

  • NullFilter
    1175ms 1306cpu_ms 
     900ms 1423cpu_ms 
     910ms 1190cpu_ms 
    1442ms 1353cpu_ms 
    1152ms 1610cpu_ms 
     982ms 1190cpu_ms 
    

    うーん、気持早くなってる。
    100~200ms くらいだろうか。
    通常のアプリの spin-up 時間は 3000~4000ms くらいかかると思われるので 誤差の範囲と言えなくも無い。

    まぁキチキチを目指す場合は最初から Filter にしとけって事くらいかな。
    後から変えようとすると結構面倒だし。

    以下、ソース。

  • NullServlet.java: package org.kotemaru.test; import java.io.*; import java.net.*; import javax.servlet.*; import javax.servlet.http.*; public class NullServlet extends HttpServlet { public void init(ServletConfig config) throws ServletException { System.out.println("Servlet init"); } public void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // nop. } }

  • NullFilter.java: package org.kotemaru.test; import java.io.*; import java.net.*; import javax.servlet.*; public class NullFilter implements Filter { public void init(FilterConfig config) throws ServletException { System.out.println("Filter init"); } public void destroy() { // nop. } public void doFilter(ServletRequest _req, ServletResponse _res, FilterChain chain) throws java.io.IOException, ServletException { // nop. } }

    関連記事:


  • 2011/02/27

    GAE/Jのspinupとmemcache:クラスのキャッシュ化

    リソースのmemcache化で効果が有ったのでクラスのロードでも 同じ効果が無いか試して見ることにした。

    やり方としては自前のクラスローダからServletを生成して 以下は自前のクラスローダからクラスを読み込ませる。 memcacheにはクラスを byte[] で保存して置く。

    但し、java.* や com.google.* のパッケージは自前のクラスローダ からの読み込みを許可されないので避ける必要がある。

    実験結果:

  • 標準
     3602ms 2290cpu_ms 
    38834ms 2290cpu_ms 
     1442ms 1586cpu_ms 
     7532ms 2163cpu_ms 
    17550ms 2364cpu_ms 
    13556ms 2273cpu_ms 
     3111ms 2200cpu_ms 
     1224ms 1633cpu_ms 
     5620ms 2346cpu_ms
     6169ms 2530cpu_ms 
    

  • memcache付き自前クラスローダ
     8000ms 4490cpu_ms
     3946ms 4435cpu_ms
    21187ms 4362cpu_ms
     7975ms 4215cpu_ms
     6042ms 3978cpu_ms
     3722ms 3996cpu_ms
     5941ms 4656cpu_ms
     7032ms 4362cpu_ms 
    13031ms 4106cpu_ms 
    14204ms 4070cpu_ms 
    15714ms 4307cpu_ms 
    

    う、変わって無いと言うか逆に遅くなってる。

    コード自体かなり余分な処理が処理が入るのでCPU時間が 増えるのは仕方無いが I/O 待ちを減らすメリットは 出ていないみたいだ。

    さすがにクラスロードに関しては GAE 側に何らかのチューニング が入っていると言うことだろうか。
    まぁそうで有っても全然不思議じゃ無い。

    効果無かったけど一応実験ソースは置いておく。

  • MemcacheClassLoader.java: memcacheを使うクラスローダ package org.kotemaru.test; import java.io.*; import com.google.appengine.api.memcache.*; public class MemcacheClassLoader extends ClassLoader { private static final String NS_MEMCACHE = "MemcacheClassLoader"; private static MemcacheService memcache = MemcacheServiceFactory.getMemcacheService(NS_MEMCACHE); public MemcacheClassLoader(ClassLoader parent) { super(parent); } protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { if ( name.startsWith("java") || name.startsWith("com.google.") || name.startsWith("org.xml.sax.") || name.startsWith("org.w3c.") || name.startsWith("sun.") || name.startsWith("org.mortbay.") ) { try { return getParent().loadClass(name); } catch (ClassNotFoundException e) { return loadClass0(name, resolve); } } return loadClass0(name, resolve); } private Class loadClass0(String name, boolean resolve) throws ClassNotFoundException { Class clazz = findLoadedClass(name); if (clazz != null) return clazz; clazz = findClass(name); if (resolve) resolveClass(clazz); return clazz; } protected Class findClass(String name) throws ClassNotFoundException { try { String path = name.replace('.','/')+".class"; //System.out.println("->"+path); InputStream in = null; try { byte[] data = (byte[]) memcache.get(name); if (data == null) { in = getParent().getResourceAsStream(path); data = readBytes(in); memcache.put(name, data); } Class clazz = defineClass(name, data, 0, data.length); return clazz; } finally { if (in != null) in.close(); } } catch (Throwable t) { throw new ClassNotFoundException(name, t); } } private static byte[] readBytes(InputStream in) throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); byte[] buff = new byte[4096]; int n; while ((n=in.read(buff)) > 0) { out.write(buff, 0, n); } return out.toByteArray(); } }

  • MemcacheCLServlet.java: MemcacheClassLoaderを使わせる為のラッパーServlet package org.kotemaru.test; import java.io.*; import java.net.*; import javax.servlet.*; import javax.servlet.http.*; public class MemcacheCLServlet extends HttpServlet { private static final String ORIGIN = "org.kotemaru.test.Test2Servlet"; private HttpServlet origin; public void init(ServletConfig config) throws ServletException { try { System.out.println("Initialize MemcacheCLServlet."); MemcacheClassLoader ccl = new MemcacheClassLoader(this.getClass().getClassLoader()); origin = (HttpServlet) Class.forName(ORIGIN, true, ccl).newInstance(); origin.init(config); } catch (Exception e) { throw new ServletException(e); } } public void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { origin.service(request, response); } }

  • Test2Servlet.java: ターゲットのServlet package org.kotemaru.test; import java.io.*; import java.net.*; import javax.servlet.*; import javax.servlet.http.*; import org.mozilla.javascript.*; public class Test2Servlet extends HttpServlet { public void init(ServletConfig config) throws ServletException { try { System.out.println("start init."); Context cx = Context.enter(); try { Scriptable scope = cx.initStandardObjects(); String src = "var x=1;"; Script script = cx.compileString(src, "test.js", 1, null); script.exec(cx, scope); } catch (RuntimeException e) { e.printStackTrace(); throw e; } finally { Context.exit(); } } catch (Exception e) { e.printStackTrace(); throw new ServletException(e); } } public void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.getWriter().write("test"); return; } }

    関連記事:


  • 2011/02/20

    GAE/Jのspinupとmemcache その3

    感覚的な物だけでは何なので I/O -> memcacheの効果を検証してみる為にした。
    init()の中で 85bytes のリソースを5つ読み込むだけのServletで試した。
    テスト用に用意した環境が撃重で凄い結果になった。

    - memcache無し

    実行時間 CPU時間
    17238ms  1778cpu_ms 
    24499ms  1832cpu_ms 
    17569ms  1870cpu_ms 
    11694ms  1777cpu_ms 
    

    - memcache有り

     6167ms  2015cpu_ms 
     9290ms  1905cpu_ms 
     6573ms  1833cpu_ms 
     1918ms  1376cpu_ms 
    

    CPU時間は変わらないのに実行時間は memcache有りの場合、半分以下になっている。

    たまたま凄く重い環境に当たったのだと思うが実機での検証結果なので 何もしないServletが24秒とか実際に起こり得るらしい。
    むしろ貴重な情報が取れてラッキーだったかもしれない。

    spinup実時間のバラツキに対しては かなり効果が見込めそうなので試してみて欲しい。
    できれば結果をコメントしてもらえると情報が蓄積できるのでお願いしまふ。


    テストに使ったServlet:

    package org.kotemaru.test; import java.io.*; import java.net.*; import javax.servlet.*; import javax.servlet.http.*; import com.google.appengine.api.memcache.*; public class TestServlet extends HttpServlet { private static final String NS_MEMCACHE = "Test"; private static MemcacheService memcache = MemcacheServiceFactory.getMemcacheService(NS_MEMCACHE); public void init(ServletConfig config) throws ServletException { try { System.out.println("start init."); /* getResource(TestServlet.class, "test1.properties"); getResource(TestServlet.class, "test2.properties"); getResource(TestServlet.class, "test3.properties"); getResource(TestServlet.class, "test4.properties"); getResource(TestServlet.class, "test5.properties"); */ getResourceWithCache(TestServlet.class, "test1.properties"); getResourceWithCache(TestServlet.class, "test2.properties"); getResourceWithCache(TestServlet.class, "test3.properties"); getResourceWithCache(TestServlet.class, "test4.properties"); getResourceWithCache(TestServlet.class, "test5.properties"); } catch (Exception e) { e.printStackTrace(); throw new ServletException(e); } } public void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.getWriter().write("test"); return; } public static String getResourceWithCache(Class clazz, String name) throws IOException { String val = (String) memcache.get(name); if (val != null) { val = getResource(clazz, name); memcache.put(name, val); } return val; } public static String getResource(Class clazz, String name) throws IOException { InputStream in = clazz.getResourceAsStream(name); try { return readerToString(new InputStreamReader(in, "UTF-8")); } finally { in.close(); } } public static String readerToString(Reader reader) throws IOException { StringBuffer sbuf = new StringBuffer(); int n = 0; char[] buff = new char[4096]; while ((n=reader.read(buff)) > 0) { sbuf.append(buff, 0, n); } return sbuf.toString(); } }

    関連記事:


    2011/02/13

    GAE/Jのspinupとmemcache その2

    Google の中の人と話す機会があったのsinup/downについて聞いてきた。
    でいくつか思い違いが有ったのでメモっとく。
    • spindownは時間じゃなくてリソース競合で起こる
    他のアプリが使いたいリソースを開放させる為に発生するらしい。
    それが無ければずっとインスタンスは生きているとの事。
    実際試しているとアクセスが無いのに30分以上 spindown が起こらない事がある。
    • sipnupで遅い時が有るのは主にI/O待ちが発生している。
    I/Oリソースを共有している他のアプリと競合した場合に待たされているらしい。
    したがってCPUタイムと無関係にバラツキの有る処理時間になるとの事。


    この話からするととにかく I/O は memcache にキャッシュすれば バラツキを押えて CPU 時間前後で安定する事になる。

    それを前提に現在実験中なのだがログからはコード改修の結果なのか 単に環境の負荷が下がっているだけなのかの区別が付かないので結論が出ない。

    少し、長い目で見るしか無いねこの件は。

    ちなみに今のところはっきり効果を感じたのはクラスパス内の リソースファイルの読み込みを memcache 化した場合。 複数有ったため 1000ms くらいがっつり減った。



    GAEのspinupとmemcache の記事で変化が無かったのはデシリアライズで失敗してた為、 memcache が効いていなかった。
    これはクラスローダの問題が絡んでいたのだがこっちはこっちで根が深いのでまた別の機会に。

    関連記事:


    2011/01/23

    GAE から Apps のスプレッドシートにアクセスしてみた。

    gdata API を使って GAE から Apps のスプレッドシートを参照してみた。

    Apps のスプレッドシートがそもそも何かって言うとエクセルの Webアプリ版だと思えば良い。 もちろん機能的にはエクセルに及ばないが複数の人が同時に編集出来るなど WEbアプリならではの機能も有る。

    エクセルのテンプレートで社内文書を提出させる会社は多いと思うが Appsのスプレッドシート -> GAE の流れとすればクラウド上で完結することができる。 FORMと違って提出文書はそのまま残るのでサーバ側は処理結果だけ持っていれば 良く実装量も減るはず。

    と言うわけでいつもの用に WSJS でお試し。

    まずは gdata API のライブラリを落してくる。

  • http://code.google.com/intl/ja/apis/apps/libraries_and_samples.html

    で、jar を自前クラスローダから読み込まそうとしただがエラーになった。

    java.lang.NullPointerException
    com.google.appengine.runtime.Request.process-8c1503d338b4a612(Request.java)
    java.io.ByteArrayInputStream.(ByteArrayInputStream.java:106)
    org.kotemaru.wsjs.RepositoryClassLoader.getJarStream(RepositoryClassLoader.java:90)
    org.kotemaru.wsjs.RepositoryClassLoader.findClassForJarFile(RepositoryClassLoader.java:62)
    org.kotemaru.wsjs.RepositoryClassLoader.findClass(RepositoryClassLoader.java:33)
    java.lang.ClassLoader.loadClass(ClassLoader.java:114)
              :
    

    com.google.** のパッケージはシステムのクラスローダしか受け付け無いらしい。

    あきらめて WEB-INF/lib の下に以下の jar をコピーして Upload しなおし。

     gdata-core-1.0.jar
     gdata-client-meta-1.0.jar
     gdata-client-1.0.jar
     gdata-spreadsheet-meta-3.0.jar
     gdata-spreadsheet-3.0.jar
     google-collect-1.0-rc1.jar
     jsr305.jar
    

    スプレッドシートにアクセスする為には最低限、以下の情報が必要。

    • スプレッドシートの名前
    • アクセス権の有るユーザの名前
    • アクセス権の有るユーザのパスワード
    ユーザ/パスワードは OAuth でも行けるかもしれない。

    スプレッドシート名/ユーザ/パスワード を受け取って シートの内容をCSVで吐き出すサンプルを書いてみる。

    /* 自前クラスローダはダメっぽいのでコメントアウト __ENV__.addClassPath("gdata-core-1.0.jar"); __ENV__.addClassPath("gdata-client-meta-1.0.jar"); __ENV__.addClassPath("gdata-client-1.0.jar"); __ENV__.addClassPath("gdata-spreadsheet-meta-3.0.jar"); __ENV__.addClassPath("gdata-spreadsheet-3.0.jar"); __ENV__.addClassPath("google-collect-1.0-rc1.jar "); __ENV__.addClassPath("jsr305.jar"); */ // imports. var FeedURLFactory = Packages.com.google.gdata.client.spreadsheet.FeedURLFactory; var ListQuery = Packages.com.google.gdata.client.spreadsheet.ListQuery; var SpreadsheetQuery = Packages.com.google.gdata.client.spreadsheet.SpreadsheetQuery; var SpreadsheetService = Packages.com.google.gdata.client.spreadsheet.SpreadsheetService; var CustomElementCollection = Packages.com.google.gdata.data.spreadsheet.CustomElementCollection; var ListEntry = Packages.com.google.gdata.data.spreadsheet.ListEntry; var ListFeed = Packages.com.google.gdata.data.spreadsheet.ListFeed; var SpreadsheetEntry = Packages.com.google.gdata.data.spreadsheet.SpreadsheetEntry; var SpreadsheetFeed = Packages.com.google.gdata.data.spreadsheet.SpreadsheetFeed; var WorksheetEntry = Packages.com.google.gdata.data.spreadsheet.WorksheetEntry; function doGet(req, res) { var html = <html> <head> </head> <body> <form name="form1" method="POST"> Spread sheet:<input type="text" name="spreadsheet"/><br/> <!--Word sheed:<input type="text" name="worksheet"/><br/>--> User:<input type="text" name="username"/><br/> Pass:<input type="password" name="password"/><br/> <input type="submit" value="送信"/> </form> <p/> </body> </html>; res.setContentType("text/html; charset=utf-8"); res.writer.write(html); } function doPost(req, res) { var spreadsheet = req.getParameter("spreadsheet"); //var worksheet = req.getParameter("worksheet"); var username = req.getParameter("username"); var password = req.getParameter("password"); // サービス開始 var service = new SpreadsheetService("Sample"); service.setUserCredentials(username, password); // spread-sheet 取得 var urlFactory = FeedURLFactory.getDefault(); var spreadsheetQuery = new SpreadsheetQuery(urlFactory.getSpreadsheetsFeedUrl()); spreadsheetQuery.setTitleQuery(spreadsheet); // 検索である事に注意。複数取れちゃうよ。 var spreadsheetFeed = service.query(spreadsheetQuery, SpreadsheetFeed); var spreadsheetEntry = spreadsheetFeed.getEntries().get(0); // work-sheet 取得。全部取るなら getWorksheets()。 worksheetEntry = spreadsheetEntry.getDefaultWorksheet(); // work-sheetの中身取得。 var listQuery = new ListQuery(worksheetEntry.getListFeedUrl()); var listFeed = service.query(listQuery, ListFeed); var list = listFeed.getEntries(); // 項目名一覧取得。 var tags = listFeed.getEntries().get(0).getCustomElements().getTags(); // データ出力。 res.setContentType("text/plain; charset=utf-8"); var writer = res.writer; var ite = tags.iterator(); while(ite.hasNext()){ var name = ite.next(); writer.write(name+","); } writer.write("\n"); for (var i=0; i<list.size(); i++) { var elements = list.get(i).getCustomElements(); var ite = tags.iterator(); while(ite.hasNext()){ var name = ite.next(); var val = elements.getValue(name); writer.write(val+","); } writer.write("\n"); } }

    まず取得対象のスプレッドシートを "test" と言う名前で作成。

    サンプルページにアクセスして スプレッドシート名/ユーザ/パスワード を入力。

    スプレッドシートの内容が CSV で取れている。

    スプレッドシートへの出力も可能なようなので結果ページをメールで送信して ワークフローとかも作れちゃいそうだね。

    参考ページ: http://www.atmarkit.co.jp/fwcr/rensai2/spreadsheetsapi01/02.html


  • 2011/01/08

    GAE/J の OpenID を使ってみた。

    参考にしたページはこの辺り
  • http://code.google.com/p/appengine-openid-test/
  • http://d.hatena.ne.jp/knj77/20100612/1276328107
  • http://d.hatena.ne.jp/hidemon/20100521/1274472966
  • http://d.hatena.ne.jp/int128/20100525/1274809086

    openid4java で OpenID は実装した事があるが GAE/J の API では試した事が無かったのでいつもの様に WSJS でお手軽に 試してみた。

    まずは認証の必要なページの作成。

  • 未ログインの場合は OpenID を入力する自前のページに飛ばす。

    var UserServiceFactory = Packages.com.google.appengine.api.users.UserServiceFactory; function doGet(req, res) { var userService = UserServiceFactory.getUserService(); var user = userService.getCurrentUser(); if (user == null) { res.sendRedirect("/openid/openid.ssjs?continue="+req.getRequestURI()); } res.writer.write("This id target page. UserName="+user); }

    web.xml に <security-constraint> を設定しておけば自動で /_ah/login_required に飛ばしてくれるみたいです。 /_ah/login_required には自前の Servlet が必要。

    OpenID の入力画面とプロバイダのログイン画面への転送処理。

  • 画面は流用なので無意味に凝っている。
  • continue パラメータをセッション変数で引き継いでいる事に注意。 var UserServiceFactory = Packages.com.google.appengine.api.users.UserServiceFactory; // OpenIDの入力画面 function doGet(req, res) { req.session.setAttribute("returnUrl", req.getParameter("continue")); var styleUrl = "" +"border:1px solid #A6B9CA;" +"color: black;" +"background-color: #f0f8ff;" +"font-family:Arial,MS PGothic;" +"font-size:12px;" +"padding:3px;" +"padding-bottom:3px;" ; var html = <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja"> <body style="font-family: sans-serif,'MS ゴシック'"> <form method="POST"> <table><tr><td> <fieldset><legend>ユーザ認証</legend> <div style="font-size:20px;"> <img src="/_wsjs_/login/images/OpenID-32.png" style="vertical-align:bottom;" /> OpenIDを入力して下さい。 </div> <div style="margin: 8px 0px;"> <nobr> <input id="openid_url" name="openid_url" size="40" style={styleUrl}/> <input value="Login" name="Login" type="submit"/> </nobr> </div> <div style="font-size:12px;" > <span style="color:red;">※</span>Yahoo! OpenIDは現在サポートできていません。<br/> Google又はOpenID.ne.jpをお勧めします。<br/> OpenIDをお持ちで無い方は <a href="http://openid.ne.jp" target="_blank">こちら</a> から取得して下さい。<br/> </div> </fieldset> </td></tr></table> </form> </body> </html> ; res.setContentType("text/html; charset=utf-8"); res.writer.write('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">\n'); res.writer.write(html); } // OpenID受け取り function doPost(req, res) { var returnTo = req.session.getAttribute("returnUrl"); if (returnTo.startsWith("/")) { returnTo = "http://"+req.getServerName()+returnTo; } var domain = req.getServerName(); var openid_url = req.getParameter("openid_url"); var attributes = new java.util.HashSet(); var userService = UserServiceFactory.getUserService(); var loginUrl = userService.createLoginURL( returnTo, domain, openid_url, attributes ); // プロバイダのログイン画面へ res.sendRedirect(loginUrl); }

    動かしてみる。 まず、

  • http://wsjs-gae.appspot.com/openid/target-page.ssjs にアクセスすると
  • http://wsjs-gae.appspot.com/openid/openid.ssjs?continue=/openid/target-page.ssjs に転送され OpenID の入力を行う。

    あれ?何かエラーが...

    java.lang.IllegalArgumentException: The requested URL was not allowed: http://wsjs-gae.appspot.com/openid/target-page.ssjs
    	com.google.appengine.api.users.UserServiceImpl.makeSyncCall(UserServiceImpl.java:131)
    	com.google.appengine.api.users.UserServiceImpl.createLoginURL(UserServiceImpl.java:62)
    

    ググったらすぐ分かった。 GAEに認証で gmail.com 以外を許可してなかった。 管理画面から設定。

    再度挑戦すると無事 プロバイダのログイン画面が現れた。

    ログインするとターゲットのページに戻って OpenID の登録メアドが 表示されてOK。

    思ったより簡単。 面倒なのは最初のURLを引き回すとこだけだね。


  • プロフィール
    20年勤めた会社がリーマンショックで消滅、紆余曲折を経て現在はフリーランスのSE。 失業をきっかけにこのブログを始める。

    サイト内検索

    登録
    RSS/2.0

    カテゴリ

    最近の投稿【GAE/J】

    リンク

    アーカイブ