Android からGoogleアカウント認証でGAEアクセスするとServer Error になってしまう

  1. Android (実機) と GAE を連携させるためのデバッグ環境を Windows7 に構築する
  2. Android アプリから Google アカウントを利用して GAE アプリケーションを利用する
  3. GAE開発サーバーアカウントを利用する
  4. Android GAE を利用したアプリを開発するのに Google App Engine Launcher が便利

と徐々に Android と GAE を連携させるべく調査、テストを進めてきたのだが、当初問題なく動いていたテストアプリが、突然エラーになるようになってしまった。

アプリから、送信しているURL

http://typea-android-apps.appspot.com/_ah/login?continue={認証後に遷移するパス}&auth={認証トークン}

を、ブラウザから投げてみると、

gae_auth01

The server encountered an error and could not complete your request.

If the problem persists, please report your problem and mention this error message and the query that caused it.

てな、エラーが発生している。

しばらく、トライ&エラーを繰り返すも解決せず・・・

うーむ。

上記、メッセージで検索してみると、StackOverflow に以下の情報が

http://stackoverflow.com/questions/3919071/what-is-the-proper-url-to-get-an-auth-cookie-from-a-gae-based-application

どうも、認証が期限切れなのが原因のようですね。

それにしても StackOverflow にはいつも助けられる。ありがとうございます。

GAE 側の Application Setting から、Cookie Expiration を確認すると、1Day になっているので、最初のテストの時には、OKだが、数日たって再テストすると、期限切れになっていたのだろう。

gae_auth02

 

ということで、API リファレンスを確認すると、、、

public void invalidateAuthToken (String accountType, String authToken)

・認証トークンをAccountManagerのキャッシュから削除します。
・認証トークンが、キャッシュにない場合何もしません。
・アプリケーションは、期限が切れた、もしくは、認証リクエストが無効になる、認証トークンが見つかった場合、このメソッドを呼ぶべき。
・AccountManager  は、キャッシュされた認証トークンを、有効化したり期限切れにしたり
等はしない。
・このメソッドはメインスレッドから呼んでも安全。
・このメソッドは、呼び出し元に MANAGE_ACCOUNTS もしくは USE_CREDENTIALS パーミッ
ションを要求

パラメータ

accountType     認証トークンの、アカウントタイプ。null不可
authToken       無効にする認証トークン、null の場合あり

てな感じのことが書いてあるように自分には読めた。

今回のエラーでは、ステータスコード 500が帰ってきているので、とりあえず、ステータスコードを確認して、500なら、キャッシュを削除して、再度認証トークンを取得するようにすれば良さそうだ。

そのようにコードを書き直してみる。(500番台のエラーとしてもいいのかも)

 

