
本文介绍如何在 laravel 中实现在用户会话自动过期前主动触发登出事件,从而准确记录用户实际离线时间(而非仅依赖手动点击注销),并通过事件监听器持久化登出时间与在线时长。
本文介绍如何在 laravel 中实现在用户会话自动过期前主动触发登出事件,从而准确记录用户实际离线时间(而非仅依赖手动点击注销),并通过事件监听器持久化登出时间与在线时长。
在 Laravel 应用中,仅监听 Illuminate\Auth\Events\LoggedOut 事件无法覆盖会话因超时被动失效的场景——该事件仅在显式调用 Auth::logout()(如点击“退出登录”按钮)时触发,而用户关闭浏览器、长时间无操作导致 session 过期等情况并不会触发它。若需精确统计用户真实在线时长(例如用于审计、行为分析或计费系统),必须确保每次会话终止(无论主动或被动)都能捕获并记录 last_logout 时间。
✅ 正确方案:结合中间件 + 自定义事件实现「准实时登出钩子」
核心思路是:不依赖会话过期本身(Laravel 不提供过期前回调),而是通过前端主动探测 + 后端预埋事件机制,在会话即将失效前由客户端发起一次登出预通知。但更实用且可控的做法是——统一收口登出逻辑,将所有登出路径(含手动注销、Token 失效、甚至定时清理脚本)都导向同一事件触发点。
以下为推荐的工程化实现:
1. 定义自定义登出事件
// app/Events/LogoutEvent.php
<?php
namespace App\Events;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class LogoutEvent
{
use Dispatchable, SerializesModels;
public function __construct(public int $userId)
{
//
}
}2. 创建中间件,在登出路由执行前派发事件
// app/Http/Middleware/RegisterLogoutEventMiddleware.php
<?php
namespace App\Http\Middleware;
use App\Events\LogoutEvent;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;
class RegisterLogoutEventMiddleware
{
public function handle(Request $request, Closure $next): Response
{
if (Auth::check()) {
event(new LogoutEvent(Auth::id()));
}
return $next($request);
}
}⚠️ 注意:此中间件必须置于 auth 中间件之后(确保用户已认证),且仅挂载在明确的登出入口(如 /logout),不可全局应用。
3. 注册事件监听器(复用原有逻辑,但增强健壮性)
// app/Listeners/LoggedOutListener.php
<?php
namespace App\Listeners;
use App\Events\LogoutEvent;
use App\UserTrack;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
class LoggedOutListener
{
public function handle(LogoutEvent $event): void
{
// 使用事务确保数据一致性
DB::transaction(function () use ($event) {
$track = UserTrack::where('user_id', $event->userId)
->orderByDesc('id')
->first();
if ($track && $track->last_login) {
$end = Carbon::now();
$start = Carbon::createFromFormat('Y-m-d H:i:s', $track->last_login);
$track->last_logout = $end;
$track->timestamp = $start->diffInSeconds($end); // 在线秒数
$track->save();
}
});
}
}4. 配置事件服务提供者
// app/Providers/EventServiceProvider.php
protected $listen = [
// ... 其他事件
\App\Events\LogoutEvent::class => [
\App\Listeners\LoggedOutListener::class,
],
];5. 路由注册(Laravel 10+ 推荐写法)
// routes/web.php
use App\Http\Middleware\RegisterLogoutEventMiddleware;
Route::post('/logout', [AuthenticatedSessionController::class, 'destroy'])
->middleware(['auth', 'web', RegisterLogoutEventMiddleware::class])
->name('logout');? 补充说明:如何应对「真正静默过期」?
上述方案仍无法 100% 捕获用户未主动登出、也未触发任何请求的纯静默过期场景(如关机、断网)。对此,建议补充以下策略:
- 后台任务兜底:使用 schedule:run 每 5–10 分钟扫描 sessions 表或自定义 user_sessions 表中 last_activity 超过 session.lifetime 的记录,批量更新对应 UserTrack::last_logout;
- 前端心跳保活 + 主动登出:在页面注入 JS 心跳(如每 30 秒请求 /api/keep-alive),并在 beforeunload 或 visibilitychange 事件中调用 /logout 接口(需 CSRF 保护);
- JWT / Sanctum Token 场景:监听 token_invalidated 或自定义 TokenExpired 事件,原理同上。
✅ 总结
- ❌ 不要依赖 LoggedOut 原生事件处理会话过期场景;
- ✅ 使用中间件在所有登出入口统一派发自定义 LogoutEvent;
- ✅ 监听器中使用数据库事务保障 last_login → last_logout → timestamp 更新原子性;
- ✅ 对静默过期,需结合定时任务与前端协同实现高精度覆盖。
通过该设计,你不仅能精准记录每次可感知的登出行为,也为后续扩展审计日志、用户行为分析提供了可靠的数据基础。










