Laravel Socialite 實作前後端分離的第三方登入 API

hms5232
15 min readFeb 21, 2021

--

本文使用網站的 FB 登入做示範
採用 Laravel 8 + Socialite 5
使用 Session 記錄狀態

不同版本可能會有些許語法及方法上的差異,請自行調整

前言

最近因為碰到需要實作 OAuth 第三方登入的需求,只好把之前隨便看看的東西撿回來研究並實作。不過我找到多數現存的中文文章都是前後端整合的寫法,在成功做出來後,寫一篇自己記錄一下兼分享給日後有需要的人參考。

前置作業

安裝 PHP

請依照欲使用的 Laravel 版本來安裝 PHP,比較舊的 Laravel 用新的 PHP 會有問題;反之亦然。

安裝 composer

composer 是 PHP 的套件管理器,請自行至 composer 官網依照環境對應的指示下載安裝。

一個 Laravel project

如果還沒有 Laravel 專案的請使用下列方法開一個吧:

composer create-project laravel/laravel {your-project-name}

別忘記把一些資料庫相關設定加進去,否則就跑不動囉。

本教學文範例

開始之前先給大家本次教學文範例的程式碼倉庫吧:

https://github.com/hms5232/laravel-api-socialite-fb-login

範例採用 Apache 授權(如有變更,以程式碼倉庫最新狀態為準),希望大家取之於社群,也別忘回饋於社群。教學相長。

初始狀態我設定在已經修改好一些專案環境的情況,此時大概是這個提交的狀態:https://github.com/hms5232/laravel-api-socialite-fb-login/commit/25150cc948f21f8a94e75071416b88a1086da5b3

那就準備開始吧~

前置作業(Auth 相關)

是的沒看錯,因為本次範例使用的 Laravel 8 沒有 make:auth 指令了(查了資料說是從 Laravel 6 開始移出去獨立的),為了方便快速建立相關的 Controller 及資料夾,故要做一些前置步驟(當然你覺得沒差要自己寫也可以,只是我比較懶惰希望可以快速產生)。如果已經有 Auth 相關的 Controller 的話,可以直接跳至下一步。

這邊我們使用 Laravel 官方套件 ui:

composer require laravel/ui --dev
php artisan ui vue --auth

安裝完之後,應該就有 Auth資料夾及相關的登入及註冊 Controller。

此時 commit:https://github.com/hms5232/laravel-api-socialite-fb-login/commit/959c2a72fb733562b63dbf1a8ff98cafa8865b3b

安裝與設定 Socialite

Socialite 是 Laravel 官方的套件,支援 Google、Github、Twitter、Gitlab、Facebook、linkedin、bitbucket 等網站的第三方登入(可能依版本不同有不同的支援度,請以官方文件為主)。當然這種作法可能有缺點,但這邊不討論,有興趣可以查看這篇大大寫的文章

安裝方式很簡單:

composer require laravel/socialite

如果安裝過程發生錯誤,代表 Laravel 版本太就不被最新版 Socialite 支援,請去官方 Github 查 release log 確認最後支援的版本並重新嘗試安裝。

例如:Laravel 5.7 及 5.8 在 Socialite 5 之後不再被支援,故只能使用 4.4.1 或以下之版本

composer require laravel/socialite:4.4.1

安裝好了之後接著要為 Socialite 做一些設定,請在 .env.env.example 中加入下面幾項設定:

FB_CLIENT_ID=
FB_CLIENT_SECRET=
# 重新導向的網址請依照自己的環境及 URI 填寫
FB_REDIRECT=http://localhost:8000/auth/facebook-login-callback

內容要填入什麼呢?晚點就知道啦。

接著要註冊 Socialite,請至 config/app.php 新增:

Laravel\Socialite\SocialiteServiceProvider::class,

如果有需要的話也可以順便設定 alias:

'Socialite' => Laravel\Socialite\SocialiteServiceProvider::class,

然後在 config/services.php 新增:

'facebook' => [
'client_id' => env('FB_CLIENT_ID'),
'client_secret' => env('FB_CLIENT_SECRET'),
'redirect' => env('FB_REDIRECT'),
],

注意:key 一定要是 Socialite 所指定的名稱!支援哪些請至官方文件查詢。

此時 commit:https://github.com/hms5232/laravel-api-socialite-fb-login/commit/4289014144e88e0ade1f3fb5f6bbed2dabdfafc2

Facebook 設定

這邊步驟有經驗的人來說可能簡單,不過有 GUI 大致上還算簡單,不要被一堆文字搞混亂即可。另外,因為臉書不斷在改版,畫面可能會有些許不同,但邏輯是相同的。

首先,先去 FB 開發人員專區(沒註冊過開發人員的會被要求註冊)建立一個新的應用程式:

Facebook 建立新應用程式的類型選擇清單
要選哪個應該很顯而易見吧

接著填寫應用程式名稱、聯絡的電子信箱地址等資訊,這邊不困難就不截圖囉。填寫完成後就完成建立新的應用程式了。

下一步應該也很顯而易見,就是設定我們要的 Facebook login 啦:

如果是使用現有應用程式的朋友,請直接進入現有應用程式的主控台點選圖片紅框處選項

接下來會要你選擇是要在什麼類型的 Client 端使用 FB 登入,因為本文範例是使用網頁,所以選擇「網站」

下一步會要你輸入網址,這邊應為是開發用的,請依照開發環境設定即可,正式上線前都可以修改不用擔心

這邊輸入 Laravel 開發用 local server 預設網址及連接埠

接下來的步驟可以不用理會,如果有需要的同學可以自己去看那些東西寫的是什麼,是否有符合自己的需要。

