示例演示
准备工作
单用户登录的作用?
单用户登录可以保障网站的权益和用户的账号安全。
从网站角度来说: 假如一个站长创办了一个有会员付费机制的内容网站,他肯定不希望用户共享账号给他人共享使用,这样会影响网站的收入。
从用户角度来说: 像 QQ 这样的应用,当其他设备登录你的账号时,必须要顶掉之前的账号,并且发送通知来告诉用户的账号在其他地方登录了,可能有被盗风险。
下面我们一起来实现单用户登录功能吧!
创建应用
composer create-project laravel/laravel single-user-login --prefer-dist
执行
composer install
添加前端模板,我们仅在开发环境中使用,因为正式的生产环境都是打包好的前端文件,不需要加载这个包,所以我们添加 --dev
参数。
composer require laravel/ui --dev
引入 Vue
php artisan ui vue
安装前端依赖并编译
npm install && npm run dev
如果你遇到了以下报错:
Error: Cannot find module 'webpack/lib/rules/DescriptionDataMatcherRulePlugin'
尝试升级 vue-loader
npm update vue-loader
现在前端文件应该编译成功了,因为我们要编写一些前端代码,所以还需要配置一下 laravel mix
,让开发流程更顺畅。
打开项目根目录的 webpack.mix.js
文件:
mix.js('resources/js/app.js', 'public/js')
.vue().version() // 添加 .version() 避免缓存
.sass('resources/sass/app.scss', 'public/css');
mix.browserSync({
proxy: 'single-user.test' // 你的本地项目地址
});
我们使用了 broswerSync
这样就不需要每次更改前端代码后都去 F5 刷新页面。
再次执行
npm run watch
这时,你的浏览器应该会自动打开项目地址,并且每次修改前端代码都会自动刷新。
一切准备就绪,下面我们只需要专注开发部分就可以了。
功能实现
保证一个账号只有一个用户登录
用户在访问网站时,会与网站建立 Session
,并将 SessionId
存储在 Cookie
,以此作为用户此次会话的凭证。
单用户登录的原理是:在用户登录后,销毁 这个用户之前所有与网站 通信时建立的
Session
。
下面我们来一起实现吧!
打开 .env
文件,我们将 SESSION_DRIVER
修改为 databse
,这样做的好处是可以沉淀每次与用户的会话做数据分析,用户地区分布,用户访问设备等客户端信息。
...
SESSION_DRIVER=database // 修改为 database
SESSION_LIFETIME=120
...
假设你已经配置好了数据库,我们既然要将 session
存储在数据库,那么还需要建立一个 sessions
表, Laravel 已经非常贴心的为我们准备好了。
执行命令:
php artisan session:table
Laravel 已经为我们生成了迁移文件,我们直接执行迁移命令来创建数据表就好
php artisan migrate
用户登录
下面我们来完成用户登录功能,执行命令来创建脚手架
php artisan ui:auth
访问 your-project-url/register
创建一个用户,并登录。
我们打开数据库中的 session
表
这条记录就是我们刚刚与网站建立的 Session
| id | user_id | ip_address | user_agent | payload | last_activity |
| | | | | | |
| 服务端Session | 关联的 user 表 ID | 访问地址 | 终端设备 | 前端加密的SessionID | 最后活跃时间 |
销毁之前的 Session
我们已经生成了用户脚手架,打开 app/Http/Controllers/Auth/LoginController.php
文件。
Laravel
的登录功能依赖于 AuthenticatesUsers
Trait。
// LoginController.php
class LoginController extends Controller
{
...
use AuthenticatesUsers; // 打开这个 trait
...
我们来打开这个文件看看,往下找到 authenticated
方法
/**
* The user has been authenticated.
*
* @param \Illuminate\Http\Request $request
* @param mixed $user
* @return mixed
*/
protected function authenticated(Request $request, $user)
{
//
}
这个方法的触发节点是 用户授权成功之后,写入 Session 之前,那我们可以通过它来销毁已登录用户之前的所有 Session
。
把它复制一下,然后回到 LoginController
文件,将 authenticated
方法覆写成我们自己的业务逻辑。
protected function authenticated(Request $request, $user)
{
DB::table('sessions')->where('user_id', $user->id)->delete()
}
我们可以简单粗暴的直接将用户之前登录 Session
删除,但是这样做并没有收集到用户信息,建立 Session
表就没意义了。
其实只需要将 session
表存储的 id
进行修改,使其与前端传输的 SessionID
不匹配,就代表会话失效,所以我们稍微修改一下
protected function authenticated(Request $request, $user)
{
DB::table('sessions')->where('user_id', $user->id)
->update([
'id' => DB::raw("concat('OUTMAN_', user_id, '_', id)"),
'user_id' => null,
]);
}
将 id
添加 OUTMAN
前缀,并拼接 user_id
字段,这样既达到了 Session 失效,又保留了用户信息。
你可能会疑问,既然我们只更新数据,那为什么不直接使用 软删除
呢?
其实 sessions
表不只存储已登录用户的 session
,在用户访问网站那一刻,就已经建立了 session
,如果用户没有登录,那么这条记录其实对我们来讲其实没有意义。
回到浏览器,打开一个 窗口A
,再使用快捷键 Ctrl+Shift+N
创建一个无痕 窗口B
,达到 Session 隔离的目的。
先在 窗口A
登录账号,再到 窗口B
登录相同的账号,然后再回到 窗口A
,按 F5 刷新一下页面,发现 窗口A
的用户已经退出。说明我们刚刚的代码生效了,因为在 窗口B
登录的时候,执行了查询,并把 窗口A
的 session 更新了。
我们再来看一下 sessions
表,已经有一个失效的 Session
了。
消息推送
发送实时消息
接下来我们来实现当某个用户的账号在其他地方登录时,发送一个实时消息给当前的登录用户,告诉他的账号在其他设备登录了。
后端组件: laravel-websockets
,pusher
我们开始吧!
先来完成后端的功能,打开 .env
文件
PUSHER_APP_ID=123123
PUSHER_APP_KEY=321321
PUSHER_APP_SECRET=secret
PUSHER_APP_CLUSTER=mt1
这些是 pusher
的配置,随意填写,只需要保证和前端的 laravel-echo
一致即可。如果一台服务器上有多个站点,需要保证 id 和 key 的唯一性。
安装组件
laravel-websocets
依赖 guzzlehttp/psr7
组件的 1.5 版本,但是它的最新版本已经到 2.1
,而 guzzlehttp
也依赖它,并且要求最低是 1.8
两者相冲突了,所以不能直接使用 composer require
命令安装。
同样,pusher
也需要使用 3.0
版本。
打开 composer.json
"require": {
...
"guzzlehttp/psr7": "^1.5",
"beyondcode/laravel-websockets": "^1.12.0",
"pusher/pusher-php-server": "~3.0"
},
我们直接把这两个组件粘贴进去,然后执行命令:
composer update
发布 laravel-websockets
php artisan vendor:publish
在列表里选择 BeyondCode\LaravelWebSockets\WebSocketsServiceProvider
对应的序号。
Laravel
框架的消息推送功能默认使用的是 pusher
第三方 API 服务,我们使用 laravel-websocket
来拦截 pusher
发出的请求,转发到我们自己的服务器。
配置 websocket
关于 websockets
的配置,可以在 config/websockets.php
里找到
官方文档:点击这里
你只需要知道它在哪里即可,我们没有需要定制的配置。
然后打开 config/broadcasting.php
,修改 pusher
发送的请求地址为我们本地 ip 地址。
'connections' => [
'pusher' => [
...
'options' => [
'cluster' => env('PUSHER_APP_CLUSTER'),
'encrypted' => true,
'host' => '127.0.0.1',
'port' => 6001,
'scheme' => 'http'
],
],
好了,组件安装完成,执行命令启动 websocket
服务
php artisan websockets:serve
打开 your-project/laravel-websockets
可以看到控制台,点击 Connect
可以看到所有的发送消息明细。
推送消息
我们来创建一个事件,在每次用户登录成功后触发,执行命令:
php artisan make:event UserAuthenticatedEvent
还记得上一节提到的 authenticated
方法吗?我们在这个方法里面来触发事件:
public function authenticated(Request $request, $user)
{
DB::table('sessions')->where('user_id', $user->id)
->update([
'id' => DB::raw("concat('OUTMAN_', user_id, '_', id)"),
'user_id' => null
]);
// 触发广播事件,发送一个消息给这个登录用户
broadcast(new UserAuthenticatedEvent($user));
}
既然是广播事件,那就需要实现广播的接口,打开 UserAuthenticatedEvent.php
<?php
namespace App\Events;
use App\Models\User;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class UserAuthenticatedEvent implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $user;
public function __construct(User $user)
{
$this->user = $user;
}
public function broadcastOn()
{
// 发送频道拼接 user_id,避免干扰
return new Channel('outman-user-'. $this->user->id);
}
}
再回到首页登录一次,然后查看 storage/logs/laravel.log
[2021-12-13 13:34:43] local.INFO: Broadcasting [App\Events\UserAuthenticatedEvent] on channels [outman-user-1] with payload:
{
"user": {
"id": 1,
"name": "ricky",
"email": "ricky@me.com",
"email_verified_at": null,
"created_at": "2021-12-12T14:03:00.000000Z",
"updated_at": "2021-12-12T14:03:00.000000Z"
},
"socket": null
}
消息已经发出了,因为我们现在的 BROADCAST_DRIVER
还是 log,所以打印到了日志中。
完成监听
前端实时显示推送消息
上一节我们已经实现了消息的推送,现在需要前端来实时接收并显示给用户,本节也是教程的最后一节,马上就要完成了,加油。
我们开始吧!
安装依赖
前端依赖组件: laravel-echo
,pusher-js
执行命令:
npm install laravel-echo pusher-js --save
编写组件
我们来编写一个提示框,用来提醒用户
在 resources/js/components
文件夹新建一个 OutmanAlert.vue
<template>
<div class="container" v-if="alerting">
<div class="col-md-8">
<div class="alert alert-danger" role="alert">
您的账号已在别处登录!
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
alerting: true // 通过 alerting 变量来控制组件显示隐藏
}
},
mounted() {
console.log('Component mounted.')
}
}
</script>
将这个组件注册,打开 resources/js/app.js
window.Vue = require('vue').default;
...
Vue.component('outman-alert', require('./components/OutmanAlert.vue').default);
引用这个组件,打开 resources/views/layouts/app.blade.php
<main class="py-4">
<outman-alert></outman-alert>
@yield('content')
</main>
先来编译一下,看看显示是否正常
npm run watch
添加前端监听
组件已经正常工作了,下面我们来添加监听。
首先打开 .env
文件,更改 BROADCAST_DRIVER
...
DB_PASSWORD=
BROADCAST_DRIVER=pusher
...
接着打开 resources/js/bootstrap.js
,打开底部的注释
import Echo from 'laravel-echo';
window.Pusher = require('pusher-js');
window.Echo = new Echo({
broadcaster: 'pusher',
key: '321321', // 此处与 .env PUSHER_APP_KEY 相同
wsHost: window.location.hostname,
wsPort: 6001,
forceTLS: false,
disableStats: true,
});
最后回到 OutmanAlert.vue
组件,添加事件监听
<script>
export default {
props: [
'id'
],
data() {
return {
alerting: false
}
},
mounted() {
window.Echo.channel('outman-user-' + this.id).listen('UserAuthenticatedEvent', () => {
this.alerting = true
setTimeout(() => {
window.location = '/login'
}, 3000)
})
}
}
</script>
在组件加载后,监听 outman-user-user_id
频道,如果有消息发送过来,把 alerting
变量修改为 true
,并等待 3 秒后跳转登录地址。
但是 vue
没办法读取到 user_id
,我们需要在 blade
模板中传递给它。
回到引用它的文件 resources/views/layout/app.blade.php
<main class="py-4">
@if(auth()->check())
<outman-alert id="{{auth()->id()}}"></outman-alert>
@endif
@yield('content')
</main>
至此,所有功能已经完成了,执行编译
npm run dev
如果遇到以下错误无须理会,pusher 的自检,但是我们已经将它的消息转发给本地的 websocket 服务了。
Notifications are disabled
Reason: DisabledForApplication Please make sure that the app id is set correctly.
Command Line: d:\laragon\www\single-user-login\node_modules\node-notifier\vendor\snoreToast\snoretoast-x64.exe -appID "Laravel Mix" -pipeName \\.\pipe\notifierPipe-a7662603-67a6-45de-9ef2-aa40181d3f1a -p d:\laragon\www\single-user-login\node_modules\laravel-mix\icons\laravel.png -m "Build successful" -t "Laravel Mix"
打开浏览器测试一下:
发表评论 取消回复