2013年3月10日日曜日

Google+ Sign-in / Moment API

Google+ Platformの大きなアップデート、Google+ Sign-inが発表されました!詳細はニュース記事などに譲るとして、今回はとりあえずサインインしてAPIを呼び出すところまでやってみたのでまとめます。
こちらを参考に、ちょっと分からなかったところを補いつつ、Moment APIでApp Activityを送信するところまでやってみます。環境は、前回の記事で構築したものとなります。

準備その1:Client情報の登録

まずはGoogle+のClient IDとClient Srcretを取得します。公式ドキュメントのStep 1に従えばいいので省略します。

準備その2:必要なjarを配置

今回はMaven2で依存するjarを管理しているので、pom.xmlに設定を追加しておきます。WEB-INF/lib/にコピーも忘れずにしておきます。

  <repositories>
    <repository>
      <id>google-api-services</id>
      <url>http://google-api-client-libraries.appspot.com/mavenrepo</url>
    </repository>
  </repositories>

 ...

    <dependency>
      <groupid>com.google.api-client</groupid>
      <artifactid>google-api-client</artifactid>
      <version>1.13.2-beta</version>
    </dependency>
    <dependency>
      <groupid>com.google.api-client</groupid>
      <artifactid>google-api-client-servlet</artifactid>
      <version>1.13.0-beta</version>
    </dependency>
    <dependency>
      <groupid>com.google.api-client</groupid>
      <artifactid>google-api-client-appengine</artifactid>
      <version>1.13.2-beta</version>
    </dependency>
    <dependency>
      <groupid>com.google.apis</groupid>
      <artifactid>google-api-services-plus</artifactid>
      <version>v1-rev57-1.13.2-beta</version>
    </dependency>
    <dependency>
      <groupid>com.google.apis</groupid>
      <artifactid>google-api-services-oauth2</artifactid>
      <version>v2-rev9-1.7.2-beta</version>
    </dependency>

実装その1:CSRF対策

サインインボタンを表示するControllerを作成します。
この後説明するAccess Tokenの保存処理が不正に呼び出されないように、ランダムなパラメータを生成してセッションに保存しておきます。
同時に、Access Tokenの保存処理を呼び出すJavascriptにパラメータを渡せるように、リクエストにセットしておきます。

    @Override
    public Navigation run() throws Exception {
        String token = new BigInteger(130, new SecureRandom()).toString(32);
        sessionScope("validateToken", token);
        requestScope("validateToken", token);
        return forward("test.vm");
    }

実装その2:Javascriptライブラリの読み込み

Oauth2のAuthorization Code取得まではJavascriptでやるようなので、test.vmでGoogle+ライブラリを読み込んでおきます。
公式ドキュメントのStep 3のコピペでOKです。

実装その3:サインインボタンの設置

test.vmにサインインボタンを設置します。ここがポイントなのですが、
Moment APIを使う場合、data-requestvisibleactionsが必須になります。おそらくAPIの認可が必要ということなのでしょう。見事にハマったorz
他に指定できるパラメータはこちらに載っているのでご参照ください。

<div id="signinButton">
    <span class="g-signin"
        data-scope="https://www.googleapis.com/auth/plus.login"
        data-clientid="あなたのClient ID"
        data-redirecturi="postmessage"
        data-accesstype="offline"
        data-cookiepolicy="single_host_origin"
        data-callback="signInCallback"
        data-requestvisibleactions="http://schemas.google.com/AddActivity">
    </span>
</div>

実装その4:callback関数の実装

上記サインインボタンに設定したcallback関数を実装します。この中でAuthorization Codeを元にAccess Tokenを取得するリクエストを送信します。パラメータには、CSRF対策用のパラメータも設定します。

function signInCallback(authResult) {
    if (authResult['code']) {

        // Hide the sign-in button now that the user is authorized, for example:
        $('#signinButton').attr('style', 'display: none');

        // Send the code to the server
        jQuery.ajax({
            type: 'POST',
            url: '/api/storetoken',
            success: function(result) {
                
            },
            data: {
                code: authResult['code'],
                validateToken: '$validateToken',
            }
        });
    } else if (authResult['error']) {
        alert('Google+ Authorication Error');
    }
}

実装その5:Access Tokenの保存