將應用程式的設定加入 Socialite

這個看圖就能懂了吧,不多說嚕

臉書應用程式編號位置示意圖
FB_CLIENT_ID 位置
FB_CLIENT_SECRET 位置

撰寫 API 及商業邏輯

如果對 OAuth 不熟的朋友,可以先去了解下這個到底是啥,沒興趣的話,至少知道使用者去 FB 登入,以及之後的跳轉都是 GET 進行的(但回呼之後前後端需要 POST 溝通一次才能加上 credentials 寫入登入狀態)。

資料庫新增欄位記錄

這部份看大家的需求,通常我會建議記住,方便之後有需要可以操作。

首先下個指令來產生一個 migration :

php artisan make:migration AddFbIdColumnToUsersTable

然後寫一下加入要新增的欄位:

public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->string('fb_id', 30)->nullable()->comment('使用者的臉書 ID'); // 加入這行
});
}

別忘了跑 php artisan migrate 才會新增。

還有記得將欄位寫入 model 否則沒辦法記錄進去資料庫:

protected $fillable = [
// 省略
'fb_id',
];

此時 commit:https://github.com/hms5232/laravel-api-socialite-fb-login/commit/6b151efa1bda73a43d48327571dc6453e947e5b6

撰寫 API 與商業邏輯

由於這些都是吃 session 的,所以 route 都會寫在 route/web.php 中。

首先,原本的東西都沒用,通通註解吧~然後加上兩條 route URI:

// FB 登入
Route::get('/auth/facebook-login', [App\Http\Controllers\Auth\LoginController::class, 'fbLogin']);
// FB 登入 callback
Route::get('/auth/facebook-login-callback', [App\Http\Controllers\Auth\LoginController::class, 'fbLoginCallback']);

修改 app\Http\Controllers\Auth\LoginController.php 來告訴前端需要轉去哪裡登入 FB 以及之後回呼回來的處理:

// 省略
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Session;
use Laravel\Socialite\Facades\Socialite;
// 省略
// 註解 middleware
// 省略
/*
|--------------------------------------------------------------------------
| Facebook Login
|--------------------------------------------------------------------------
*/
public function fbLogin(Request $request)
{
$redirect_url = Socialite::driver('facebook')
->scopes(['email']) // 額外要求要使用者的電子信箱地址
->redirect()->getTargetUrl();
return response()->json(['target' => [$redirect_url]], 302);
}
public function fbLoginCallback(Request $request)
{
if (is_null($request['code']) || is_null($request['state'])){
return response()->json(['messages' => ['授權失敗']], 401);
}
Session::put('state', $request['state']); // 不寫入 session 就要 stateless
try{
$fb_user = Socialite::driver('facebook')->user();
} catch (\Exception $exception){
Log::error($exception);
return response()->json(['messages' => [strval($exception)]], 401);
}
// 確認使用者是否已經使用此方法註冊過
if (User::where('facebook_id', $fb_user->getId())->exists()){ // 有
// 登入
Auth::guard('web')->login(User::where('facebook_id', $fb_user->getId())->first());
} else if (User::where('email', $fb_user->getEmail())->exists()){ // 有相同 email 的使用者
// 更新使用者資料
DB::transaction(function () use ($fb_user){
User::where('email', $fb_user->getEmail())->update([
'facebook_id' => $fb_user->getId()
]);
});
Auth::guard('web')->login(User::where('email', $fb_user->getEmail())->first());
}else { // 沒找到
// 自動註冊
DB::transaction(function () use ($fb_user){
User::create([
'name' => $fb_user->getName(),
'email' => $fb_user->getEmail(),
'password' => Hash::make(uniqid('FB_')),
'facebook_id' => $fb_user->getId()
]);
});
Auth::guard('web')->login(User::where('email', $fb_user->getEmail())->where('facebook_id', $fb_user->getId())->first());
}
return response()->json(['messages' => ['登入成功!']], 200);
}

不好意思排版有些歪掉,建議上範例的 Github repo 看比較不傷眼。

簡單說明一下, fbLogin 這個 function 是前端跟我 API 說,使用者想要使用 FB 登入,API 這邊就會產生一組重新導向的網址給前端,前端轉過去給使用者登入 FB 後,FB 會丟 callback 回來,此時 GET 的網址就是剛剛在 .env 中設定的環境變數 FB_REDIRECT 設定的,因此這邊要看各位怎麼實作前後端溝通來改變這個流程(總不能回呼直接呼叫 API ,那使用者就看著 200 的 json 畫面發呆吧)

FB 丟回來之後會收到 codestate 兩個參數(失敗的話就變成 error_codeerror_message 了),之後傳到後端再拿著去和 FB 要使用者資料,要到之後就是你的商業邏輯處理部分囉。

範例是寫成:

  1. 有符合的 fb_id :直接登入。
  2. 沒有 fb_id 但有重複的 email:幫使用者連結帳號後登入。
  3. 以上都沒有:註冊新帳號後登入。

此時 commit:https://github.com/hms5232/laravel-api-socialite-fb-login/commit/266ee2c3b666eae493561fdc2167c8590f35de4b

參考資料

請見 Repo 的 README.md

結語

一開始有這個需求的時候覺得很複雜就沒研究,直到最近才撿回來認真研究,發現其實都有套件幫你包好一堆事情了,果然人懶惰的話就有一堆藉口XD 感謝大家這次收看,前端好難。

慣例的聲明

本文為作者親手撰寫,如有轉載請註明出處。有任何問題歡迎直接留言,或是在 repo 發 issue/discussion 。若有幫助的話,歡迎幫忙拍手、按讚或是星星/fork一下 repo,有想斗內一下的也歡迎~

--

--