步骤一:申请facebook开发应用程式,在『 产品 』中新增facebook登入。
步骤二:在专案目录下安装facebook SDK,并开始撰写程式
安装facebook SDK套件composer require facebook/graph-sdk
.env加入应用程式id、应用程式密钥第三方登入大致流程:
使用者点下『 facebook登入 』连结,会发request到server端,而server端则会回传fb登入画面连结,query string包括client_id, state(类似CSRF token的作用), response_type, sdk版本, redirect_uri( call-back api), scope(要求的使用者资料範围)等,让前端显示。登入成功后,client端这时会发送一个GET的请求到server的call-bac这支api(也就是刚刚在query string的redirect_uri),query string包含code(这位使用者成功登入后所拿到的验证码)和state,在这个档案中拿到使用者access token,再发送请求向fb换取使用者meta data。后端储存(第一次登入)或更新使用者资料进DB,告知前端使用者登入成功并回传后端产生的api_token和使用者资料。看图比较快
整个过程的重点就是,让使用者能以facebook的帐号登入,让后端能储存使用者的facebook资料,后端再把自己产生的api_token给前端。
facebook OAuth 官方文件有提供详细的範例程式码及说明,因此不在此说明程式码的部分。
不过要特别注意的是,程式码中必须在开头加上seesion_start(),在跳转页面时才能将state参数内容传到下一个网页,否则会出现CSRF问题。
步骤三:部署程式码至云端,并将应用程式改成已上线状态
应用程式状态分为已上线与调整中。
应用程式上线前置作业:隐私政策网址(可用github page製作)和在设定中加入有效的 OAuth 重新导向 URI。
在『 调整中 』阶段,只提供我们以已授权的开发人员fb帐号登入测试,此阶段可以暂时以localhost来作为测试网域;一旦将状态改成已上线后,就必须使用https开头的URL。
当使用者成功登入facebook后,会发请求到call-back这裏,也就是这边要填写的OAuth 重新导向 URI。这个跳转的网址其实是由前端带的,而facebook的开发者页面之所以要求要填入这串,目的是为了确保前端带上的redirect_url是否属于application server这的。每次跳转,fb都会检查这个网址有没有存在于有效的 OAuth 重新导向 URI这个栏位里。
看一下call-back这个method
//登入成功后,以code换取使用者AccessToken public function fbCallback() { session_start(); //to deal with CSRF $fb = new Facebook([ 'app_id' => env('FB_CLIENT_ID'), 'app_secret' => env('FB_CLIENT_SECRET'), 'default_graph_version' => 'v3.2', ]); $helper = $fb->getRedirectLoginHelper(); try { $accessToken = $helper->getAccessToken(); } catch (FacebookResponseException $e) { // When Graph returns an error return response()->json('Graph returned an error: ' . $e->getMessage(), 400); } catch (FacebookSDKException $e) { // When validation fails or other local issues return response()->json('Facebook SDK returned an error: ' . $e->getMessage(), 400); } if (!isset($accessToken)) { if ($helper->getError()) { return response()->json( "Error: " . $helper->getError() . "\n" . "Error Code: " . $helper->getErrorCode() . "\n" . "Error Reason: " . $helper->getErrorReason() . "\n" . "Error Description: " . $helper->getErrorDescription() . "\n" , 401); } else { return $this->sendError('Bad request', 400); } } try{ // 若登入成功,拿accessToken来换使用者资料 $login = $this->login($accessToken); return View::make('layout.loginSuccess')->with('user', $login); }catch (Exception $e){ return view('layout.loginFailed'); } } //换取使用者资料,将以储存或更新,并在server端产生新的token(for 应用程式) public function login($token) { date_default_timezone_set('Asia/Taipei'); $fb = new Facebook([ 'app_id' => env('FB_CLIENT_ID'), 'app_secret' => env('FB_CLIENT_SECRET'), 'default_graph_version' => 'v3.2', ]); $endpoint = env('FBEndpoint'); $response = $fb->get($endpoint, $token); $resource = $response->getGraphUser(); if (count(User::where('account', $resource['id'])->get()->toArray()) == 0) { $create = User::create([ 'name' => $resource['name'], 'access_token' => $token->getValue(), 'account' => $resource['id'], 'password' => 'facebook', 'api_token' => Str::random(20), 'image' => $resource['picture']['url'], 'point' => 0, 'bad_record' => 0, ]); return $create; } else { $user = User::where('account', $resource['id'])->first(); $user->update([ 'name' => $resource['name'], 'api_token' => Str::random(20), 'access_token' => $token->getValue(), 'image' => $resource['picture']['url'], ]); return $user; } }
登入成功的话,就能拿到向fb请求的使用者metadata了。
实作fb Oauth踩到的坑:跨域问题
所谓的跨域问题是当client端向server请求资源时,会先发一个preflighted(预检请求),先发送 OPTIONS 请求进行确认,浏览器会检查server端和client端的网域是否一致,若不一致则会阻止client端发送接下来的request。这个机制是浏览器为了避免使用者受到钓鱼网站等攻击而设计的,但对于开发人员来说,在测试阶段会是个很麻烦的问题。
由于这个专案在开发时,只有我(后端)的部分的程式码有部署到云端,而和我合作的前端是在本机测试串接第三方登入的,因此我们前后端在不同的网域,被跨域问题搞了很久。由于OAuth的流程是:
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.当使用者输入完密码,若登入成功,client端会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的方式拿到资料:
前端程式码
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>