iT邦幫忙

1

Laravel 實作 Facebook OAuth 第三方登入筆記

facebook 第三方登入

步驟一:申請facebook開發應用程式,在『 產品 』中新增facebook登入。

https://ithelp.ithome.com.tw/upload/images/20200225/20120024lpqMsTWWAq.png

步驟二:在專案目錄下安裝facebook SDK,並開始撰寫程式

  1. 安裝facebook SDK套件
composer require facebook/graph-sdk
  1. .env加入應用程式id、應用程式密鑰

第三方登入大致流程:

  1. 使用者點下『 facebook登入 』連結,會發request到server端,而server端則會跳轉到fb登入畫面,會回一串登入畫面的連結,query string包括client_id, state(類似CSRF token的作用), response_type, sdk版本, redirect_uri( call-back api), scope(要求的使用者資料範圍)等,讓前端顯示。
  2. 登入成功後,這時會發送一個GET的請求到server的fb-callback這支api,query string包含code(這位使用者成功登入後所拿到的驗證碼)和state(STP,類似CSRF token的作用),在這個檔案中拿到使用者access token,以向fb換取使用者meta data。
  3. 後端儲存(第一次登入)或更新使用者資料進DB,告知前端使用者登入成功並回傳後端產生的api_token和使用者資料。

整個過程的重點就是,讓使用者能以facebook的帳號登入,讓後端能儲存使用者的facebook資料,後端再把自己產生的api_token給前端。
facebook OAuth 官方文件有提供詳細的範例程式碼及說明,因此不在此說明程式碼的部分。
不過要特別注意的是,程式碼中必須在開頭加上seesion_start(),在跳轉頁面時才能將state參數內容傳到下一個網頁,否則會出現CSRF問題。

步驟三:部署程式碼至雲端,並將應用程式改成已上線狀態

應用程式狀態分為已上線與調整中。
應用程式上線前置作業:隱私政策網址(可用github page製作)和在設定中加入有效的 OAuth 重新導向 URI。
在『 調整中 』階段,只提供我們以已授權的開發人員fb帳號登入測試,此階段可以暫時以localhost來作為測試網域;一旦將狀態改成已上線後,就必須使用https開頭的URL。
當使用者點下『 facebook登入 』時,會跳轉到call-back.php這個檔案,而跳轉到這個檔案的路徑就是 OAuth 重新導向 URI。
每次跳轉,fb都會檢查這個網址有沒有存在於有效的 OAuth 重新導向 URI這個欄位裡。

https://ithelp.ithome.com.tw/upload/images/20200225/20120024t1p0xX7TnA.png

登入成功的話,就能拿到向fb請求的使用者metadata了。

跨域問題

所謂的跨域問題是當client端向server請求資源時,會先發一個preflighted(預檢請求),先發送 OPTIONS 請求進行確認,瀏覽器會檢查server端和client端的網域是否一致,若不一致則會阻止client端發送接下來的request。這個機制是瀏覽器為了避免使用者受到釣魚網站等攻擊而設計的,但對於開發人員來說,在測試階段會是個很麻煩的問題。
由於這個專案在開發時,只有我(後端)的部分的程式碼有部署到雲端,而和我合作的前端是在本機測試串接第三方登入的,因此我們前後端在不同的網域,被跨域問題搞了很久。由於OAuth的流程是:

  1. 當使用者點下facebook登入時,會發request到server端(call redirect這支api),告訴server使用者要透用facebook登入,此時server會跳轉到facebook登入的畫面讓client端顯示。
 function redirect()
    {
        session_start();
        $fb = new Facebook([
            'app_id' => env('FB_CLIENT_ID'),
            'app_secret' => env('FB_CLIENT_SECRET'),
            'default_graph_version' => 'v3.2',
        ]);
        $helper = $fb->getRedirectLoginHelper();
        $permissions = ['email']; // Optional permissions

        $loginUrl = $helper->getLoginUrl(env('FB_REDIRECT'), $permissions);
        return redirect($loginUrl); //跳轉到facebook登入畫面
    }

2.當使用者輸入完密碼,若登入成功,fb會自動callcall-back這支api(網址定義在env('FB_REDIRECT')裡),server端透過fb給的參數$code(裡面帶有該位使用者登入成功的資訊),在call-back的這個function中換取該使用者的access token,根據這個access token能換取使用者資料(帳號、姓名、大頭照等),將使用者資料存入資料庫後,再回傳給前端server這邊所存的使用者資料。
跨域問題就出在使用者送出帳號密碼,fb的sdk自動call server端的call-back時,就算server這邊的header有帶處理跨域問題的三個header(allow origin, allow method, allow headers),跳轉到fb的網頁時也還是會遇到跨域問題,因為facebook Oauth的SDK就是不會帶這些header,我們不能也不應該(安全性考量)幫他加上去。

Solution:

後來問了比較資深的前輩,得到的答案是,若是正式上線的情況,前後端的code應該會被部署到同個網域,因此理應不會有跨域問題,後端header帶allow origin,尤其value是" * "時是很危險的,表示無論哪個網域都能向server請求資源。

但是在測試階段,前後端的程式碼還沒部署在一起時怎麼辦?

由於前端不能把facebook的登入畫面直接嵌在自己刻的畫面裡,因此前端JavaScript要以window.postMessage的方式,開啟另一個視窗讓使用者登入facebook帳號,登入成功/失敗後,後端用postMessage的方式把使用者的metadata帶在header給前端拿。

postMessage是一個能在不同網頁之間傳送文字資料的方式,有點類似前端開一個母視窗,使用者登入成功後再開一個對應的子視窗,並且把資料帶在這個視窗的header中。

而且無論登入成功或失敗,顯示登入結果的畫面也要寫在後端,並把要給前端的資訊帶在header,才不會讓使用者看到,這樣才能避免call-back跑完後,跳轉回寫前端的網佔不會又碰到跨域問題。
⬇登入成功的畫面(使用者的meta data在header中),會是空白的是因為這個畫面必須由後端顯示,但又不能把使用者meta data直接暴露在畫面上,因此資料帶在header給前端拿,javascript以postWindow,對應後端postMessage的方式拿到資料:
https://ithelp.ithome.com.tw/upload/images/20200331/20120024QmbKJmTB36.png

前端程式碼

 const getFBLoginData = msg => {
    if (msg.origin === serverDomain) {
      if (msg.data.status === "success") {
        setToken(msg.data);
        localStorage.setItem("user_token", msg.data.api_token);
      } else {
        console.log("FB login error");
      }
    }
  };
  const getFB = () => {
    window.addEventListener("message", getFBLoginData);
    window.open(
      `後端redirect uri`,
      "_blank",
      "menubar=no,toolbar=no"
    );
  };

後端登入成功畫面的程式碼,登入成功的話,message放的就是使用者的meta data;登入失敗的話,則是回傳登入失敗的message讓前端知道。

<!DOCTYPE html>
<html>
<head>
    <title>Facebook Login Success</title>
    <meta charset="UTF-8">
</head>
<body>
<script>
    var message = new Object();

    message.status = 'success';
    message.id = '{{$user['id']}}'
    message.name = '{{$user['name']}}'
    message.account = '{{$user['account']}}'
    message.api_token = '{{$user['api_token']}}'
    message.image = '{{$user['image']}}'
    message.email = '{{$user['email']}}'
    message.point = '{{$user['point']}}'
    message.phone = '{{$user['phone']}}'

    window.opener.postMessage(message, '*');
    window.close();

</script>
</body>

</html>



尚未有邦友留言

立即登入留言