Google+ログインボタンを使ってユーザー登録とログイン管理を行う

前回ではGoogle+ログインボタンの設置方法について説明しました。今回はGoogle+ログイン処理で取得できるユーザー情報を使ってGoogle App Engineのデータストアにユーザー登録を行う方法と、ログイン管理について説明して行きたいと思います。

ユーザー登録のユーザーモデル

まず、Google App Engineのデータストアで保持するユーザー情報モデルを作成します。(このサンプルはSlim3を使っています)このモデルではEmailアドレスをキーとし、「Google+ のユーザーID」、「アクセストークン」、「リフレッシュトークン」をデータストアに登録します。

@Model(schemaVersion = 1)
public class UserModel implements Serializable {

    private static final long serialVersionUID = 1L;
    /**
     * キー(Email)
     */
    @Attribute(primaryKey = true)
    private Key key;

    @Attribute(version = true)
    private Long version;

    /** userId */
    private String userId;

    /** アクセストークン */
    private String accessToken;

    /** リフレッシュトークン */
    private String RefreshToken;
    
    ・・・・・<ゲッターとセッターを省略>・・・・・


トークンの有効チェックとユーザー登録

Google+ボタンでログイン(アプリ承認)を行うとワンタイムコードを取得できることは前回説明しました。このワンタイムコードを利用する前に有効なワンタイムコードかどうかを事前にチェックする必要があります。

また、今回のサンプルでは先ほど作成したユーザーモデルのキーがメールアドレスになっているため、ログインしたユーザーのメールアドレスを取得する必要があります。メールアドレスはトークン情報に含まれており、このトークン情報はワンタイムコードを使ってGoogleに問い合わせる必要があります。取得したトークン情報には「アクセストークン」、「リフレッシュトークン」、「メールアドレス」が含まれていますので、これらの情報を使ってユーザーを行います。

実際のロジックは以下になります。

定数の定義

まずプログラムで必要な定数を定義します。ここで必要な定数は以下です。

  • クライアントID(アプリ作成時に取得したもの)
  • クライアントシークレット(アプリ作成時に取得したもの)
  • HttpTransport(ライブラリで提供されているクラス)
  • JacksonFactory(ライブラリで提供されているクラス)
public class AddUserController extends Controller {

    private static final String CLIENT_ID = "******************";
    private static final String CLIENT_SECRET = "*******************";

    private static final HttpTransport TRANSPORT = new NetHttpTransport();
    private static final JacksonFactory JSON_FACTORY = new JacksonFactory();


トークン情報の取得

次にワンタイムコードを使ってトークン情報を取得します。

        // ---------------------------------------------------------
        // トークン情報の取得
        // ---------------------------------------------------------
        // 承認コードをアクセス・更新トークンにアップグレードします。
        GoogleTokenResponse tokenResponse =
                new GoogleAuthorizationCodeTokenRequest(TRANSPORT, JSON_FACTORY,
                    CLIENT_ID, CLIENT_SECRET, code, "postmessage").execute();

        // トークン情報の取得(アクセストークン、リフレッシュトークン・・・)
        GoogleCredential credential = new GoogleCredential.Builder()
        .setJsonFactory(JSON_FACTORY)
        .setTransport(TRANSPORT)
        .setClientSecrets(CLIENT_ID, CLIENT_SECRET).build()
        .setFromTokenResponse(tokenResponse);


トークンの有効チェック

ここではトークンが有効であるかどうかのチェックと自分のアプリが発行したトークンかどうかをチェックします。

        // ---------------------------------------------------------
        // トークン情報の有効チェック
        // ---------------------------------------------------------
        // トークンの有効チェック
        Oauth2 oauth2 = new Oauth2.Builder(
            TRANSPORT, JSON_FACTORY, credential).build();
        Tokeninfo tokenInfo = oauth2.tokeninfo()
            .setAccessToken(credential.getAccessToken()).execute();
        // トークン情報にエラーがあれば、中断すしま。
        if (tokenInfo.containsKey("error")) {
            throw new Exception();
        }

        // 受け取ったトークンが自分のアプリのものであることを確認します。
        if (!tokenInfo.getIssuedTo().equals(CLIENT_ID)) {
            throw new Exception();
        }


ユーザー登録

トークン情報が問題なければ、それらをデータストアに登録します。ここで注意しないといけないのは、トークン情報に含まれる「リフレッシュトークン」は初回しか取得できません。もっと簡単にいうと、例えばユーザーがGoogleにログインしていない場合は、設置したGoogle+ログインボタンを使ってGoogleにログインすることができます。まだ登録していないユーザーはログインと同時にアプリ承認も行うことになっています。このアプリ承認を行った場合のみ「リフレッシュトークン」がトークン情報に含まれます。また、ユーザーがGoogle+上でアプリの承認を取り消した場合は、再度アプリの承認が求められるため、この場合は「リフレッシュトークン」を再度取得できます。

