開発情報・ナレッジ

投稿者: SPIRERS ナレッジ向上チーム 2023年5月2日 (火)

Googleアカウントで認証エリアにログインさせるサンプルプログラム

Googleのアカウント情報を利用して SPIRAL ver.2にAPIで登録を行い、認証エリアにログインさせるサンプルプログラムを作成いたしました。
SPIRAL ver.2とGoogleを連携して認証エリアにソーシャルログインさせたい場合は、ぜひ参考にしてみてください。
デモはこちら

仕様概要

本記事ではGoogleの公式ドキュメント を参考に、Javascript APIを利用してGoogleアカウント情報を取得しソーシャルログインを実装しています。
処理詳細
1)ログインページで「Googleでログイン」ボタンをクリックした時、アカウント情報を取得
2)取得したアカウント情報をGETパラメータとして付与し、ログイン処理ページへ遷移
3)ログイン処理ページでパラメータからGoogleアカウント情報を取得し、ver.2の認証DBへアカウントを登録
4)登録したアカウント情報で認証APIを実行し認証エリアへ遷移

上記のようにすることで、Googleアカウントの情報を使ってver.2の認証エリアにログインすることができます。

Google APIの設定

ソーシャルログインを実装するために、Google API クライアントIDを取得する必要があります。
設定方法に関しては、セットアップ | Authentication | Google Developers を参考にしてください。
取得したクライアントIDは以下でご紹介するログインプログラム内で使用いたします。

認証DBの設定

ログインする認証エリアを作成するため、認証DBを作成する必要があります。
Googleアカウントから取得したデータを登録するため下記の項目を追加するようにしてください。
必須項目
項目名 フィールドタイプ DB上で必須な属性 備考
認証ID テキスト 必須制約:あり
ユニーク成約:あり
認証IDとして利用するフィールドです。
ソーシャルログインする際に、GoogleアカウントIDを登録します。
パスワード パスワード 必須制約:あり
APIでログインさせる際に必要なフィールドです。
ソーシャルログインする際は、パスワードを自動生成します。
任意項目
項目名 フィールドタイプ DB上で必須な属性 備考
アカウント名 テキスト or
テキストエリア
なし Googleアカウント名を登録するフィールドです。
姓、名を分けて登録することも可能で、その場合は2つフィールドを用意してください。
メールアドレス メールアドレス なし Googleメールアドレスを登録するフィールドです。

ログインページの設定

headタブにJavascript APIを実行するために必要なクライアントライブラリを読み込んでください。
headタブ
<script src="https://accounts.google.com/gsi/client" async defer></script>
JavascriptタブにGoogleアカウントの情報を取得して、ログイン処理ページにリダイレクトするJavascriptを記載します。
Javascriptタブ
const googleClientId = "";
const loginProcessPage = "";

window.onload = function () {
    google.accounts.id.initialize({
        client_id: googleClientId,
        callback: handleCredentialResponse
    });
    google.accounts.id.renderButton(
        document.getElementById("buttonDiv"),
        { theme: "outline", size: "large" }
    );
    google.accounts.id.prompt();
};

function handleCredentialResponse(response) {
    const responsePayload = decodeJwtResponse(response.credential);
    const session = document.getElementById("loginPageSession");
    const sessionValue = session.textContent;

    window.location.href = `${loginProcessPage}?number=${responsePayload.sub}&email=${responsePayload.email}&name=${responsePayload.name}&session=${sessionValue}`;
};
function decodeJwtResponse(token) {
    const base64Url = token.split('.')[1];
    const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
    return JSON.parse(decodeURIComponent(escape(window.atob(base64))));
};
また、コード内の設定値に以下のようにそれぞれ値を設定してください。
設定値
googleClientId 発行したGoogle API クライアントIDを設定してださい。
loginProcessPage 次の手順で作成するログイン処理ページのURLを設定してください。
URLは相対パスで記載するようにしてください。
例)/googleLoginProcess

また、標準で用意されているボタンのHTMLをページ内の任意の場所に追加してください。
bodyタブ
<div id="buttonDiv"></div>
ボタンデザインは先ほど記載したJavascriptの
{ theme: "outline", size: "large" }
の部分のデータによって変更することが可能です。
詳細は「Google でログイン」Google API のリファレンスを参照してください。

同じくbodyタブにsessionの値をJavascriptに受け渡すためのThymeleafを記載してください。
非表示の項目ですので、先ほど追加したJavascriptよりも上であればどこに記載しても大丈夫です。
bodyタブ
<div id="loginPageSession" style="display:none;" th:text="${cp.result.value['session']}"></div>
最後にログインページにアクセスした際にsessionを発行するプログラムをPHPタブに記載します。
sessionはログイン処理ページへの不正経路からのアクセスを防止するために利用しています。
PHPタブ
<?php
session_start();
$_SESSION['randStr'] = makeRandStr(10);
$SPIRAL->setTHValue("session", $_SESSION['randStr']);

