本篇主要討論前端使用 LINE Login 下如何與後端做身份驗證
前台網站主要應用在 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 的回應才能執行,導致回應時間過久。
為了改善上面的缺點,因此要加上一層自己的驗證機制。
大致上的流程會像下面這樣
LINE Verify API
驗證 accessToken 是否正確,並檢查資料表有沒有這個 User。JWT (JSON Web Token)
並回傳至前端。Vuex
或是 LocalStorage
等等。Header
帶入我們後端簽發的 Token
。MiddleWare
時就驗證該 Token
是否正確。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
<?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
<?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
...
];
<?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');