        // ---------------------------------------------------------
        // ユーザー登録
        // ---------------------------------------------------------
        // 念のために登録されていないユーザーかどうかを確認する
        Key key = Datastore.createKey(UserModelMeta.get(), tokenInfo.getEmail());
        UserModel userModel = Datastore.getOrNull(UserModel.class, key);

        // User情報モデルの作成
        // ログインチェック処理でEmailで検索するため、モデルのキーをEmailにする
        if(userModel == null) {
            userModel = new UserModel();
            userModel.setKey(key);
            userModel.setUserId(tokenInfo.getUserId());
            userModel.setAccessToken(credential.getAccessToken());
            userModel.setRefreshToken(credential.getRefreshToken());

            Datastore.put(userModel);
        }


AddUserControllerの全ソース

念のためにすべてのソースを下に載せておきます。

package it.trick.google.plus.login.controller;

import it.trick.google.plus.login.meta.UserModelMeta;
import it.trick.google.plus.login.model.UserModel;

import org.slim3.controller.Controller;
import org.slim3.controller.Navigation;
import org.slim3.datastore.Datastore;

import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeTokenRequest;
import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
import com.google.api.client.googleapis.auth.oauth2.GoogleTokenResponse;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.jackson2.JacksonFactory;
import com.google.api.server.spi.response.UnauthorizedException;
import com.google.api.services.oauth2.Oauth2;
import com.google.api.services.oauth2.model.Tokeninfo;
import com.google.appengine.api.datastore.Key;


public class AddUserController extends Controller {

    private static final String CLIENT_ID = "**************";
    private static final String CLIENT_SECRET = "***************";

    private static final HttpTransport TRANSPORT = new NetHttpTransport();
    private static final JacksonFactory JSON_FACTORY = new JacksonFactory();


    @Override
    public Navigation run() throws Exception {
        try {

            // Stateチェック
            checkState();

            // ユーザー情報の取得して登録
            addUser();

        }catch(Exception e) {
            throw new UnauthorizedException("No Login");
        }

        return null;
    }

    /**
     * チェックステート(なりすまし防止)
     * <pre>
     * リクエストのなりすましが行われておらず、この接続要求を送ったユーザーが、
     * 想定されたユーザーであることをここで確認します。
     * </pre>
     */
    private void checkState() throws Exception {
        // 承認リクエストに含まれるステート
        String requestState = asString("state");
        // セッションに含まれるステート
        String sessionState = sessionScope("state");

        // リクエストのなりすましが行われておらず、この接続要求を送ったユーザーが、
        // 想定されたユーザーであることをここで確認します。
        if (!requestState.equals(sessionState)) {
            throw new Exception();
        }
    }

