You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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守卫:

  1. 创建中间件:
php artisan make:middleware SetFortifyGuard
  1. 编写中间件逻辑:
<?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);
    }
}
  1. 将中间件绑定到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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.27 22:07:40