iT邦幫忙

0

筆記- Laravel 前後端分離下的 LINE Login 驗證機制

前言

本篇主要討論前端使用 LINE Login 下如何與後端做身份驗證


Laravel 作為 後台網站 兼 前台網站的API Server

前台網站主要應用在 LINE 上面,藉由 LIFF (LINE Front-end Framework) 可以讓使用者不用額外開啟瀏覽器就能使用網頁的功能。所以我選用 Vue.js,並使用 Quasar 這個 Vue框架。 會這樣用的原因,因為用 express 寫API Server 沒有 Eloquent ORM … 在寫存取 SQL 的資料部分會比較麻煩。

* 不用Laravel Vue的原因是因為 Quasar 東西很多,用引入的使用起來較爲複雜,且之後若要包成PWA也相對麻煩。


直覺上的做法

由於是透過 OAuth 登入的,我們資料庫不會存 User 的帳號密碼,只能透過驗證 LINE 給的 token 來知道使用者是否登入。

當前端發請求的時候帶上 LINE 所給的 accessToken,後端收到請求後先透過 LINE 的 Verify API 去檢查這個 accessToken 是否正確,然後再做接下來的處理。

缺點:每個請求都要等 LINE API 的回應才能執行,導致回應時間過久。


現在的做法

為了改善上面的缺點,因此要加上一層自己的驗證機制。

大致上的流程會像下面這樣

登入流程:

  1. 進入前端頁面時 拿 LINE 給的 accessToken 及 userId 到後端做登入。
  2. 後端透過 LINE Verify API 驗證 accessToken 是否正確,並檢查資料表有沒有這個 User。
  3. 拿 UserId 簽發 JWT (JSON Web Token) 並回傳至前端。
  4. 前端將 token 保存在 Vuex 或是 LocalStorage 等等。

平常的溝通流程:

  1. 前端發送請求時在 Header 帶入我們後端簽發的 Token
  2. 後端在 MiddleWare 時就驗證該 Token 是否正確。
  3. 再透過 Controller 做對應的動作及回應。

實作開始

為了達成上面的流程,我們會建立以下幾個檔案:

// 寫一個 Helper Function 之後調用比較方便
app/Libraries/Token.php

// Middleware 用來攔截每個請求,才不用在每個 function 都寫一次驗證
app/Http/Middleware/LineLoginAuth.php

// 負責登入的 Controller
// 因為我 API 的東西有多一層 API 的資料夾,不一定要照著我放
app/Http/Controllers/Api/LineAuthController.php


先從安裝套件開始

composer require lcobucci/jwt

app/Libraries/Token.php

<?php
use Lcobucci\JWT\Builder;
use Lcobucci\JWT\Signer\Key;
use Lcobucci\JWT\Signer\Hmac\Sha256;
use Lcobucci\JWT\Parser;
use Lcobucci\JWT\ValidationData;

class Token
{

    public static function createToken($uid = null) 
    {

        $signer = new Sha256();
        $time = time();
        $token = (new Builder())->issuedBy('https://example.com') // Configures the issuer (iss claim)
                        ->permittedFor('https://example.com') // Configures the audience (aud claim)
                        ->identifiedBy('123456789', true) // Configures the id (jti claim), replicating as a header item
                        ->issuedAt($time) // Configures the time that the token was issue (iat claim)
                        // ->canOnlyBeUsedAfter($time + 60) // Configures the time that the token can be used (nbf claim)
                        ->expiresAt($time + 86400) // Configures the expiration time of the token (exp claim)
                        ->withClaim('uid', $uid) // Configures a new claim, called "uid"
                        ->getToken($signer, new Key('自定義金鑰')); // Retrieves the generated token
        return (String) $token;     
    }

    public static function validateToken($tokan = null) 
    {
        try {
            $token = (new Parser())->parse((String) $tokan);
            $signer =  new Sha256();
            if (!$token->verify($signer, '自定義金鑰')) {
                return false;
            }
  
            $time = time();
            $data = new ValidationData(); // It will use the current time to validate (iat, nbf and exp)
            $data->setIssuer('https://example.com');
            $data->setAudience('https://example.com');
            $data->setCurrentTime($time);
            return $token->validate($data);
        } catch (Exception $e) {
            return false;
        }
      
    }

    public static function getUid ($tokan = null)
    {
        try {
            $token = (new Parser())->parse((String) $tokan);
            $claims = $token->getClaims();
            return $claims['uid'];
        } catch (Exception $e) {
            return false;
        }
    }
}

建立 Libraries 記得要修改 composer.json 裡的 autoload

"autoload": {
  "psr-4": {
    "App\\": "app/"
  },
  "classmap": [
    "database/seeds",
    "database/factories",
    "app/Libraries" // <<<<<<<< 加上這行
  ]
}

還有執行

composer dump-autoload

app/Http/Middleware/LineLoginAuth.php

<?php

namespace App\Http\Middleware;

use Closure;
use Token;
use Helper;

class LineLoginAuth
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        $token = $request->header('X-token');
        if (!$token) { return 'token not found'; }
        if (Token::validateToken($token)) {
            // 將 uid 加到原本的請求內,方便之後取用
            $request['token_uid'] = Token::getUid($token);
            return $next($request);
        } else {
            return 'token verify error';
        }
    }
}

建立 Middleware 記得到 app/Http/Kernel.php 加入

protected $routeMiddleware = [
...
'lineAuth' => \App\Http\Middleware\LineAuth::class
...
];

app/Http/Controllers/Api/LineAuthController.php

<?php

namespace App\Http\Controllers\Api;

use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use Token;
use App\Member;
use Helper;

class LineAuthController extends Controller
{
    public function login (Request $request) {
        $member_id = $request['userId'];
        $access_token = $request['accessToken'];

        // 先檢查有沒有這個 MEMBER
        $member = Member::find($member_id);
        if ($member) {
            // LINE 的 token 驗證
            $url = 'https://api.line.me/oauth2/v2.1/verify?access_token=' . $access_token;
            $ch = curl_init();
            curl_setopt($ch, CURLOPT_URL, $url);
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
            $output = json_decode(curl_exec($ch));
            curl_close($ch);
            if (isset($output->error)) { return Helper::response(401, 'error', [], $output->error_description); }
            if ($output->client_id === env('CLIENT_LIFF_CHANNEL')) {
                // 判斷 accessToken 的 LIFF ChannelID 是不是跟我們 env 設置的一樣
                $token = Token::createToken($member_id);
                return Helper::response(200, 'success', $token);
            }
        } else {
            return Helper::response(401, 'error', [], 'Member not found');
        }
    }
}

然後在 router.php 補上就大功告成啦

Route::middleware('lineAuth')->group(function () {
 // 要應用驗證機制的 Route 都放這裡
});
// 登入用的 Route 要放在外面哦
Route::get('/login', 'Api\LineAuthController@login');

此文章同步發表於 Medium


尚未有邦友留言

立即登入留言