Laravel × Nuxt.jsのGoogleログイン実装方法

Laravel

こんにちは!

さて、今回はLaravel × Nuxt.jsでのGoogleログイン方法を解説していきます。

Laravelには、「Laravel Socialite」というSNSログイン用の便利なライブラリが用意されているので、今回はこちらを使っていきます。

Laravel Socialite公式ドキュメント

技術構成

  • Laravel 9.19
  • Nuxt.js 3.8.1
  • Composer 2.6.5

下準備

それでは、早速実装してみましょう。

まずは下準備から行なっていきます。

ライブラリのインストール

まずは、Laravel Socialiteをcomposer経由でインストールしていきます。

composer require laravel/socialite

Googleの設定

今回はGoogleの事前準備が必要になります。

設定はGoogle Developer Consoleにアクセスして行います。

下記の記事がわかりやすいので、こちらを参考に設定してください。

Laravel で作ったアプリに Google アカウントでログインする方法(OAuth 2.0)

今回はコールバックURLを「http://localhost:3000/login/googleCallback」としておきます。

Laravelの設定

次に、Laravel側でOAuthプロバイダの認証情報を設定しておく必要があります。

設定情報は`config/services.php`に書きます。

Googleの場合は以下のようになります。

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

環境変数に各認証情報を記載しておきます。

GOOGLE_CLIENT_ID=******
GOOGLE_CLIENT_SECRET=******
GOOGLE_CALLBACK=http://localhost:3000/login/googleCallback

ログイン認証処理の実装

それでは、これから実際にログインのコードを書いていくわけですが、その前にきちんと設計してみましょう。

全体の処理の流れは以下のようになります。

  1. 「Googleアカウントでログイン」ボタンを押す
  2. GoogleへのリダイレクトAPIが実行される
  3. Googleのアカウント選択画面に遷移する
  4. Google側でアカウントを選択する
  5. .envで指定したコールバックURLに返される
  6. 遷移先でコールバックAPIを実行する
  7. Google側からのユーザー情報を受け取って、認証処理を走らせる
  8. 認証通過or不通過

API側はGoogleのアカウント選択画面へ遷移するためのリダイレクトAPIと、コールバックを受け取った後に認証を行うためのコールバックAPIの2つが必要になります。

まずはこれらのAPIを作っていきましょう。

API側

まずは、ルーティングからです。

// ソーシャルログイン
Route::get('/login/{provider}', 'App\Http\Controllers\SocialController@redirect')->name('social_login');
Route::get('/login/{provider}/callback', 'App\Http\Controllers\SocialController@callback')->name('social_login_callback');

今回はusersテーブルにprovider_idとprovider_nameを保存しておきたいので、カラムを追加しておきます。
また、パスワードがデフォルトで必須になっていますが、Googleログインではパスワードを保存しないため、null許容にしておきます。

public function up()
{
    Schema::create('users', function (Blueprint $table) {
        $table->id();
        $table->string('name');
        $table->string('email')->unique()->nullable();
        $table->timestamp('email_verified_at')->nullable();
        $table->string('password')->nullable();
        $table->string('provider_id')->nullable();
        $table->string('provider_name')->nullable();
        $table->rememberToken();
        $table->timestamps();
        $table->softDeletes();
    });
}

Userモデルのfillableにも登録しておきます。

protected $fillable = [
    'name',
    'email',
    'password',
    'provider_id',
    'provider_name',
];

次に、コントローラを作成していきます。

php artisan make:controller SocialController
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

// Facades
use Laravel\Socialite\Facades\Socialite;
use Illuminate\Support\Facades\Auth;

// Models
use App\Models\User;

class SocialController extends Controller
{
    public function redirect($provider)
    {
        $redirect_url = Socialite::driver($provider)->redirect()->getTargetUrl();
        return response()->json($redirect_url);
    }

    public function callback($provider)
    {
        $user_from_social = Socialite::driver($provider)->user();
        $user = User::where('email', $user_from_social->getEmail())->first();

        if ($user) {
            Auth::login($user);
        } else {
            $user = User::create([
                'name' => $user_from_social->getName(),
                'email' => $user_from_social->getEmail(),
                'password' => bcrypt('password'),
                'provider_id' => $user_from_social->getId(),
                'provider_name' => $provider,
            ]);
            Auth::login($user);
        }

        return response()->json([
            'user' => $user,
            'access_token' => $user->createToken('user')->plainTextToken,
        ]);
    }
}

redirectメソッドに関して、通常Laravel単体のプロジェクトの場合は、以下のようになります。

return Socialite::driver($provider)->redirect();

ただし、今回はクライアント側にNuxt.jsを採用しているため、サーバー側でリダイレクトすることができません。

