「手把手」利用websocket实现手机扫码登陆后,同步登陆信息到web端页面

「手把手」利用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软件包

laravel-websockets 是一款封装好了 websocket 服务的软件包。其特点是通过拦截 pusher 的请求来摆脱对 pusher 服务商(国外)的依赖。还具有调试面板,实时统计信息,甚至允许您创建自定义 WebSocket 控制器等能力。以下执行了两步操作,首先引入软件包,然后发布软件包的配置文件和数据库迁移文件。你可以在config文件夹中找到websockets.php配置文件。数据库迁移文件可以先不管。

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

启动websocket监听

我们需要在.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端页面

建立socket连接

我们需要在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 协议》,转载必须注明作者和本文链接
再见了妈妈今晚我就要远航,别为我担心我有快乐和智慧的桨~
本帖由系统于 2年前 自动加精
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
讨论数量: 29

路由 /login 中 post提交请求,500错误

event(new \App\Events\LoginedEvent(session('login_info')));

session(‘login_info’);打印有值。
注释掉,就没报错。

本地和线上环境都这样。

Laravel

Laravel

php版本原因,我laravel是7.30版本

2年前 评论
LiamHao (楼主) 2年前
LiamHao (楼主) 2年前

file

file

file

为什么事件中的属性要定义成public
Laravel

最后在这里出发的 pusher/pusher 的trigger()方法,
Laravel

请求的路由和参数,这看的比较直接了
Laravel

这个pusher都是curl请求,效率不高啊

2年前 评论

使用了一段时间laravel-websocket和pusher,我发现larave的这套广播机制,无法双向通信。
只能通过ajax 把数据推送到后端,再通过广播推往前端
比如,我要实现用户实时在线状态管理,就无法实现。

3年前 评论

@LiamHao 建议标题改成,手摸手...

2年前 评论
LiamHao (楼主) 2年前

composer create-project --prefer-dist laravel/laravel joker 之后是不是要 npm install 一下把 node包下载到本地?

2年前 评论
LiamHao (楼主) 2年前

file @LiamHao 触发登录时间报错信息,我想应该和框架版本没关系。

file laravel-websockets服务有启动啊

2年前 评论
LiamHao (楼主) 2年前

@LiamHao 谢谢楼主,更新了这个包,就OK了,再次表示感谢~

composer require pusher/pusher-php-server ^4.1

对比了报错行的代码

Laravel

我没有升级框架版本,不知道你框架版本对应的pusher包版本是多少,我的是5.0.1 ,等于是降版本了

2年前 评论
LiamHao (楼主) 2年前

https 无法连接到websocket , Connect按钮下面显示Channels current state is unavailable

2年前 评论
LiamHao (楼主) 2年前

請問一下 [这里仅作为演示无需重复登录即可同步登陆信息的实现,其中会有很多安全问题,这里就先不考虑了,大佬们勿喷。] 會有那些安全問題!!???

2年前 评论

mark一遍,有个问题 LoginedEvent类中这个方法 public function broadcastOn() 在什么时候触发的,我看到是event方法,到了这里 if ($this->shouldBroadcast($payload)) { $this->broadcastEvent($payload[0]); } 走了队列pushon但是还没有看到具体怎么触发LoginedEvent类中这个方法 public function broadcastOn(),还有一个问题具体怎么把这个类属性值推到一样的channel上呢

2年前 评论
LiamHao (楼主) 2年前
川夏 (作者) 2年前

前端接收到json数据里,中文是unicode编码,json中的引号前也有\ ,这个格式可以修改吗

2年前 评论
LiamHao (楼主) 2年前

这二项你知道怎么用的吗,第一个实时统计一直没数据 file

2年前 评论
LiamHao (楼主) 2年前

:joy:卡了我半天

file

不知道怎么回事了百度都说什么https的原因,我是http呀

BROADCAST_DRIVER=pusher PUSHER_APP_ID=joker PUSHER_APP_KEY=joker PUSHER_APP_SECRET=joker LARAVEL_WEBSOCKETS_PORT=8888 PUSHER_APP_CLUSTER=eu

file

file

1年前 评论
Smilephp (作者) 1年前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!