「手把手」利用websocket實現手機掃碼登陸後,同步登陸資訊到web端頁面

LiamHao發表於2021-03-27

「手把手」利用websocket實現手機掃碼登陸後,同步登陸資訊到web端頁面

> composer create-project --prefer-dist laravel/laravel joker
> cd joker
> php artisan key:generate
Application key set successfully.
> php artisan --version
Laravel Framework 8.34.0

laravel-websockets 是一款封裝好了 websocket 服務的軟體包。其特點是透過攔截 pusher 的請求來擺脫對 pusher 服務商(國外)的依賴。還具有除錯皮膚,實時統計資訊,甚至允許您建立自定義 WebSocket 控制器等能力。以下執行了兩步操作,首先引入軟體包,然後釋出軟體包的配置檔案和資料庫遷移檔案。你可以在config資料夾中找到websockets.php配置檔案。資料庫遷移檔案可以先不管。

> composer require beyondcode/laravel-websockets
> php artisan vendor:publish --provider="BeyondCode\LaravelWebSockets\WebSocketsServiceProvider"

我們需要在.env檔案中配置幾個引數,PUSHER_APP_IDPUSHER_APP_KEYPUSHER_APP_SECRET可以隨便寫,與文件中提到的 pusher 無關。因為有 Laravel-websockets在程式裡攔截了pusher的轉發動作,會將事件轉發到本地。BROADCAST_DRIVER指定廣播驅動為pusher固定值。LARAVEL_WEBSOCKETS_PORT是指 socket 監聽的埠號,一定要讓這個埠可以被外網訪問:

// .env
BROADCAST_DRIVER=pusher
PUSHER_APP_ID=joker
PUSHER_APP_KEY=joker
PUSHER_APP_SECRET=joker
LARAVEL_WEBSOCKETS_PORT=2020

配置好以後,我們就可以啟動 socket 監聽程式了。雖然上面的.env檔案中,我們已經配置好2020埠了,但這裡的啟動命令中,我們還是需要--port=2020手動指定埠號,否則預設是6001埠:

> php artisan websockets:serve --port=2020
Starting the WebSocket server on port 2020...

看,啟動一個 socket 監聽程式就是這麼簡單。為了驗證是否能進行通訊,我們在位址列中輸入http://<your.host>/laravel-websockets,然後會顯示一個在Laravel-websockets中已經註冊好的頁面:

【手把手】利用websocket實現手機掃碼登陸後,同步登陸資訊到web端頁面

我們點選那個connect按鈕,如果如下圖所示,則說明可以正常通訊了:

【手把手】利用websocket實現手機掃碼登陸後,同步登陸資訊到web端頁面

routes/web.php中新增web路由,並建立相應的view檢視

Route::get('/hello', function () {
    // 首頁顯示二維碼
    return view('hello');
});

Route::get('login', function () {
    // 掃碼後,手機顯示的登陸頁
    return view('login');
});

Route::post('/login', function () {
    // 這裡先空著,後面會補充
    return 'Logined';
});

【手把手】利用websocket實現手機掃碼登陸後,同步登陸資訊到web端頁面

我們需要在resources/views/hellow.blade.php中先引入一些JS工具,然後編寫一些JS程式碼。

