示例演示

image.png

准备工作

单用户登录的作用?

单用户登录可以保障网站的权益和用户的账号安全。

从网站角度来说: 假如一个站长创办了一个有会员付费机制的内容网站,他肯定不希望用户共享账号给他人共享使用,这样会影响网站的收入。

从用户角度来说: 像 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创建一个用户,并登录。

image.png

我们打开数据库中的 session

image.png

这条记录就是我们刚刚与网站建立的 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 了。

image.png

消息推送

发送实时消息

接下来我们来实现当某个用户的账号在其他地方登录时,发送一个实时消息给当前的登录用户,告诉他的账号在其他设备登录了。
后端组件: laravel-websocketspusher

我们开始吧!

先来完成后端的功能,打开 .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 对应的序号。

image.png

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 可以看到所有的发送消息明细。

image.png

推送消息

我们来创建一个事件,在每次用户登录成功后触发,执行命令:

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-echopusher-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

image.png

添加前端监听

组件已经正常工作了,下面我们来添加监听。
首先打开 .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"

打开浏览器测试一下:

image.png

点赞(3)

评论列表 共有 0 评论

暂无评论

微信服务号

微信客服

淘宝店铺

support@elephdev.com

发表
评论
Go
顶部