利用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_ID
、PUSHER_APP_KEY
、PUSHER_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
中已经注册好的页面:
我们点击那个connect
按钮,如果如下图所示,则说明可以正常通信了:
创建两个页面
在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';
});
建立socket连接
我们需要在app/resources/views/hellow.blade.php
中先引入一些JS
工具,然后编写一些JS
代码。
<!DOCTYPE html>
<html>
<head>
<title>Hello</title>
<!-- 引入jQuery工具 -->
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<!-- 引入二维码工具 -->
<script src="https://cdn.bootcdn.net/ajax/libs/jquery.qrcode/1.0/jquery.qrcode.min.js"></script>
<!-- 引入laravel-echo工具,其实使用Larave自带的也可以。但是,使用自带的还需要用到node前端构建工具,我这里只简单的演示后端实现过程,就不用node了 -->
<script src="https://cdn.bootcdn.net/ajax/libs/laravel-echo/1.10.0/echo.iife.js"></script>
<!-- 引入pusher工具,pusher是Laravel-echo底层,Laravel-echo是pusher的一层封装 -->
<script src="https://cdn.bootcdn.net/ajax/libs/pusher/7.0.3/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 已经与服务端进行了通信。再看下服务端,会显示连接者的一些信息:
手机端扫码登陆
这一步我们只需要做两件事,写一个简单的登陆页面,然后实现登陆验证:
简单的登陆页
我们打开app/resources/views/login.blade.php
视图文件,写入以下内容:
<!DOCTYPE html>
<html>
<head>
<title>Login</title>
<!-- 引入jQuery工具 -->
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.5.1/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>
很简单的一个页面,我们用微信扫一扫看下页面效果:
哈哈,虽然很丑,但很简洁不是吗?接下来,我们要进行后台代码的完善工作了。
登陆验证
进入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
页面中,在两个输入框中写入内容,然后点击登陆,看下返回结果:
确实返回了我们输入的信息哈。一步一个脚印,一步一回头,每完成一个小功能呢,就小测一下,个人习惯哈。
服务端触发登陆事件
这一环节,做的事情也不是很多,我们一步一步来。首先要把config/app.php
中的App\Providers\BroadcastServiceProvider::class
注释打开:
在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;
// 只有 punlic 属性的变量会被广播到指定的频道中,所以这里要使用 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>/login
和http://<your.host>/laravel-websockets
页面,然后用手机微信的扫一扫功能扫描页面上的二维码。在手机上任意输入内容,点击登陆
按钮,看一下是否有新的广播数据:
主要流程已经通了,现在离完成我们的目标已经近在咫尺了,加油!
页面跳转(重点)
现在我们来调整下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 数据不会进行持久化保存。详细原因可参考源码:
这里仅作为演示无需重复登录即可同步登陆信息的实现,其中会有很多安全问题,这里就先不考虑了,大佬们勿喷。先调整下app/Events/LoginedEvent.php
文件:
class LoginedEvent implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
// 只有 punlic 属性的变量会被广播到指定的频道中,所以这里要使用 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']);
}
}
在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');
});
最后是app/resources/views/hellow.blade.php
视图文件,我们只需要修改监听到事件后,执行回调的部分:
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;
});
最后看下结果吧!
此时已经实现手机端页面与 web 端页面使用的是同一个 session 数据了。记得在改后端代码时,尽量重启一下laravel-wesocket
的 http 服务,以免出现改代码后,没有生效的问题。
相关文章
《利用laravel-echo主动向服务端发送消息,实现在线状态管理》:主要讲述了laravel-echo
如何主动向服务器发送消息,并在后端编写自己的控制器逻辑。