そのため、以下のようにAPI側ではリダイレクトURLを返却し、クライアント側でリダイレクトすることにします。

$redirect_url = Socialite::driver($provider)->redirect()->getTargetUrl();
return response()->json($redirect_url);

また、データベースに該当のユーザーがいなかった場合はユーザーを新規で登録するようにしています。

さて、これでサーバー側の実装は完了しました。

次に、クライアント側の実装に移っていきます。

クライアント側

まずはGoogleログインのボタンを作っていきます。

<button @click="googleLogin" type="button" class="flex justify-center gap-2 items-center shadow-xl border border-slate-700 rounded-full px-8 py-4">
    <i class="fa-brands fa-google"></i>Googleアカウントでログイン
</button>

CSSについては割愛しますが、TailwindCSSを使っています。

async function googleLogin()
{
    const url = `${runtimeConfig.public.baseUrl}/api/login/google`;
    const headers = {
        'Content-Type': 'application/json',
        'X-XSRF-TOKEN': csrfToken.value
    }
    await fetch(url, {
        method: "GET",
        headers: headers,
        credentials: "include",
    }).then((res) => {
        return res.json();
    }).then((data) => {
        console.log(data);
        window.location.href = data; // リダイレクト
    });
}

あとは、リダイレクトAPIを叩いてレスポンスからGoogleへのリダイレクトURLを受け取り、リダイレクトさせます。

リダイレクトされた後は、Googleのアカウント選択画面に遷移し、選択されたアカウント情報をクエリパラメータにつけてコールバックURL(アプリ側)に返されます。

コールバックURLは、.envとGoogle Developer Consoleで指定したURLになります。

後はコールバックの遷移先でアカウント情報をリクエストに含めてコールバックAPIを叩けば、認証処理が走ります。

<template>
    <div class="card">
        <h1>認証中</h1>
        <img class="loading-img" src="@/assets/images/loading2.svg" alt="ローディング">
    </div>
</template>

<script setup>
const runtimeConfig = useRuntimeConfig();
const router = useRouter();
const route = useRoute();
const store = useGoogleStore();
const csrfToken = useCookie('XSRF-TOKEN');
const { $toast } = useNuxtApp();

onMounted(async() => {
    const params = route.query;
    const query_params = new URLSearchParams(params);
    const url = `${runtimeConfig.public.baseUrl}/api/login/google/callback?${query_params.toString()}`;
    await fetch(url, {
        method: "GET",
        headers: {
            'Content-Type': 'application/json',
            'X-CSRF-TOKEN': csrfToken.value
        },
        credentials: "include",
    }).then((res) => {
        return res.json();
    }).then((data) => {
        console.log(data);
        store.token = data.token;
        store.user = data.user;
        router.push('/event');
        $toast.success('ログインしました');
    });
})
</script>

<style scoped>
.card {
    width: 100vw;
    height: 100vh;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    z-index: 99;
    position: fixed;
    top: 0;
    left: 0;
    background-color: white;
}
h1 {
    font-size: 30px;
}
.loading-img {
    width: 70px;
    height: 70px;
}
</style>

これで完成!と言いたいところですが、このままだとおそらくCORSエラーになります。

CORSについては、他のいろいろな記事で詳しく解説されていますので、調べてみてください。

CORSエラーってなに?どうすれば解決するの?

要するに、あるオリジンから異なるオリジンにアクセスしようとすると、セキュリティ上の理由からエラーになる、ということです。

オリジンは、今回の場合で言うとhttp://localhost:3000とhttp://localhost:8000になります。

このCORSエラーを解決するためには、Laravelの`config/cors.php`を設定すればよいです。

<?php

return [

    /*
    |--------------------------------------------------------------------------
    | Cross-Origin Resource Sharing (CORS) Configuration
    |--------------------------------------------------------------------------
    |
    | Here you may configure your settings for cross-origin resource sharing
    | or "CORS". This determines what cross-origin operations may execute
    | in web browsers. You are free to adjust these settings as needed.
    |
    | To learn more: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
    |
    */

    'paths' => ['api/*', 'sanctum/csrf-cookie'],

    'allowed_methods' => ['*'],

    'allowed_origins' => ['*'], // http://localhost:3000でもOK

    'allowed_origins_patterns' => [],

    'allowed_headers' => ['*'],

    'exposed_headers' => [],

    'max_age' => 0,

    'supports_credentials' => true,

];

`allowed_origins`というのが重要で、これにクライアント側のオリジンを指定すれば受け入れるようになります。

はい、これで完成しました!

あとは、ログインボタンを押してみて動作確認してみましょう。

ログインが成功したら大成功です。

ではでは😊

コメント

タイトルとURLをコピーしました