LINE 登入不只是串好就好!資深工程師帶你重構 Laravel x LINE Login,打造企業級認證架構
嗨,我是浪花科技的 Eric。很高興又在技術專欄見到大家。今天不談新功能,我們來聊聊「重構」與「架構」。
我知道,很多教學文,包含我們之前寫的那篇 Laravel x LINE Login 串接實戰,目標都是讓你「快速實現功能」。你跟著步驟,複製貼上,很快地,使用者就能透過 LINE 帳號一鍵登入你的網站。很有成就感,對吧?客戶看了也開心。
但,身為一個資深工程師(aka 在坑裡打滾多年的老鳥),我必須囉嗦幾句:「能動」跟「寫得好」是兩回事。當你的專案開始變大、需求開始變多(例如:「Eric,我們現在也要串 Google 登入」、「Eric,可以讓使用者綁定既有帳號嗎?」),你會發現當初寫在 Controller 裡的那些 LINE Login 邏輯,就像一盤義大利麵,盤根錯節,牽一髮而動全身。改 A 壞 B,新增功能比登天還難。這就是技術債,而且利息高得嚇人。
今天,我就要帶你跳出「能動就好」的思維,從架構層面重新思考 Laravel 與 LINE Login API 串接這件事,打造一個可維護、可擴展、可測試的企業級認證系統。
為什麼你的 Controller 正在哀嚎?
我們先來看看一個典型的「義大利麵式」程式碼會長怎樣。通常,你會在 `LoginController` 或類似的控制器裡,寫兩個方法:一個是 `redirectToLine()`,另一個是 `handleLineCallback()`。
// 這是一個「不推薦」的範例
class LoginController extends Controller
{
public function redirectToLine()
{
// ... 一堆產生 state、建立 LINE OAuth2 URL 的邏輯 ...
return redirect($authUrl);
}
public function handleLineCallback(Request $request)
{
// 1. 檢查 state 是否相符
// 2. 用收到的 code 去跟 LINE 換 access token
// 3. 用 access token 去拿使用者 profile
// 4. 檢查資料庫有沒有這個使用者 (依據 LINE User ID)
// 5. 如果沒有,就建立一個新使用者
// 6. 幫使用者登入系統 (Auth::login())
// 7. 導向到會員中心
// ... 還要處理各種錯誤,例如使用者拒絕授權、LINE API 掛掉 ...
}
}
看起來好像沒什麼問題?但仔細想想:
- 違反單一職責原則 (SRP):Controller 的職責應該是接收 HTTP 請求,並回傳 HTTP 回應。它不應該知道如何跟 LINE API 溝通、如何操作資料庫、如何處理使用者認證邏輯。這些都混在一起,就是災難的開始。
- 難以測試:你要怎麼測試 `handleLineCallback` 這個方法?你必須模擬一個完整的 HTTP 請求,還要 Mock 一堆外部依賴(GuzzleHttp、Session、Auth…)。光想就頭痛。
- 難以複用與擴展:如果今天想在另一個地方(例如 API)也使用 LINE 登入,難道要把整段邏輯複製貼上嗎?如果想加入 Google 登入,是不是要在同一個 Controller 裡塞入更多 `if/else`?
好了,抱怨結束。身為工程師,我們不只提出問題,更要解決問題。解決方案就是:服務層 (Service Layer)。
導入服務層 (Service Layer):為你的認證邏輯找個家
我們要做的第一件事,就是把所有跟 LINE Login 相關的髒活累活,全部從 Controller 抽離出來,放到一個專門的 Service Class 裡。這樣 Controller 就能保持乾淨,只做它該做的事。
第一步:建立 `LineLoginService`
我們在 `app/Services` 目錄下建立一個 `LineLoginService.php` 檔案。這個類別將會封裝所有與 LINE API 互動的細節。
<?php
namespace App\Services;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Auth;
use App\Models\User;
use Illuminate\Support\Str;
class LineLoginService
{
protected $clientId;
protected $clientSecret;
protected $redirectUri;
public function __construct()
{
$this->clientId = config('services.line.client_id');
$this->clientSecret = config('services.line.client_secret');
$this->redirectUri = config('services.line.redirect');
}
/**
* 產生導向到 LINE 登入頁面的 URL
* @return string
*/
public function getAuthorizationUrl(): string
{
$state = Str::random(40);
session()->put('state', $state);
$url = 'https://access.line.me/oauth2/v2.1/authorize?';
$params = [
'response_type' => 'code',
'client_id' => $this->clientId,
'redirect_uri' => $this->redirectUri,
'state' => $state,
'scope' => 'profile openid email',
];
return $url . http_build_query($params);
}
/**
* 處理從 LINE 回調的請求,並登入使用者
* @param string $code
* @param string $state
* @return User
* @throws \Exception
*/
public function handleCallback(string $code, string $state): User
{
if (empty($state) || ($state !== session()->pull('state'))) {
throw new \Exception('Invalid state');
}
$tokenData = $this->getAccessToken($code);
$lineProfile = $this->getLineProfile($tokenData['access_token']);
$user = $this->findOrCreateUser($lineProfile);
Auth::login($user, true);
return $user;
}
// ... 私有輔助方法 ...
}
你看,我們把產生 URL 和處理 Callback 的邏輯都放進來了。接下來,我們需要實作那些輔助方法。
第二步:完成 Service 內部邏輯
我們在 `LineLoginService` 裡繼續加上 `getAccessToken`、`getLineProfile` 和 `findOrCreateUser` 等方法。
<?php
// ... 接續上面的 LineLoginService.php
class LineLoginService
{
// ... __construct 和 getAuthorizationUrl ...
// ... handleCallback ...
/**
* 使用授權碼 (code) 換取 Access Token
* @param string $code
* @return array
* @throws \Exception
*/
private function getAccessToken(string $code): array
{
$response = Http::asForm()->post('https://api.line.me/oauth2/v2.1/token', [
'grant_type' => 'authorization_code',
'code' => $code,
'redirect_uri' => $this->redirectUri,
'client_id' => $this->clientId,
'client_secret' => $this->clientSecret,
]);
if ($response->failed()) {
throw new \Exception('Failed to get access token from LINE.');
}
return $response->json();
}
/**
* 使用 Access Token 獲取 LINE 使用者資料
* @param string $accessToken
* @return array
* @throws \Exception
*/
private function getLineProfile(string $accessToken): array
{
$response = Http::withToken($accessToken)->get('https://api.line.me/v2/profile');
if ($response->failed()) {
throw new \Exception('Failed to get user profile from LINE.');
}
return $response->json();
}
/**
* 尋找或建立使用者
* @param array $lineProfile
* @return User
*/
protected function findOrCreateUser(array $lineProfile): User
{
// 這邊的邏輯很重要!
// 優先用 line_user_id 找,確保唯一性
$user = User::where('line_user_id', $lineProfile['userId'])->first();
if ($user) {
// 如果使用者已存在,可以考慮更新他的頭像或名稱
$user->update([
'name' => $lineProfile['displayName'],
'avatar' => $lineProfile['pictureUrl'] ?? null,
]);
return $user;
}
// 如果使用者不存在,就建立新帳號
return User::create([
'name' => $lineProfile['displayName'],
'email' => null, // 注意:LINE 的基本 scope 不會給 email,需要額外申請權限
'password' => bcrypt(Str::random(16)), // 產生一個隨機密碼
'line_user_id' => $lineProfile['userId'],
'avatar' => $lineProfile['pictureUrl'] ?? null,
]);
}
}
第三步:改造你的 Controller
現在,我們有了強大的 `LineLoginService`,Controller 就可以變得非常乾淨、清爽。
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Services\LineLoginService;
use Exception;
class LineLoginController extends Controller
{
protected $lineLoginService;
public function __construct(LineLoginService $lineLoginService)
{
// 使用 Laravel 的依賴注入,是不是很優雅?
$this->lineLoginService = $lineLoginService;
}
public function redirectToLine()
{
return redirect($this->lineLoginService->getAuthorizationUrl());
}
public function handleLineCallback(Request $request)
{
try {
// Controller 只負責調用 Service,並處理結果
$user = $this->lineLoginService->handleCallback(
$request->input('code'),
$request->input('state')
);
return redirect()->route('dashboard')->with('success', '歡迎回來,' . $user->name);
} catch (Exception $e) {
// 處理所有可能的錯誤,給使用者一個友善的提示
// 這裡還可以記錄錯誤日誌 Log::error($e->getMessage());
return redirect()->route('login')->with('error', 'LINE 登入失敗,請稍後再試。');
}
}
}
看到了嗎?Controller 現在完全不知道 LINE Login 的內部細節。它只知道要呼叫 `LineLoginService` 的方法,然後根據結果(成功或拋出例外)來決定要導向到哪裡。這就是關注點分離 (Separation of Concerns) 的威力!
不只是重構:企業級架構的延伸思考
做到這裡,你的程式碼已經比 90% 的專案都來得乾淨了。但身為追求卓越的工程師,我們還可以做得更多。
1. 使用者綁定與關聯
在 `findOrCreateUser` 的邏輯中,你可以加入更複雜的判斷。例如,如果一個使用者已經用 Email 註冊,但這次是第一次用 LINE 登入,你應該提示他「是否要將此 LINE 帳號綁定到您 `xxx@email.com` 的帳戶?」,而不是直接幫他建立一個新帳號。
2. 抽象化與 Socialite
如果未來要支援 Google、Facebook 登入,你可以建立一個 `SocialLoginInterface`,讓 `LineLoginService`、`GoogleLoginService` 都去實作它。這樣 Controller 就可以依賴介面而不是具體的類別,擴充性更高。當然,更懶人(聰明)的做法是直接使用或擴充 Laravel Socialite 這個官方套件,它已經幫你把這些抽象化都做好了。
3. 安全性:State 參數的重要性
我必須再次強調 `state` 參數的重要性。它是一個隨機產生的字串,用來防止 CSRF (跨站請求偽造) 攻擊。流程是:
- 我們導向到 LINE 之前,產生一個 `state` 存在 Session。
- LINE 處理完後,會把這個 `state` 原封不動地帶回來。
- 我們在 Callback 中,必須比對帶回來的 `state` 和 Session 裡的是否一致。
如果不一致,代表這個請求很可能是偽造的,必須立刻中止,絕對不能讓它登入!我們的 `handleCallback` 已經做了這個檢查,請務必保留。
重構與架構設計,從來都不是為了炫技,而是為了讓未來的自己(和同事)活得更輕鬆。一個好的架構,能讓你的專案在時間的考驗下依然保持彈性與活力。希望今天的分享,能讓你對 Laravel 的社群登入串接有更深一層的理解,不再只是複製貼上,而是能寫出讓自己驕傲的程式碼。
延伸閱讀
- 告別傳統註冊!Laravel x LINE Login API 終極串接實戰,打造一鍵登入的絲滑體驗
- 金牌給你,API 給我鎖!Laravel + JWT 打造銅牆鐵壁般的 API 認證機制全解析
- 別再讓你的 API 裸奔!資深工程師的 Laravel Webhook 安全實戰:從設計到簽名驗證,打造滴水不漏的自動化橋樑
如果你們公司也有複雜的系統整合、API 串接需求,或是想對現有系統進行架構健檢與重構,卻不知從何下手?歡迎聯繫浪花科技,我們的團隊擁有豐富的企業級專案經驗,可以協助你打造穩定、高效且易於維護的數位解決方案。
常見問題 (FAQ)
Q1: 為什麼不把所有 LINE Login 邏輯都放在 Controller 裡?這樣不是比較快嗎?
A1: 短期來看,或許比較快。但長期而言,這會導致 Controller 職責不清、程式碼難以測試、複用性差等問題,形成所謂的「技術債」。將邏輯抽離到專門的 Service Class (服務層),可以讓程式碼架構更清晰、更容易維護與擴展,也方便進行單元測試。這是一個在大型專案中非常重要的設計模式。
Q2: LINE 登入流程中的 `state` 參數到底是什麼?可以不用嗎?
A2: 絕對不行!`state` 參數是防範 CSRF (跨站請求偽造) 攻擊的關鍵機制。它的作用是確保從 LINE 返回到你網站的請求,確實是由你的使用者最初發起的,而不是駭客偽造的。忽略 `state` 的驗證,等於是把網站的大門敞開,是非常危險的行為。
Q3: 如果一個使用者已經用 Email 註冊,第一次用 LINE 登入時,我該怎麼辦?
A3: 最佳的使用者體驗是「帳號綁定」而非「建立新帳號」。你可以在 `findOrCreateUser` 方法中增加邏輯:當找不到 `line_user_id` 但發現使用者處於登入狀態時,可以將收到的 `lineProfile[‘userId’]` 綁定到當前登入的帳號上。如果使用者未登入,則可以引導他先用 Email 登入,再進行綁定,避免產生重複的孤兒帳號。






