🎃

Googleでログインお勉強メモ2:レガシーGoogle Sign-In

2021/09/25に公開

はじめに

Googleでログインする方法はいくつかあります。これは レガシーGoogle Sign-In を使って実装をお試ししたメモです

Google Sign-In

以下のことは書いていません

❌ Google Identity Services(g_id_signin)

❌ Spring Security OAuth2/OpenID Connect Client を使った実装

❌ macOS以外の実装

❌ Google One Tapについて

❌ Kotlin(Java)以外の実装

注意

Googleのサポートは2023 年 3 月 31 日で終了とのこと

今後は Google Identity Services を使いましょう

参考

環境

フロントはHTML+JavaScriptで構成する
バックエンドはSpring Boot & Kotlin で実装する

  • macOS Big Sur
  • Kotlin
  • Spring Boot

準備

Spring Bootプロジェクト作成

spring initializr で以下の設定でプロジェクトを作成する

  • Project
    • Gradle Project
  • Language
    • Kotlin
  • Dependencies
    • Spring Web
    • Thymeleaf

クライアントID取得

Google Cloud Platform で クライアント ID を取得する

以下の説明を参考にする

Get your Google API client ID

以下の設定をする

  • 承認済みの JavaScript 生成元 には http://localhosthttp://localhost:8080 の2つを設定する

実装

1. Sign-In With Googleボタンを配置する

Front - HTML

resources/templates/index.html

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Google Sign-In Demo</title>
</head>
<body>
    <script src="https://apis.google.com/js/platform.js" async defer></script>
    <meta name="google-signin-client_id" content="CLIENT ID">
    <div class="g-signin2" data-onsuccess="onSignIn"></div>
</body>
</html>
  • google-signin-client_idcontent に取得したクライアントID(xxxx.apps.googleusercontent.com)を設定する

  • g-signin2data-onsuccess はログイン完了のコールバック関数(JavaScritp)を設定する

  • ボタンのデザイン調整など細かい情報は Building a custom Google Sign-In buttonを参照のこと

Backend - Kotlin

上記 index.html を表示するコントローラを作成する

RootController.kt

import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.GetMapping

@Controller
class RootController {

    @GetMapping("/")
    fun index(): String {
        return "index"
    }
}

動作確認

この時点でGoogleでログインは動作します。(ログイン完了後のコールバック関数が無いのでエラーになります)

ログイン画面

2. ログイン完了コールバックを実装する

Googleでログイン完了した時のコールバックを作成します。

フロントのJavaScritpでコールバックを受けてバックエンドに処理を移します。

Front - HTML

  • index.htmlにコールバックonSignInを実装する
  • auth2.disconnect()でログイン状態を保持しないようにする
  • googleUser.getAuthResponse()でIDトークンを取得しバックエンドhttp://localhost:8080/signinに送る
    • SignInController
  • バックエンドから応答が来たらhttp://localhost:8080/userinfoにリダイレクトする
    • UserInfoController

resources/templates/index.html

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Google Sign-In Demo</title>
</head>
<body>
    <script src="https://apis.google.com/js/platform.js" async defer></script>
    <meta name="google-signin-client_id" content="CLIENT ID">
    <div class="g-signin2" data-onsuccess="onSignIn"></div>

    <script>
        function onSignIn(googleUser) {
            console.log('onSignIn.');
            var auth2 = gapi.auth2.getAuthInstance();
            auth2.disconnect();

            // Get ID Token
            var id_token = googleUser.getAuthResponse().id_token;

            // Send ID Token to Backend
            var xhr = new XMLHttpRequest();
            xhr.open('POST', 'http://localhost:8080/signin');
            xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
            xhr.onload = function() {
                if(xhr.readyState == 4 && xhr.status == 200){
                    // Redirect
                    window.location.href = 'http://localhost:8080/userinfo';
                }else{
                    console.log('Error');
                }
            };
            xhr.send('credential=' + id_token);
        }
    </script>
</body>
</html>

Gradle