    /**
     * ユーザー登録
     * @return
     * @throws Exception
     */
    private void addUser() throws Exception {

        String code = asString("code");

        // ---------------------------------------------------------
        // トークン情報の取得
        // ---------------------------------------------------------
        // 承認コードをアクセス・更新トークンにアップグレードします。
        GoogleTokenResponse tokenResponse =
                new GoogleAuthorizationCodeTokenRequest(TRANSPORT, JSON_FACTORY,
                    CLIENT_ID, CLIENT_SECRET, code, "postmessage").execute();

        // トークン情報の取得(アクセストークン、リフレッシュトークン・・・)
        GoogleCredential credential = new GoogleCredential.Builder()
        .setJsonFactory(JSON_FACTORY)
        .setTransport(TRANSPORT)
        .setClientSecrets(CLIENT_ID, CLIENT_SECRET).build()
        .setFromTokenResponse(tokenResponse);


        // ---------------------------------------------------------
        // トークン情報の有効チェック
        // ---------------------------------------------------------
        // トークンの有効チェック
        Oauth2 oauth2 = new Oauth2.Builder(
            TRANSPORT, JSON_FACTORY, credential).build();
        Tokeninfo tokenInfo = oauth2.tokeninfo()
            .setAccessToken(credential.getAccessToken()).execute();
        // トークン情報にエラーがあれば、中断すしま。
        if (tokenInfo.containsKey("error")) {
            throw new Exception();
        }

        // 受け取ったトークンが自分のアプリのものであることを確認します。
        if (!tokenInfo.getIssuedTo().equals(CLIENT_ID)) {
            throw new Exception();
        }

        // ---------------------------------------------------------
        // ユーザー登録
        // ---------------------------------------------------------
        // 念のために登録されていないユーザーかどうかを確認する
        Key key = Datastore.createKey(UserModelMeta.get(), tokenInfo.getEmail());
        UserModel userModel = Datastore.getOrNull(UserModel.class, key);

        // User情報モデルの作成
        // ログインチェック処理でEmailで検索するため、モデルのキーをEmailにする
        if(userModel == null) {
            userModel = new UserModel();
            userModel.setKey(key);
            userModel.setUserId(tokenInfo.getUserId());
            userModel.setAccessToken(credential.getAccessToken());
            userModel.setRefreshToken(credential.getRefreshToken());

            Datastore.put(userModel);
        }
    }
}

ユーザーのログインチェック

では最後に、Google+ボタンを使って登録したユーザーの情報を元にログインチェックを行う方法について説明します。

ログインチェックといっても、ユーザーがGoogleにログインしているかどうかをチェックし、ログインしている場合は利用アカウントのメールアドレスがデータストアに登録されているかをチェックするだけです。ユーザーがGoogleにログインしているかどうかのチェックとログインしているアカウントのメールアドレスを取得するにはGoogle App EngineのUserServiceを使えば簡単に実現できます。

今回は登録済みユーザーに表示するためのUserTopページを用意し、ログインしたユーザーのメールアドレスとGoogle+でのユーザーIDを表示します。ログインしていないユーザーもしくは未登録のユーザーがこのUserTopページにアクセスすると/index (ログインページ)にリダイレクトするようにします。

Googleログインチェックのための設定(web.xml)

Googleにログインしているかどうかのチェックとログインしているアカウントのメールアドレスを取得するにはGoogle App EngineのUserServiceを使いますが、このUserServiceを使えるためにはまず「web.xml」に以下の設定を追加する必要があります。

    <security-constraint>
        <web-resource-collection>
            <web-resource-name>/user</web-resource-name>
            <url-pattern>/user/*</url-pattern>
        </web-resource-collection>

        <auth-constraint>
            <role-name>*</role-name>
        </auth-constraint>
    </security-constraint>
</web-app>

UserTopページのコントローラークラス

UserTopページのコントローラークラスは以下のようになります。難しいことはしていないので説明を割愛させて頂きます。
package it.trick.google.plus.login.controller.user;

import it.trick.google.plus.login.meta.UserModelMeta;
import it.trick.google.plus.login.model.UserModel;

import org.slim3.controller.Controller;
import org.slim3.controller.Navigation;
import org.slim3.datastore.Datastore;

import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.users.User;
import com.google.appengine.api.users.UserService;
import com.google.appengine.api.users.UserServiceFactory;

public class UserTopController extends Controller {

    @Override
    public Navigation run() throws Exception {

        try {
            // ユーザー情報の取得
            UserModel model = getLoginUser();

            // 表示情報をリクエストスコープに登録
            requestScope("email", model.getKey().getName());
            requestScope("userId", model.getUserId());

        }catch(Exception e) {
            // ログインしていない、もしくは登録していないユーザーの場合、
            // index画面にリダイレクトする
            return redirect("/");
        }


        return forward("userTop.jsp");
    }

    /**
     * 登録ユーザーの場合、登録情報を取得する。
     * 登録ユーザーではない、もしくGoogleアカウントにログインしていない場合は、
     * エラーを生成
     * @return
     * @throws Exception
     */
    private UserModel getLoginUser() throws Exception {
        // Google App Engineのユーザーサービスからユーザー情報を取得
        UserService us = UserServiceFactory.getUserService();
        User user = us.getCurrentUser();

        // Googleアカウントにログインしていない場合
        if(user == null) throw new Exception();

        // Slim3のDatastore.get()を使うと、
        // 取得できない場合にExceptionが発生するため、
        // 登録していないユーザーの場合はこのExceptionが発生する
        Key key = Datastore.createKey(UserModelMeta.get(), user.getEmail());
        return Datastore.get(UserModel.class, key);
    }
}

userTopページJSP

JSPは以下のようになります。ここではログインユーザーのGoogle+ IDとメールアドレスを表示しています。

<%@page pageEncoding="UTF-8" isELIgnored="false" session="false"%>

<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>UserTop</title>
</head>
<body>
<p>Hello UserTop !!!</p>
<p>あなたのEmail: ${email }<p>
<p>あなたのGoogle+ のユーザーID: ${userId }<p>
</body>
</html>


最後に

これで4回にわたって説明してきた「Google+ログインボタンを使ったユーザー登録とログイン機能の作り方」は以上になります。いかがでしたでしょうか。Google App Engineを使えばこんなにも簡単にGoogle+ ログイン機能を実現できることがわかりましたね。