function makeRandStr($length) {
    $str = array_merge(range('a', 'z'), range('0', '9'), range('A', 'Z'));
    $r_str = null;
    for ($i = 0; $i < $length; $i++) {
        $r_str .= $str[rand(0, count($str) - 1)];
    }
    return $r_str;
}
?>
以上で、ログインページの設定は完了です。

ログイン処理ページの設定

ログイン処理のためのページを作成してください。
作成したページにver.2認証エリアのアカウント作成とAPIでのログイン処理を行うプログラムを記載します。
PHPタブ
<?php
define("API_URL", "https://api.spiral-platform.com/v1");
define("API_KEY", "");
define("APP_ROLE", "");
define("DB_ID", "");
define("APP_ID", "");
define("SITE_ID", "");
define("AREA_ID", "");
define("LOGIN_URL", "");
define("LOGIN_TOP_PASS", "");
define("ENCRYPT_KEY", "");

session_start();
$session = isset($_SESSION['randStr']) ? $_SESSION['randStr'] : "error";
// パラメータ情報取得
$url = (empty($_SERVER['HTTPS']) ? 'http://' : 'https://'). $_SERVER['HTTP_HOST']. $_SERVER['REQUEST_URI'];
$components = parse_url($url);
parse_str($components['query'], $urlParams);
$id = isset($urlParams["number"]) ? $urlParams["number"] : "";
$name = isset($urlParams["name"]) ? $urlParams["name"] : "";
$email = isset($urlParams["email"]) ? $urlParams["email"] : "";
$getSession = isset($urlParams["session"]) ? $urlParams["session"] : "";

if ($session == $getSession) {
    $result = googleLoginProcess($id, $email, $name);
    if ($result[0] == "error") {
        $SPIRAL->setTHValue("status", $result[0]);
        $SPIRAL->setTHValue("redirectURL", LOGIN_URL);
    } else if ($result[0] == "pageRedirect") {
        $SPIRAL->setTHValue("status", $result[0]);
        $SPIRAL->setTHValue("redirectURL", $result[1]["url"]);
    }
} else {
    $SPIRAL->setTHValue("error", "データが不正です。");
}

function googleLoginProcess($id, $email, $name) {
    $commonBase = CommonBase::getInstance();

    $insertData = array(
        "認証IDフィールドの識別名"  => $id,
        "アカウント名フィールドの識別名" => $name,
        "メールアドレスフィールドの識別名" => $email,
        "パスワードフィールドの識別名" => encrypt($id),
    );
    $resultRecordInsert = $commonBase->apiCurlAction("POST", "/apps/". APP_ID. "/dbs/". DB_ID. "/records", $insertData);

    $body = array(
        "id" => $id,
        "password" => encrypt($id),
    );
    $resultLogin = $commonBase->apiCurlAction("POST", "/sites/". SITE_ID. "/authentications/". AREA_ID. "/login", $body);

    if (isset($resultLogin["token"])) {
        $body = array(
            "token" => $resultLogin["token"],
            "path" => LOGIN_TOP_PASS,
        );
        $resultOneTimeLogin = $commonBase->apiCurlAction("POST", "/sites/". SITE_ID. "/authentications/". AREA_ID. "/oneTimeLogin", $body);
        if (isset($resultOneTimeLogin["url"])) {
            $result[] = "pageRedirect";
            $result[] = $resultOneTimeLogin;
            return $result;
        } else {
            $result[] = "error";
            $result[] = $resultLogin;
            return $result;
        }
    } else {
        $result[] = "error";
        $result[] = $resultLogin;
        return $result;
    }
}
function encrypt($data){
    return openssl_encrypt(substr($data,5), 'AES-128-ECB', ENCRYPT_KEY);
}

//------------------------------
// 共通モジュール
//------------------------------
class CommonBase {
    /**
     * シングルトンインスタンス
     * @var UserManager
     */
    protected static $singleton;

