🐽

Laravel + Inertia なアンケートサービスにGoogleログイン(Socialite)を後付けした話

に公開

個人開発中のサービスで Googleログインを初めて実装した ので、
そのときのメモを簡易的にまとめておきます。

今回は Laravel + Inertia + Vue 構成です。
「とりあえず動かす」→「既存ユーザと紐付ける」までをゴールにしています。

作成したのはこちらのサービスです(宣伝!)
Laravel + Inertia で作ったアンケートサービス『リクエスとん』です。
触ってもらえるととても喜びます 🙏

https://reqton.akubilab.com/


Laravel Socialite を導入

composer require laravel/socialite

config/services.php に Google を追加

'google' => [
    'client_id' => env('GOOGLE_CLIENT_ID'),
    'client_secret' => env('GOOGLE_CLIENT_SECRET'),
    'redirect' => env('GOOGLE_REDIRECT_URI'),
],

.env に追加(※値は伏せる)

GOOGLE_CLIENT_ID=xxxx
GOOGLE_CLIENT_SECRET=xxxx
GOOGLE_REDIRECT_URI=https://your-domain.com/auth/google/callback

# ローカル用
# GOOGLE_REDIRECT_URI=http://localhost:8000/auth/google/callback

Googleログイン用コントローラ作成

php artisan make:controller Auth/GoogleLoginController
<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Support\Facades\Auth;
use Laravel\Socialite\Facades\Socialite;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Hash;

class GoogleLoginController extends Controller
{
    public function redirect()
    {
        // Google認証画面へ
        return Socialite::driver('google')->redirect();
    }

    public function callback()
    {
        // stateless() はローカル環境対策
        $googleUser = Socialite::driver('google')->stateless()->user();

        // ① google_id で検索
        $user = User::where('google_id', $googleUser->id)->first();

        if (!$user) {
            // ② email で既存ユーザ確認
            $user = User::where('email', $googleUser->email)->first();

            if ($user) {
                // ③ 既存ユーザに Google ID を紐付け
                $user->update([
                    'google_id' => $googleUser->id,
                ]);
            } else {
                // ④ 完全新規ユーザ
                $user = User::create([
                    // Googleの表示名は本名の可能性があるため仮名を生成
                    'name' => 'リクエスとん' . substr(Str::uuid(), 0, 6),
                    'email' => $googleUser->email,
                    'google_id' => $googleUser->id,
                    'password' => Hash::make(Str::random(40)),
                ]);
            }
        }

        Auth::login($user);

        return redirect()->route('dashboard');
    }
}

Userテーブルに google_id を追加

return new class extends Migration
{
    public function up(): void
    {
        Schema::table('users', function (Blueprint $table) {
            $table
                ->string('google_id')
                ->nullable()
                ->unique()
                ->after('email')
                ->comment('Google OAuth ID');
        });
    }

    public function down(): void
    {
        Schema::table('users', function (Blueprint $table) {
            $table->dropUnique(['google_id']);
            $table->dropColumn('google_id');
        });
    }
};

Userモデル修正

protected $fillable = [
    'name',
    'email',
    'password',
    'last_login_at',
    'google_id',
    'is_delete',
];

protected $hidden = [
    'email',
    'password',
    'remember_token',
    'last_login_at',
    'google_id',
    'is_delete',
];

// Googleログイン済みかどうか判定用
protected $appends = ['has_google_login'];

public function getHasGoogleLoginAttribute()
{
    return !is_null($this->google_id);
}

web.php にルーティング追加

Route::get('/auth/google', [GoogleLoginController::class, 'redirect'])
    ->name('auth.google');

Route::get('/auth/google/callback', [GoogleLoginController::class, 'callback']);

フロント(ログイン / 登録画面)

<div class="mt-8 flex justify-center">
	<a :href="route('auth.google')" class="gsi-material-button w-full max-w-[300px]">
		<div class="gsi-material-button-state"></div>
		<div class="gsi-material-button-content-wrapper">
			<div class="gsi-material-button-icon">
				<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"
				xmlns:xlink="http://www.w3.org/1999/xlink" style="display: block;">
					<path fill="#EA4335"		
					d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z">
					</path>		
					<path fill="#4285F4"
					d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z">
					</path>
					<path fill="#FBBC05"
					d="M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 16.46 0 20.12 0 24c0 3.88.92 7.54 2.56 10.78l7.97-6.19z">
					</path>
					<path fill="#34A853"
					d="M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z">
					</path>	
					<path fill="none" d="M0 0h48v48H0z"></path>
				</svg>
			</div>
			<span class="gsi-material-button-contents">Googleで続行</span>
			<span style="display: none;">Googleで続行</span>
		</div>
	</a>
</div>

<div class="flex items-center my-6">
	<div class="flex-1 border-t border-gray-200"></div>
		<span class="px-3 text-xs text-gray-400">または</span>
	<div class="flex-1 border-t border-gray-200"></div>
</div>

<!-- メールやその他のログイン方法(省略) -->

ボタンのコードは公式ガイドラインから取得

https://developers.google.com/identity/branding-guidelines?hl=ja

CSSは別ファイルに切り出し。

resources/css/app.css

@import './google-button.css'; // <- 追加.
@tailwind base;
@tailwind components;
@tailwind utilities;

ログイン方法による分岐(Inertia)

<script setup>
import { usePage } from '@inertiajs/vue3'
import { computed } from 'vue'

const page = usePage()
const user = computed(() => page.props.auth.user)
const hasGoogleLogin = computed(() => user.value?.has_google_login)
</script>

<template>
    <div v-if="hasGoogleLogin">
        Googleログイン中
    </div>
    <div v-else>
        その他のログイン方法
    </div>
</template>

Google Cloud 側の設定

https://console.cloud.google.com/

Google Cloud Console で以下を設定。

  1. APIとサービス → 認証情報
  2. OAuth クライアント ID を作成
  3. 種別:ウェブアプリ
  4. リダイレクト URI に
    /auth/google/callback を指定
    .env の GOOGLE_REDIRECT_URI と 完全一致 させるのが重要。

まとめ

  • Socialite を使えば実装自体はかなり素直
  • 既存ユーザとの email紐付け を入れておくとUXが良い
  • Googleの表示名は本名率高めなので注意
  • stateless() はローカル環境で特に助かる

「とりあえずGoogleログインを入れたい」場合の参考になれば。

Discussion