JavascriptのPOSTを受けるControllerを実装します。まずはCSRF対策でパラメータチェックします。
その後、Authorization CodeとClient ID、Client Secretを使ってAccess Tokenを取得し、セッションに保存します。

    @Override
    public Navigation run() throws Exception {
        if (!validateToken()) {
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
            Map<string string=""> data = new HashMap<string string="">();
            data.put("state", "403");
            return json(data);
        }

        String clientId = "あなたのClient ID";
        String clientSecret = "あなたのClient Secret";
        String code = asString("code");

        HttpTransport transport = new NetHttpTransport();
        JsonFactory jsonFactory = new JacksonFactory();
        GoogleTokenResponse tokenResponse = new GoogleAuthorizationCodeTokenRequest(transport, jsonFactory,
            clientId, clientSecret, code, "postmessage"
        ).execute();
        GoogleCredential credential = new GoogleCredential.Builder()
            .setJsonFactory(jsonFactory)
            .setTransport(transport)
            .setClientSecrets(clientId, clientSecret).build()
            .setFromTokenResponse(tokenResponse);
        Oauth2 oauth2 = new Oauth2.Builder(transport, jsonFactory, credential).build();
        Tokeninfo tokenInfo = oauth2.tokeninfo().setAccessToken(credential.getAccessToken()).execute();
        if (tokenInfo.containsKey("error")) {
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
            Map<string string=""> data = new HashMap<string string="">();
            data.put("state", "403");
            return json(data);
        }
        if (!tokenInfo.getIssuedTo().equals(clientId)) {
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
            Map<string string=""> data = new HashMap<string string="">();
            data.put("state", "403");
            return json(data);
        }

        sessionScope("gPlusId", tokenInfo.getUserId());
        sessionScope("gAccessToken", credential.getAccessToken());

        Map<string string=""> data = new HashMap<string string="">();
        data.put("state", "200");
        return json(data);
    }

    protected boolean validateToken() {
        String specified = requestScope("validateToken");
        String token = sessionScope("validateToken");
        if (token.equals(specified)) {
            return true;
        }
        return false;
    }

    protected Navigation json(Object data) {
        try {
            JsonFactory jsonFactory = new JacksonFactory();
            JsonGenerator generator = jsonFactory.createJsonGenerator(response.getWriter());
            generator.serialize(data);
            generator.flush();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

        return null;
    }

実装その6:Moment API 呼び出し

最後に、取得したAccess Tokenを使ってMoment APIを呼び出すControllerを実装します。

    @Override
    public Navigation run() throws Exception {
        String gPlusId = sessionScope("gPlusId");
        String gAccessToken = sessionScope("gAccessToken");

        String clientId = "あなたのClient ID";
        String clientSecret = "あなたのClient Secret";
        String applicationName = "あなたのアプリ名";

        HttpTransport transport = new NetHttpTransport();
        JsonFactory jsonFactory = new JacksonFactory();
        GoogleCredential credential = new GoogleCredential.Builder()
            .setJsonFactory(jsonFactory)
            .setTransport(transport)
            .setClientSecrets(clientId, clientSecret).build()
            .setAccessToken(gAccessToken);

        Plus plus = new Plus.Builder(transport, jsonFactory, credential).setApplicationName(applicationName).build();

        Moment moment = new Moment();
        moment.setType("http://schemas.google.com/AddActivity");
        ItemScope itemScope = new ItemScope();
        itemScope.setId("target-id-1"); 
        itemScope.setType("http://schemas.google.com/AddActivity");
        itemScope.setName("The Google+ Platform");
        itemScope.setDescription("hogehoge!");
        moment.setTarget(itemScope);
        Moment momentResult = plus.moments().insert("me", "vault", moment).execute();

        MomentsFeed moments = plus.moments().list("me", "vault").execute();

        return redirect("/");
    }

自分の英語力不足なのか、公式ドキュメントからは分からなくてハマったところも多かったですが(Oauth2クラスとかTokenInfoクラスを使うのにどのjarが必要か、とか)、なんとか出来ました。
Client IDなどを設定ファイルにしたりなどリファクタリングもまだまだ必要ですが。
道具は揃ったので、これをどう使っていくか考えてみようと思ってます!