Laravel Fortify多路由守卫配置及单元测试问题求助
Laravel Fortify动态切换守卫在单元测试中失效的解决方案
问题描述
我在给路由编写单元测试时遇到异常:routes/fortify.php中定义的部分路由需要使用不同的fortify.guard(实现admin与user的权限隔离),因此在FortifyServiceProvider的register方法中通过判断请求路由动态设置守卫。但运行php artisan test时,请求路由被识别为GET /,导致admin登录测试中守卫未切换为admin,程序尝试获取User模型而非Admin模型,最终认证失败。现寻求正确的路由守卫声明方式。
现有代码
FortifyServiceProvider.php
/** * Register any application services. */ public function register(): void { Fortify::ignoreRoutes(); if(request()->is('api/v1/admin/*')){ config()->set('fortify.guard', 'admin'); } }
单元测试代码
public function loginAsTestAdmin(): void { $payload = ['email' => 'admin@example.com', 'password' => bcrypt('password')]; $this->post('api/v1/admin/login', $payload)->assertStatus(200); }
问题根源
register方法属于Laravel服务容器的初始化阶段,此时请求尚未完全解析。在单元测试环境下,初始请求路由会被默认设为GET /,导致路由判断逻辑失效,无法正确切换守卫。
正确实现方式
方案1:路由组绑定守卫(推荐)
修改routes/fortify.php,将不同权限的路由划分到指定守卫的路由组中,同时在FortifyServiceProvider的boot方法中配置对应认证逻辑:
// routes/fortify.php // Admin权限路由组 Route::prefix('api/v1/admin')->middleware(['auth:admin'])->group(function () { Fortify::loginRoute(); // 其他Admin专属Fortify路由 }); // 普通用户权限路由组 Route::prefix('api/v1')->middleware(['auth:web'])->group(function () { Fortify::loginRoute(); // 其他用户专属Fortify路由 });
// FortifyServiceProvider.php boot方法 public function boot(): void { Fortify::authenticateUsing(function (Request $request) { // 根据路由判断要验证的模型 if ($request->is('api/v1/admin/*')) { $admin = Admin::where('email', $request->email)->first(); return $admin && Hash::check($request->password, $admin->password) ? $admin : null; } else { $user = User::where('email', $request->email)->first(); return $user && Hash::check($request->password, $user->password) ? $user : null; } }); }
方案2:自定义中间件动态切换守卫
创建一个中间件,在请求处理阶段(路由解析完成后)动态设置Fortify守卫:
- 创建中间件:
php artisan make:middleware SetFortifyGuard
- 编写中间件逻辑:
<?php namespace App\Http\Middleware; use Closure; use Illuminate\Http\Request; use Laravel\Fortify\Fortify; class SetFortifyGuard { public function handle(Request $request, Closure $next) { if ($request->is('api/v1/admin/*')) { Fortify::guard('admin'); } else { Fortify::guard('web'); } return $next($request); } }
- 将中间件绑定到Admin路由组:
// routes/fortify.php Route::prefix('api/v1/admin')->middleware([\App\Http\Middleware\SetFortifyGuard::class])->group(function () { Fortify::routes(); });
方案3:测试用例中手动设置守卫
仅用于测试场景,在测试方法执行前手动指定守卫:
public function loginAsTestAdmin(): void { // 手动设置Fortify守卫为admin \Laravel\Fortify\Fortify::guard('admin'); // 注意:payload中密码应为原始字符串,而非加密后的值 $payload = ['email' => 'admin@example.com', 'password' => 'password']; $this->post('api/v1/admin/login', $payload)->assertStatus(200); }
关键注意点
- 测试代码中不要用
bcrypt('password')作为请求密码:登录时Fortify会自动用Hash::check对比原始密码与数据库中存储的加密密码,请求只需传原始密码字符串。 - 优先选择方案1或方案2:这两种方式在生产环境和测试环境都能稳定工作,避免了服务初始化阶段请求未解析的问题。
内容的提问来源于stack exchange,提问作者NeebletWorm
相关产品推荐
相关产品推荐