<!DOCTYPE html>
<html>
    <head>
        <title>Hello</title>
        <!-- 引入jQuery工具 -->
        <script src="https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js"></script>
        <!-- 引入二維碼工具 -->
        <script src="https://cdn.jsdelivr.net/npm/jquery.qrcode@1.0.3/jquery.qrcode.min.js"></script>
        <!-- 引入laravel-echo工具,其實使用Larave自帶的也可以。但是,使用自帶的還需要用到node前端構建工具,我這裡只簡單的演示後端實現過程,就不用node了 -->
        <script src="https://cdn.jsdelivr.net/npm/laravel-echo@1.10.0/dist/echo.iife.js"></script>
        <!-- 引入pusher工具,pusher是Laravel-echo底層,Laravel-echo是pusher的一層封裝 -->
        <script src="https://cdn.jsdelivr.net/npm/pusher-js@7.0.3/dist/web/pusher.min.js"></script>
    </head>
    <body>
        <h1>二維碼</h1>
    </body>
    <script type="text/javascript">

        // 簡單模擬一個 uuid 唯一身份碼,為了後端廣播時,不會廣播給錯人
        var uuid = Math.random().toString(36);

        // 初始化 laravel-echo 外掛
        window.Echo = new Echo({
            // 這裡是固定值 pusher
            broadcaster: 'pusher',
            // 這裡要和你在 .env 中配置的 PUSHER_APP_KEY 保持一致
            key: 'joker',
            wsHost: location.hostname,
            // 這裡是我們在上一步啟動 socket 監聽的埠
            wsPort: 2020,
            // 這個也要加上
            forceTLS: false,
        });

        // 我們隨便監聽一個頻道,這個頻道在專案還不存在,但不影響建立 socket 連線
        Echo.channel('abcdefg.'+uuid)
        // 隨便監聽一個事件,這個事件在專案中還不存在,但不影響建立 socket 連線
        .listen('LoginedEvent', (e) => {
          console.log(e);
        });

        // 顯示一個二維碼,內容是一個登陸頁地址,後面拼接 uuid。這個 uuid 會在後面廣播中用到,用來給監聽此 uuid 頻道的 socket 傳送資料
        $("body").qrcode(location.origin+"/login?uuid="+uuid);

    </script>
</html>

程式碼中已經註釋的比較清晰了,我們儲存檔案,然後在瀏覽器中輸入http://<your.host>/hello點選回車。我們開啟瀏覽器的開發者工具,找到WS選項,我們會看到 socket 已經與服務端進行了通訊。再看下服務端,會顯示連線者的一些資訊:

【手把手】利用websocket實現手機掃碼登陸後,同步登陸資訊到web端頁面

這一步我們只需要做兩件事,寫一個簡單的登陸頁面,然後實現登陸驗證:

簡單的登陸頁

我們開啟resources/views/login.blade.php檢視檔案,寫入以下內容:

<!DOCTYPE html>
<html>
    <head>
        <title>Login</title>
        <!-- 引入jQuery工具 -->
        <script src="https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js"></script>
    </head>
    <body>
        <input type="text" name="username">
        <input type="text" name="password">
        <button>登陸</button>
    </body>
    <script type="text/javascript">

        // 請求登陸介面
        $('button').click(function(event) {
            // 還記得前面掃碼的連結裡,帶有 uuid 引數嗎。簡單的獲取 url 中 uuid 引數
            var uuid = location.search.substring(1).split('=')[1];
            // 請求登陸介面
            $.ajax({
                // 這個登陸介面就是上面 routes/web.app 中定義好的路由。因為是 POST 請求,所以會進入 Route::post() 定義的路由中
                url: location.origin+'/login',
                type: 'POST',
                dataType: 'json',
                data: {
                    username: $('input[name="username"]').val(),
                    password: $('input[name="password"]').val(),
                    uuid: uuid,
                    // Laravel 預設帶有 csrf_token 驗證,所以這裡要加 _token 變數
                    _token: '{{ csrf_token() }}',
                },
                success: function(data){
                    console.log(data);
                }
            });
        });

    </script>
</html>

很簡單的一個頁面,我們用微信掃一掃看下頁面效果:

【手把手】利用websocket實現手機掃碼登陸後,同步登陸資訊到web端頁面

哈哈,雖然很醜,但很簡潔不是嗎?接下來,我們要進行後臺程式碼的完善工作了。

登陸驗證

進入routes/web.php中,修改Route::post()路由。為了簡化程式碼流程,我就直接在路由中寫登陸邏輯了, 但平時一定不要這麼幹

