Laravel Sanctum数据库令牌过多及设备令牌管理方案咨询
解决Laravel Sanctum令牌过多的最佳实践
核心问题拆解
- 未授权请求可无限调用登录接口生成令牌,导致数据库冗余
- 多设备登录场景下,无法直接删除所有现有令牌
- 令牌以哈希形式存储,无法直接复用用户最新令牌
具体解决方案
1. 限制单用户令牌数量
给每个用户设置令牌数量上限,生成新令牌时自动清理超出上限的最旧令牌。比如设置每个用户最多保留10个有效令牌:
// 在登录逻辑中添加令牌数量控制 $maxAllowedTokens = 10; $user->tokens() ->where('name', 'login-token') // 确保匹配你的令牌命名规则 ->orderBy('created_at', 'asc') ->skip($maxAllowedTokens - 1) ->delete(); // 生成新令牌 $plainTextToken = $user->createToken('login-token')->plainTextToken;
这种方式既保留了多设备的登录权限,又避免了令牌无限增长。
2. 为令牌设置过期时间
利用Sanctum的令牌过期特性,生成令牌时指定有效期,配合定时任务自动清理过期令牌:
// 生成带7天有效期的令牌 $plainTextToken = $user->createToken( 'login-token', [], // 权限范围,按需调整 now()->addDays(7) )->plainTextToken;
然后在app/Console/Kernel.php中添加定时任务,每日清理过期令牌:
protected function schedule(Schedule $schedule) { $schedule->call(function () { DB::table('personal_access_tokens') ->where('expires_at', '<', now()) ->delete(); })->daily(); }
3. 对登录接口做防刷限制
阻止未授权请求无限调用登录接口,可通过两种方式实现:
- 添加验证码验证:要求前端传入图形/短信验证码,验证通过后才生成令牌
- 请求频率限制:使用Laravel内置的
Throttle中间件限制请求次数,比如1分钟内最多5次请求:
// 在登录路由中添加中间件 Route::post('/api/login', [AuthController::class, 'login']) ->middleware('throttle:5,1');
4. 基于设备标识复用令牌(替代方案)
由于令牌哈希存储无法直接复用,可通过设备标识关联令牌,同一设备再次登录时更新令牌而非生成新令牌:
// 获取前端传入的设备唯一标识 $deviceId = $request->input('device_id'); // 查找该用户对应设备的有效令牌 $existingToken = $user->tokens() ->where('name', 'login-token') ->where('metadata->device_id', $deviceId) // 假设用metadata存储设备信息 ->first(); if ($existingToken) { // 删除旧令牌,生成新令牌(无法直接获取明文,只能替换) $existingToken->delete(); $plainTextToken = $user->createToken( 'login-token', [], now()->addDays(7), ['device_id' => $deviceId] )->plainTextToken; } else { // 生成新令牌并绑定设备标识 $plainTextToken = $user->createToken( 'login-token', [], now()->addDays(7), ['device_id' => $deviceId] )->plainTextToken; }
额外优化建议
- 给
personal_access_tokens表的user_id、expires_at、last_used_at字段添加索引,提升查询和清理效率 - 定期清理长期未使用的令牌,比如删除30天内未被使用的令牌:
$schedule->call(function () { DB::table('personal_access_tokens') ->where(function ($query) { $query->where('expires_at', '<', now()) ->orWhere('last_used_at', '<', now()->subDays(30)); }) ->delete(); })->weekly();
内容的提问来源于stack exchange,提问作者Ozal Zarbaliyev
相关产品推荐
相关产品推荐