build.gradle.kts

  • 依存関係にcom.google.api-client:google-api-client を追加する
dependencies {
	implementation("com.google.api-client:google-api-client:1.31.5")
	implementation("org.springframework.boot:spring-boot-starter-thymeleaf")
	implementation("org.springframework.boot:spring-boot-starter-web")
	implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
	implementation("org.jetbrains.kotlin:kotlin-reflect")
	implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
	testImplementation("org.springframework.boot:spring-boot-starter-test")
}

Backend - Kotlin

SignInController.kt

import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier
import com.google.api.client.http.javanet.NetHttpTransport
import com.google.api.client.json.gson.GsonFactory
import org.springframework.http.HttpStatus
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.ResponseStatus
import org.springframework.web.bind.annotation.RestController
import javax.servlet.http.HttpServletRequest

@RestController
class SignInController {

    @PostMapping("signin")
    @ResponseStatus(HttpStatus.OK)
    fun signIn(
        @RequestParam("credential") credential: String,
        request: HttpServletRequest,
    ) {
        // verify ID Token
        val verifier = GoogleIdTokenVerifier.Builder(
            NetHttpTransport(), GsonFactory.getDefaultInstance()
        )
            .setAudience(listOf("CLIENT ID"))
            .build()

        val idToken = verifier.verify(credential) ?: throw Exception()
        val payload = idToken.payload

        // Get profile information from payload
        val session = request.getSession(true)
        session.setAttribute("subject", payload.subject)
        session.setAttribute("email", payload.email)
        session.setAttribute("emailVerified", payload.emailVerified)
        session.setAttribute("name", payload["name"])
        session.setAttribute("picture", payload["picture"])
        session.setAttribute("locale", payload["locale"])
        session.setAttribute("family_name", payload["family_name"])
        session.setAttribute("given_name", payload["given_name"])

        return
    }
}

3. ログイン後ページを実装する

ログインしたユーザー情報を表示するページを実装します

Front - HTML

resources/templates/userinfo.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>UserInfo</title>
</head>
<body>
    <div>subject(User ID):<span th:text="${subject}"></span></div>
    <div>email:<span th:text="${email}"></span></div>
    <div>emailVerified:<span th:text="${emailVerified}"></span></div>
    <div>name:<span th:text="${name}"></span></div>
    <div>pictureUrl:<span th:text="${picture}"></span></div>
    <div>locale:<span th:text="${locale}"></span></div>
    <div>family_name:<span th:text="${family_name}"></span></div>
    <div>given_name:<span th:text="${given_name}"></span></div>

    <a href="/">Back</a>

</body>
</html>

Backend - Kotlin

セッションからユーザー情報を取り出してフロントに引き渡します

UserInfoController.kt

import org.springframework.stereotype.Controller
import org.springframework.ui.Model
import org.springframework.web.bind.annotation.GetMapping
import javax.servlet.http.HttpServletRequest

@Controller
class UserInfoController {

    @GetMapping("userinfo")
    fun userInfo(
        request: HttpServletRequest,
        model: Model
    ): String {

        // Get User Info from session
        val session = request.getSession(false) ?: throw Exception()
        model.addAttribute("subject", session.getAttribute("subject"))
        model.addAttribute("email", session.getAttribute("email"))
        model.addAttribute("emailVerified", session.getAttribute("emailVerified"))
        model.addAttribute("name", session.getAttribute("name"))
        model.addAttribute("picture", session.getAttribute("picture"))
        model.addAttribute("locale", session.getAttribute("locale"))
        model.addAttribute("family_name", session.getAttribute("family_name"))
        model.addAttribute("given_name", session.getAttribute("given_name"))

        return "userinfo"
    }
}

動作確認

Googleでログインすると Backend の SignInController.signin()が実行された後 userinfo.html に遷移する

UserInfo画面

おつかれさまでした

  • Google Identity Servicesと大体一緒だけどpopupではなく、redirectにする方法が不明
  • ボタンをカスマイズするのが果てしなく面倒そう

Discussion