実際、肝心な所は、removeAuthTokenCache() だけなのだが、「Android アプリから Google アカウントを利用して GAE アプリケーションを利用する 」のままでは、あまりに汎用性がない(まぁ動作確認が目的なので致し方ないが)ので、ある程度使い回せることを目指して、コードを書き直す。

  1. package info.typea.lib;
  2.  
  3. import java.io.BufferedReader;
  4. import java.io.IOException;
  5. import java.io.InputStream;
  6. import java.io.InputStreamReader;
  7.  
  8. import org.apache.http.HttpResponse;
  9. import org.apache.http.HttpStatus;
  10. import org.apache.http.client.ClientProtocolException;
  11. import org.apache.http.client.methods.HttpGet;
  12. import org.apache.http.client.methods.HttpPost;
  13. import org.apache.http.client.params.ClientPNames;
  14. import org.apache.http.cookie.Cookie;
  15. import org.apache.http.impl.client.DefaultHttpClient;
  16.  
  17. import android.accounts.Account;
  18. import android.accounts.AccountManager;
  19. import android.accounts.AccountManagerCallback;
  20. import android.accounts.AccountManagerFuture;
  21. import android.accounts.AuthenticatorException;
  22. import android.accounts.OperationCanceledException;
  23. import android.content.Context;
  24. import android.content.Intent;
  25. import android.os.Bundle;
  26. import android.util.Log;
  27.  
  28. /**
  29. * @author piroto
  30. */
  31. public class GoogleServiceUtil {
  32. private GoogleServiceUtil() {}
  33. public enum GOOGLE_ACCOUNT_TYPE {
  34. GAE { public String toString() {return "ah"; } },
  35. CALENDAR { public String toString() {return "cl"; } },
  36. GMAIL { public String toString() {return "mail"; } },
  37. READER { public String toString() {return "reader"; } },
  38. TALK { public String toString() {return "talk"; } },
  39. YOUTUBE { public String toString() {return "youtube"; } },
  40. }
  41. /**
  42. * GAE での認証用URIを生成
  43. * @param appPath
  44. * @param authToken
  45. * @return
  46. */
  47. public static String defaultGAELoginUrl(String hostname, String appPath, String authToken) {
  48. return "http://" + hostname + "/_ah/login?continue=" + appPath + "&auth=" + authToken;
  49. }
  50. /**
  51. * Google のサービスを実行するクラス
  52. *
  53. * @author piroto
  54. */
  55. public static class Executer {
  56. public static final String GOOGLE_ACCOUNT_TYPE = "com.google";
  57. private Context context;
  58. private AccountManager accountManager;
  59. /**
  60. * @param context
  61. */
  62. public Executer(Context context) {
  63. this.context = context;
  64. }
  65. /**
  66. * @return
  67. */
  68. private AccountManager getAccountManager() {
  69. if (accountManager == null) {
  70. accountManager = AccountManager.get(this.context);
  71. }
  72. return accountManager;
  73. }
  74. /**
  75. * Google アカウントを取得
  76. * @return
  77. */
  78. public Account[] getGoogleAccounts() {
  79. return getAccountManager().getAccountsByType(GOOGLE_ACCOUNT_TYPE);
  80. }
  81. /**
  82. * 認証を行った後、Google サービスを実行させる
  83. * @param account Google アカウント
  84. * @param type アカウントタイプ
  85. * @param callback 認証の要求が完了したときに呼び出されるコールバック
  86. */
  87. public void execute(Account account, GOOGLE_ACCOUNT_TYPE type, AbstractGoogleServiceCallback callback) {
  88. getAccountManager().getAuthToken(
  89. account,
  90. type.toString(),
  91. false,
  92. callback,
  93. null // nullの場合、callback はメインスレッド
  94. );
  95. }
  96. /**
  97. * アカウントを削除
  98. * @param account
  99. * @param type
  100. */
  101. public void removeAccount(Account account) {
  102. getAccountManager().removeAccount(account, null, null);
  103. }
  104. }
  105. /**
  106. * Google サービスの認証部を実装したコールバック
  107. * @author piroto
  108. *
  109. */
  110. public static abstract class AbstractGoogleServiceCallback implements AccountManagerCallback {
  111. private Context context;
  112. /**
  113. * @param context
  114. */
  115. public AbstractGoogleServiceCallback(Context context) {
  116. this.context = context;
  117. }
  118. /**
  119. * @return
  120. */
  121. public Context getContext() {
  122. return this.context;
  123. }
  124. /**
  125. * @param bundle
  126. * @param authToken
  127. */
  128. public void removeAuthTokenCache(Bundle bundle, String authToken) {
  129. String accountType = bundle.getString(AccountManager.KEY_ACCOUNT_TYPE);
  130. AccountManager manager = AccountManager.get(getContext());
  131. manager.invalidateAuthToken(accountType, authToken);
  132. }
  133. /**
  134. * ACSID を取得する
  135. * @param bundle
  136. * @return
  137. */
  138. private String getACSID(Bundle bundle){
  139. final int RETRY_MAX = 3;
  140. boolean isValidToken = false;
  141. int retry = 0;
  142. String acsid = null;
  143. try {
  144. DefaultHttpClient httpClient = null;
  145. while (!isValidToken) {
  146. String authToken = bundle.getString(AccountManager.KEY_AUTHTOKEN);
  147. httpClient = new DefaultHttpClient();
  148. httpClient.getParams().setBooleanParameter(ClientPNames.HANDLE_REDIRECTS, false);
  149. String uri = getAuthenticateUri(authToken);
  150. HttpGet httpAuthGetRequest = new HttpGet(uri);
  151. HttpResponse httpAuthResponse = httpClient.execute(httpAuthGetRequest);
  152. int status = httpAuthResponse.getStatusLine().getStatusCode();
  153. if (status == HttpStatus.SC_INTERNAL_SERVER_ERROR) {
  154. // 認証エラーの場合、ログにレスポンスの内容を出力
  155. try {
  156. StringBuilder buf = new StringBuilder();
  157. buf.append(String.format("status:%d\n",status));
  158. InputStream in = httpAuthResponse.getEntity().getContent();
  159. BufferedReader reader = new BufferedReader(new InputStreamReader(in));
  160. String l = null;
  161. while((l = reader.readLine()) != null) {
  162. buf.append(l + "\n");
  163. }
  164. }catch(Exception e) {
  165. }
  166. // 認証トークンキャッシュの削除
  167. // 期限切れ、もしくは、認証リクエストが無効になるような、認証トークンが見つかった場合、
  168. // キャッシュのクリアを行う
  169. removeAuthTokenCache(bundle, authToken);
  170. } else {
  171. isValidToken = true;
  172. }
  173. retry++;
  174. if (retry > RETRY_MAX) {
  175. break;
  176. }
  177. }
  178. if (isValidToken) {
  179. // 認証エラーでなければ、Cookie から ACSIDを取得する
  180. for (Cookie cookie : httpClient.getCookieStore().getCookies()) {
  181. if ("SACSID".equals(cookie.getName()) ||
  182. "ACSID".equals(cookie.getName())) {
  183. acsid = cookie.getName() + "=" + cookie.getValue();
  184. }
  185. }
  186. }
  187. } catch (ClientProtocolException e) {
  188. // TODO Auto-generated catch block
  189. e.printStackTrace();
  190. } catch (IOException e) {
  191. // TODO Auto-generated catch block
  192. e.printStackTrace();
  193. }
  194. return acsid;
  195. }
  196. @Override
  197. public void run(AccountManagerFuture future) {
  198. HttpResponse httpBodyResponse = null;
  199. try {
  200. Bundle bundle = future.getResult();
  201. Intent intent = (Intent)bundle.get(AccountManager.KEY_INTENT);
  202. if (intent != null) {
  203. // 認証されていない場合、認証画面を起動
  204. this.context.startActivity(intent);
  205. } else {
  206. DefaultHttpClient httpClient = new DefaultHttpClient();
  207. String acsid = getACSID(bundle);
  208. // 認証が正常に行われ、ACSIDが取得できたら、サービスのリクエストをPOSTし、
  209. // レスポンスを取得
  210. if (acsid != null) {
  211. HttpPost httpPost = request();
  212. httpPost.setHeader("Cookie", acsid);
  213. httpBodyResponse = httpClient.execute(httpPost);
  214. }
  215. }
  216. } catch (OperationCanceledException e) {
  217. // TODO
  218. e.printStackTrace();
  219. } catch (AuthenticatorException e) {
  220. // TODO
  221. e.printStackTrace();
  222. } catch (IOException e) {
  223. // TODO
  224. e.printStackTrace();
  225. } finally {
  226. // コールバックする。認証などでエラーが発生している場合、レスポンスは null
  227. callback(httpBodyResponse);
  228. }
  229. }
  230. /**
  231. * 認証を行うURLを指定
  232. * たとえばGAEであれば、以下のようなURLを返す
  233. *
    "http://typea-msg-brd.appspot.com/_ah/login?continue=/&auth=" + authToken;
  234. * @param authToken

  235. * @return

  236. */

  237. public abstract String getAuthenticateUri(String authToken);

  238. /**

  239. * 認証に成功した後、実際に行いたいリクエストを実装する

  240. * @return

  241. */

  242. public abstract HttpPost request();

  243. /**

  244. * request() レスポンスを受け取る

  245. * @param response

  246. */

  247. public abstract void callback(HttpResponse response);

  248. }

  249. }

  250. 呼び出し側は、例えば、以下のように使う。

  251. GoogleServiceUtil.Executer exec = new GoogleServiceUtil.Executer(this);
  252. for (Account a : exec.getGoogleAccounts()) {
  253.     exec.execute(a, GOOGLE_ACCOUNT_TYPE.GAE, new GoogleServiceUtil.AbstractGoogleServiceCallback(this) {
  254.         @Override
  255.         public HttpPost request() {
  256.             // HTTP POST リクエストオブジェクトを生成して返す
  257.             HttpPost post =  new HttpPost("http://typea-android-apps.appspot.com/lbmsg/insert");
  258.             try {
  259.                 List parms = new ArrayList();
  260.                 parms.add(new BasicNameValuePair("lat", String.valueOf(loc.getLatitude())));
  261.                 parms.add(new BasicNameValuePair("lon", String.valueOf(loc.getLongitude())));
  262.                 parms.add(new BasicNameValuePair("message", msg));
  263.                 post.setEntity(new UrlEncodedFormEntity(parms, HTTP.UTF_8));
  264.             } catch (UnsupportedEncodingException e) {
  265.                 // TODO Auto-generated catch block
  266.                 e.printStackTrace();
  267.             }
  268.             return post;
  269.         }
  270.         
  271.         @Override
  272.         public String getAuthenticateUri(String authToken) {
  273.             // 認証を行うURLを返す
  274.             return GoogleServiceUtil.defaultGAELoginUrl("typea-android-apps.appspot.com",
  275.                                                         "/lbmsg",
  276.                                                         authToken);
  277.         }
  278.         
  279.         @Override
  280.         public void callback(HttpResponse response) {
  281.             // サービスを呼び出したレスポンスに対する処理
  282.             if (response != null) {
  283.                 int status = response.getStatusLine().getStatusCode();
  284.                 
  285.                 StringBuilder buf = new StringBuilder();
  286.                 buf.append(String.format("status:%d", status));
  287.                 try {
  288.                     InputStream in = response.getEntity().getContent();
  289.                     BufferedReader reader = new BufferedReader(new InputStreamReader(in));
  290.                     String l = null;
  291.                     while((l = reader.readLine()) != null) {
  292.                         buf.append(l + "\r\n");
  293.                     }        
  294.                     if (status != HttpStatus.SC_OK) {
  295.                         Log.e(LocationBasedMessageApplication.TAG, buf.toString());
  296.                     }
  297.                     
  298.                 } catch(Exception e) {
  299.                     e.printStackTrace();
  300.                 }                    
  301.                 (Toast.makeText(getContext(), buf.toString(), Toast.LENGTH_LONG)).show();
  302.             }
  303.         }
  304.     });
  305.     
  306.     break;
  307. }
  308. 結果、Server Error は解決されましたとさ。めでたしめでたし。

  309. Follow me!

Android からGoogleアカウント認証でGAEアクセスするとServer Error になってしまう” に対して1件のコメントがあります。

  1. 通りすがりさん より:

    StackOverflowは例外の種類ですよ。
    StackTraceのことでは?

  2. pppiroto (Hiroto YAGI) より:

    StackTrace も役に立ちますが、実は意外にと役に立つStackOverFlowもあるんです。
    http://stackoverflow.com/questions/tagged/android
    困ったときには、結構助けられてます。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です