LINE 登入不只是串好就好!資深工程師帶你重構 Laravel x LINE Login,打造企業級認證架構

2025/09/15 | Laravel技術分享

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 (跨站請求偽造) 攻擊。流程是:

  1. 我們導向到 LINE 之前,產生一個 `state` 存在 Session。
  2. LINE 處理完後,會把這個 `state` 原封不動地帶回來。
  3. 我們在 Callback 中,必須比對帶回來的 `state` 和 Session 裡的是否一致。

如果不一致,代表這個請求很可能是偽造的,必須立刻中止,絕對不能讓它登入!我們的 `handleCallback` 已經做了這個檢查,請務必保留。

重構與架構設計,從來都不是為了炫技,而是為了讓未來的自己(和同事)活得更輕鬆。一個好的架構,能讓你的專案在時間的考驗下依然保持彈性與活力。希望今天的分享,能讓你對 Laravel 的社群登入串接有更深一層的理解,不再只是複製貼上,而是能寫出讓自己驕傲的程式碼。

延伸閱讀

如果你們公司也有複雜的系統整合、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 登入,再進行綁定,避免產生重複的孤兒帳號。

 
立即諮詢,索取免費1年網站保固