    public function __construct() {
        if (self::$singleton) {
            throw new Exception('must be singleton');
        }
        self::$singleton = $this;
    }
    /**
     * シングルトンインスタンスを返す
     * @return UserManager
     */
    public static function getInstance() {
        if (!self::$singleton) {
            return new CommonBase();
        } else {
            return self::$singleton;
        }
    }
    /**
     * V2用 API送信ロジック
     * @return Result
     */
    function apiCurlAction($method, $addUrlPass, $data = null, $multiPart = null, $jsonDecode = null) {
        $header = array(
            "Authorization:Bearer ". API_KEY,
            "X-Spiral-Api-Version: 1.1",
        );
        if($multiPart) {
            $header = array_merge($header, array($multiPart));
        } else {
            $header = array_merge($header, array("Content-Type:application/json"));
        }
        if(APP_ROLE){
            $header = array_merge($header, array("X-Spiral-App-Role: ".APP_ROLE));
        }
        // curl
        $curl = curl_init();
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($curl, CURLOPT_URL, API_URL. $addUrlPass);
        curl_setopt($curl, CURLOPT_HTTPHEADER, $header);
        if ($method == "POST") {
            if ($multiPart) {
                curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
            } else {
                curl_setopt($curl, CURLOPT_POSTFIELDS , json_encode($data));
            }
            curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method);
        }
        if ($method == "PATCH") {
            curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($data));
            curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method);
        }
        if ($method == "DELETE") {
            curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($data));
            curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method);
        }
        $response = curl_exec($curl);
        if (curl_errno($curl)) {
            return curl_error($curl);
        }
        curl_close($curl);
        if($jsonDecode){
            return $response;
        }else{
            return json_decode($response, true);
        }
    }
}
?>
PHP内の設定値に以下のようにそれぞれ値を設定してください。
設定値
API_KEY 発行したAPIキーを設定してださい。別途権限の付与が必要になります。
API_ROLE 設定したアプリロールの識別名を入れてください。全権限の場合は値は空で大丈夫です。
DB_ID 認証DBのIDを設定してください。
APP_ID 認証DBがあるアプリのIDを設定してください。
SITE_ID ソーシャルログインを行う認証エリアがあるサイトのIDを設定してください。
AREA_ID ソーシャルログインを行う認証エリアのIDを設定してください。
LOGIN_URL ログインページのURLを設定してください。
URLは絶対パスで記載するようにしてください。
例)https://www.example.com/login
LOGIN_TOP_PASS 認証後トップに設定しているページのURLを設定してください。
URLは相対パスで記載するようにしてください。
例)/topPage
ENCRYPT_KEY セキュリティを向上させるため、任意のランダム値を半角英数字で設定してください。
補足
define("API_KEY", $SPIRAL->getEnvValue("apikey"));
上記はAPIキーの記述で「PHP環境変数設定」を利用した場合のコードとなります。
このように定数を設定する時に、ver.2 機能の「PHP環境変数設定」を利用することで、
本番環境とテスト環境でキーが異なっていても、コードを書き換える必要がなく、保守性が高まります。
APIキーなど複数ページで使用するものは、環境変数設定をしておきましょう。
その他詳しくはサポートサイト「PHP環境変数」をご参照ください。

また、上記の設定値以外に以下の認証DBに登録するアカウント情報のフィールド識別名に関しては、ご自身の環境に合わせて値を変更するようにしてください。
$insertData = array(
    "認証IDフィールドの識別名"  => $id,
    "アカウント名フィールドの識別名" => $name,
    "メールアドレスフィールドの識別名" => $email,
    "パスワードフィールドの識別名" => encrypt($id),
);
APIでのログイン処理が成功した際に、認証エリア内ページにリダイレクトするためにheadタブにThymeleafを記載してください。
headタブ
<th:block th:if="${cp.result.value['status']} == 'pageRedirect'">
    <meta http-equiv="refresh" th:content="|0;URL=${cp.result.value['redirectURL']}|">
</th:block>
最後にログイン処理が失敗した際にエラー文言を表示するために、bodyタブにThymeleafを記載してください。
bodyタブ
<div>
    <div th:if="${cp.result.isSuccess}">
        <div th:if="${cp.result.value['status']} == 'error'">
            <h4>ログインエラーが発生しました。</h4>
            <a href="#" th:href="${cp.result.value['redirectURL']}">ログインページへ戻る</a>
        </div>
        <h4 th:text="${cp.result.value['error']}"></h4>
    </div>
    <div th:if="${!cp.result.isSuccess}">
        <h4>エラーが発生しました。</h4>
    </div>
</div>
以上でGoogleソーシャルログインのための設定は完了です。
デザインや、文言などは要件に合わせて適宜変更をお願いします。

最後に

設定後は動作確認を必ず行い、動作に問題がないか確認をしてください。
不具合やほかのやり方が知りたい等あれば、下記の「コンテンツに関しての要望はこちら」からご連絡ください。
解決しない場合はこちら コンテンツに関しての
要望はこちら