GeekFactory

int128.hatenablog.com

AjaxによるFederated Loginの待ち時間の短縮

App Engine上のアプリでは利用者の体感待ち時間を短くするため、JSPを使わずにAjaxで実装することがあります。この方法はOpenID認証(Federated Login)でも有効です。

web.xmlのsecurity-constraintで認証をチェックする方法を以前紹介しましたが、この方法ではサーブレットを経由してログイン画面に遷移するまでに待ち時間が発生してしまいます。

代わりにクライアント側で認証をチェックすることで、ログイン画面に遷移するまでの待ち時間を省けます。クライアント側で認証をチェックするには、JavaScriptで ACSID というクッキー*1の有無を確認します。

(図の1) JavaScriptは認証Cookieの有無を確認します。初期状態では認証Cookieを持っていないので、ログインフォームを表示します。

$.extend({
  /**
   * ログイン済みかどうか返す.
   */
  isLoggedIn: function(){
    return (document.cookie.search(/dev_appserver_login=|ACSID=/) != -1);
  }
});

if($.isLoggedIn()){
  // ログイン済み
}
else{
  // 未ログイン
  $('#login').show();
}

(図の2) ユーザはログインボタンをクリックします。すると、JavaScriptAjaxでサーバにログインURLを問い合わせます。

$.get('/openapi/federation/openid', {identifier: identifier, backUrl: backUrl}, function(url){
  location.href = url;
});

サーバは UserService#createLoginURL() の結果をそのまま返します。

// OpenidController#run()
public Navigation run() throws Exception
{
  response.setContentType("text/plain");
  response.setCharacterEncoding("UTF-8");
  
  Validators v = new Validators(request);
  v.add("identifier", v.required(), v.maxlength(250));
  v.add("backUrl", v.required(), v.maxlength(250));
  if(v.validate()) {
    String identifier = asString("identifier");
    String backUrl = asString("backUrl");
    String url = authenticationService.getOpenIdLoginURL(identifier, backUrl, request.getServerName());
    response.getWriter().println(url);
  }
  else {
    response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
    response.getWriter().println(v.getErrors().toString());
  }
  return null;
}

// AuthenticationService#getOpenIdLoginURL()
public String getOpenIdLoginURL(String identifier, String backUrl, String domain)
{
  return userService.createLoginURL(
      backUrl,
      domain,
      identifier,
      Sets.newHashSet(
          "openid.mode=checkid_immediate",
          "openid.ns=http://specs.openid.net/auth/2.0",
          "openid.return_to=" + backUrl));
}

サーバからログインURLが返ってくるので、このURLに遷移します。

(図の3) App Engine内部で認証連携処理が行われます。実はこの部分でかなり時間を食っているのですが、IdP側が遅い場合もあるので改善は難しい。認証に成功すると、App Engineは認証Cookieを発行します。

(図の4) JavaScriptは認証Cookieの有無を確認します。認証Cookieを持っているので、ログイン済みの画面を表示します。

if($.isLoggedIn()){
  // ログイン済み
  $('#dashboard').init();
  $('#dashboard').show();
}
else{
  // 未ログイン...
}

なお、Ajaxリクエストを受けるサーブレットでは必ず認証をチェックするようにしてください。JavaScriptのチェックだけでは不十分です。例えばこんな感じで。

/**
 * ログイン中のユーザIDを返す.
 * @return ユーザID {@link User#getFederatedIdentity()}
 * @throws IllegalStateException ログインしていない場合
 */
public final String getUserId()
{
  if(!AppEngineUtil.isServer() || AppEngineUtil.isDevelopment()) {
    return "testId";
  }
  if(!userService.isUserLoggedIn()) {
    throw new IllegalStateException("Not logged in");
  }
  return userService.getCurrentUser().getFederatedIdentity();
}

/**
 * ログイン済みかチェックする.
 * @throws IllegalStateException ログインしていない場合
 */
public final void checkLoggedIn()
{
  if(!AppEngineUtil.isServer() || AppEngineUtil.isDevelopment()) {
    return;
  }
  if(!userService.isUserLoggedIn()) {
    throw new IllegalStateException("Not logged in");
  }
}

*1:開発環境では dev_appserver_login という名前になります。