Route::post('/login', function (\Illuminate\Http\Request $request) {
    // 為了儘可能簡化流程,我們就不執行資料庫查詢的邏輯了,直接在 session 中寫入資訊吧
    session([
        'login_info' => $request->all(),
    ]);
    // 返回響應
    return response([
        'code' => 0,
        'message' => '登陸成功',
        'data' => session('login_info'),
    ]);
});

這應該是最簡化的登陸邏輯了吧,我們來小測一下,在http://<your.host>/login頁面中,在兩個輸入框中寫入內容,然後點選登陸,看下返回結果:

【手把手】利用websocket實現手機掃碼登陸後,同步登陸資訊到web端頁面

確實返回了我們輸入的資訊哈。一步一個腳印,一步一回頭,每完成一個小功能呢,就小測一下,個人習慣哈。

這一環節,做的事情也不是很多,我們一步一步來。首先要把config/app.php中的App\Providers\BroadcastServiceProvider::class註釋開啟:

【手把手】利用websocket實現手機掃碼登陸後,同步登陸資訊到web端頁面

routes/channels.php中加入我們最開始,在前端JS中寫的監聽頻道:

Broadcast::channel('abcdefg.{uuid}', function () {
    return true;
});

config/broadcasting.php中的connections.pusher下加入兩個配置資訊:

'connections' => [
    'pusher' => [
        'driver' => 'pusher',
            'key' => env('PUSHER_APP_KEY'),
            'secret' => env('PUSHER_APP_SECRET'),
            'app_id' => env('PUSHER_APP_ID'),
            'options' => [
                'cluster' => env('PUSHER_APP_CLUSTER'),
                'useTLS' => true,
                // 攔截 pusher 的廣播後,轉發到目標 ip
                'host' => '127.0.0.1',
                // 轉發的埠,就是我們之前在 .env 檔案中配置的 2020 埠
                'port' => env('LARAVEL_WEBSOCKETS_PORT', 6001),
            ],
    ];
];

建立登陸事件

> php artisan make:event LoginedEvent
Event created successfully.

執行以上程式碼後,我們可以在app/Events資料夾中找到LoginedEvent.php檔案。

在事件中傳送廣播

只需在上面新建的app/Events/LoginedEvent.php中,進行很小的改動就可以了。

<?php

namespace App\Events;

// 首先要讓這個事件類實現 ShouldBroadcast 介面,也就是在類名後加上 implements ShouldBroadcast 即可。
class LoginedEvent implements ShouldBroadcast
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    // 只有 public 屬性的變數會被廣播到指定的頻道中,所以這裡要使用 public 關鍵字修飾你需要廣播的變數
    public $logined_info;

    public function __construct($logined_info)
    {
        $this->logined_info = $logined_info;
    }

    // 實現 ShouldBroadcast 介面後,就必須實現這個方法,系統會自動將廣播傳送到這個方法中定義的頻道中
    public function broadcastOn()
    {
        // 這裡我們改為 Channel。原 PrivateChannel 是私有頻道,未登入時前端無法監聽
        // 這裡的頻道要與 routes/channels.php 中定義的頻道格式保持一致
        return new Channel('abcdefg.'.$this->logined_info['uuid']);
    }
}

觸發

我們再回到登陸驗證的路由中,在儲存 session 資料後,加入觸發登陸事件的程式碼:

Route::post('/login', function (\Illuminate\Http\Request $request) {

    session([
        'login_info' => $request->all(),
    ]);
    // 觸發登陸事件
    event(new \App\Events\LoginedEvent(session('login_info')));

    return response([
        'code' => 0,
        'message' => '登陸成功',
        'data' => session('login_info'),
    ]);
});

好了,我們再來驗證下,登陸後在 socket 中是否會收到廣播內容。這裡我們可以同時開啟http://<your.host>/loginhttp://<your.host>/laravel-websockets頁面,然後用手機微信的掃一掃功能掃描頁面上的二維碼。在手機上任意輸入內容,點選登陸按鈕,看一下是否有新的廣播資料:

【手把手】利用websocket實現手機掃碼登陸後,同步登陸資訊到web端頁面

主要流程已經通了,現在離完成我們的目標已經近在咫尺了,加油!

現在我們來調整下app/Events/LoginedEvent.php事件和routes/web.php路由檔案中的/hello部分。主要目的是:

在手機端觸發的登陸事件中,透過廣播將登陸後的session_id返回給 web 網頁前端。前端拿到後再將其拼接到路由中,執行頁面跳轉。後端會將 url 中的session_id引數獲取到,然後將此 session id 儲存到此次 web 端請求的 session id 中。也就是使 web 端網頁與手機端使用同一個 session id 。

注意:在操作 session 資料時,如果你在框架 請求結束前 使用類似dd()die()等函式,則在此之前操作的 session 資料不會進行持久化儲存。詳細原因可參考原始碼:
「手把手」利用websocket實現手機掃碼登陸後,同步登陸資訊到web端頁面

這裡僅作為演示無需重複登入即可同步登陸資訊的實現,其中會有很多安全問題,這裡就先不考慮了,大佬們勿噴。先調整下app/Events/LoginedEvent.php檔案:

class LoginedEvent implements ShouldBroadcast
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    // 只有 public 屬性的變數會被廣播到指定的頻道中,所以這裡要使用 public 關鍵字修飾你需要廣播的變數
    public $logined_info;
    // 加入新的變數 $session_id
    public $session_id;

    public function __construct($logined_info)
    {
        $this->logined_info = $logined_info;
        // 獲取當前的 session id
        $this->session_id = session()->getId();
    }

    public function broadcastOn()
    {
        return new Channel('abcdefg.'.$this->logined_info['uuid']);
    }
}

修改resources/views/hellow.blade.php檢視檔案,我們只需要修改監聽到事件後,執行回撥的部分。把上面事件中新增的session_id欄位拼接到 url 中,然後跳轉到指定的路由:

        Echo.channel('abcdefg.'+uuid)
        .listen('LoginedEvent', (e) => {
            console.log(e);
            // 從登陸事件廣播出來的資料中,取出 session_id 欄位
            var session_id = e.session_id;
            // 拼接好引數後,跳轉到指定頁面,也就是當前頁面
            location.href = location.origin+'/hello?session_id='+session_id;
        });

routes/web.php檔案中的/hello路由裡面加入替換 session id 的程式碼,這一步很關鍵:

Route::get('/hello', function (\Illuminate\Http\Request $request) {
    if($request->get('session_id')){
        // 將手機端登陸的 session_id 設定到當前頁面的 session 中
        session()->setId($request->get('session_id'));
        // 重新讀取 session 資料。其實就是將手機端登陸後的 session 內容讀取到當前頁面的 session 中
        session()->start();
        // 這裡只是做個提示
        echo session('login_info.username')." 已透過手機掃碼登入";
    }
    return view('hello');
});

最後看下結果吧!手機端測試直接掃碼即可,如果想像我一樣在網頁上登陸,則需要在登陸頁的地址後面,手動拼接上?uuid=引數。

[手把手]利用websocket實現手機掃碼登陸後,同步登陸資訊到web端頁面

此時已經實現手機端頁面與 web 端頁面使用的是同一個 session 資料了。記得在改後端程式碼時,儘量重啟一下laravel-wesocket的 http 服務,以免出現改程式碼後,沒有生效的問題。

透過laravel-echo主動向服務端傳送訊息,實現線上狀態管理:主要講述了laravel-echo如何主動向伺服器傳送訊息,並在後端編寫自己的控制器邏輯。

本作品採用《CC 協議》,轉載必須註明作者和本文連結
再見了媽媽今晚我就要遠航,別為我擔心我有快樂和智慧的槳~

相關文章