解决 Laravel 11 404 页面 Web 中间件失效问题 (附方案)
2025-04-28 11:10:09
解决 Laravel 11 中 404 页面不经过 Web 中间件的问题
搞 Laravel 11 开发,特别是用了 Twill CMS 之后,你可能碰到过这么个怪事儿:自己写的一个重定向中间件,对能正常访问的页面(比如 /existing-page
,返回 200 OK)工作得好好的,能把用户跳转到你指定的任何地方。可一旦访问一个不存在的地址(比如 /non-existent-page
,返回 404 Not Found),嘿,这中间件就跟消失了一样,根本不执行,重定向自然也就没戏了。
就算试了把中间件加到 bootstrap/app.php
里的 web
中间件组,或者在 web.php
里定义了 Route::fallback()
并给它套上中间件,结果还是一样——404 的请求就是不理睬你的中间件。
这到底是咋回事呢?
一、 问题根源:请求生命周期和异常处理
要弄明白为啥 404 请求不走 web
中间件组里的自定义中间件,得先看看 Laravel 处理请求的大致流程。
- 请求来了 : 请求先进来,通过
public/index.php
。 - 内核处理 : 请求被交给 HTTP 内核 (
App\Http\Kernel
)。 - 全局中间件 : 请求会先穿过 全局 HTTP 中间件。这些是在
$middleware
属性里定义的。 - 路由匹配 : Laravel 的路由器开始工作,尝试把请求的 URI 跟
routes/web.php
或routes/api.php
里定义的路由规则配对。 - 找到路由 : 如果找到了匹配的路由:
- 请求接着会穿过该路由所属的 中间件组 (比如
web
或api
组)。 - 然后,再穿过 路由特定 的中间件(就是用
->middleware()
直接链式附加到路由定义上的那些)。 - 最后,请求到达控制器方法执行业务逻辑。
- 请求接着会穿过该路由所属的 中间件组 (比如
- 找不到路由 (404) : 如果路由器翻遍了所有路由规则,都没找到能匹配当前请求 URI 的,它就没辙了。这时候,Laravel 会抛出一个
Symfony\Component\HttpKernel\Exception\NotFoundHttpException
异常。 - 异常处理 : 这个异常会被 Laravel 的异常处理器 (
App\Exceptions\Handler
) 捕获。默认情况下,异常处理器会查找对应的错误视图(比如resources/views/errors/404.blade.php
)并渲染返回给用户一个 404 页面。
关键点来了 :当 NotFoundHttpException
异常被抛出时,请求的常规处理流程就被 打断 了。它不会继续往下走,去执行原本计划要加载的中间件组(比如 web
组里我们加的那个 Redirector
中间件)或者路由特定的中间件。请求直接被拐到了异常处理那边去。
这就是为啥你的重定向中间件放在 web
组里对 404 请求无效。它根本没机会运行。Route::fallback()
稍微有点不同,它确实是在找不到其他路由时才会执行,但它本质上还是一个路由,其执行时机和环境,以及附加的中间件是否能在异常流程中稳定触发,可能不如我们预期的那么直接。
二、 解决方案
明白了原因,解决起来就有方向了。我们需要让重定向逻辑在 Laravel 判定这是一个 404 请求 之前 或者 在其处理过程中 介入。
方案一:把中间件提升为全局中间件
最直接的办法,就是不把 Redirector
中间件放在 web
组里,而是把它提升为全局中间件。全局中间件对 所有 HTTP 请求都生效,包括那些最终会变成 404 的请求,因为它们在路由匹配发生 之前 就运行了。
操作步骤:
- 打开
bootstrap/app.php
文件。 - 找到配置中间件的地方,通常是
->withMiddleware(function (Middleware $middleware) { ... })
这块。 - 不要 在
$middleware->group('web', [...])
里面加。 - 使用
$middleware->append(...)
或$middleware->prepend(...)
将你的中间件添加到全局栈。append
加到最后,prepend
加到最前面。对于重定向这种可能提前结束请求的逻辑,放在前面 (prepend
) 可能更合适,避免不必要的后续处理。
// bootstrap/app.php
use App\Http\Middleware\Redirector; // 引入你的中间件
use Illuminate\Foundation\Configuration\Middleware;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware) {
// 把你的重定向中间件加到全局,这里用 prepend 加到最前面
$middleware->prepend(Redirector::class);
// 这是原来 web 组的配置,里面不要再加 Redirector::class 了
$middleware->group('web', [
\Illuminate\Cookie\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
// ... 其他 web 中间件
// \App\Http\Middleware\Redirector::class, // 这一行要从 web 组移除或注释掉
]);
// 其他中间件配置...
})
->withExceptions(function (Exceptions $exceptions) {
// ...
})->create();
原理与作用:
- 全局中间件在 Laravel 处理流程的早期运行,早于路由查找。
- 不管请求最终是 200 还是 404,你的
Redirector
中间件都会执行。 - 在中间件内部,你可以检查请求的路径,查询数据库里的重定向规则。如果找到了匹配的规则,直接返回一个
RedirectResponse
,请求就此结束,根本不会走到抛出 404 异常那一步。如果没找到匹配的重定向规则,就调用$next($request)
让请求继续往下走。
代码示例 (Redirector
中间件大致逻辑):
// app/Http/Middleware/Redirector.php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Redirect;
use App\Models\Redirection; // 假设你的重定向模型叫这个
class Redirector
{
public function handle(Request $request, Closure $next)
{
// 获取当前请求路径,可能需要处理一下开头的 '/'
$path = '/' . trim($request->path(), '/');
// 去数据库或其他地方查找是否有针对这个路径的重定向规则
// 注意:这里的查询逻辑需要根据你的 Redirection 模型/表结构调整
$redirection = Redirection::where('source_url', $path)->where('is_active', true)->first();
if ($redirection) {
// 找到了!执行重定向
// 使用永久重定向 (301) 还是临时重定向 (302) 取决于你的业务需求
return Redirect::to($redirection->target_url, $redirection->status_code ?? 301);
}
// 没找到重定向规则,让请求继续正常处理
return $next($request);
}
}
安全建议:
- 确保
Redirector
中间件里的数据库查询是高效的,因为它会对每个请求都执行。考虑使用缓存来减少数据库压力。 - 对用户输入的重定向规则(如果允许用户配置的话)做好验证和清理,防止开放重定向漏洞 (Open Redirect Vulnerability)。
进阶使用技巧:
- 你可以在中间件里加入更复杂的逻辑,比如基于用户角色、设备类型或其他请求特征来决定是否重定向。
- 如果重定向规则很多,考虑优化查询性能,比如给
source_url
字段加索引。
方案二:自定义异常处理
另一个思路是让 Laravel 按照默认流程走,当它因为找不到路由而准备抛出 NotFoundHttpException
时,我们在异常处理器里把它“截胡”,然后执行我们的重定向逻辑。
操作步骤:
- 打开
app/Exceptions/Handler.php
文件。 - 找到
register()
方法。这个方法用来注册异常处理的回调。 - 在
register()
方法内部,使用$this->renderable()
来添加一个针对NotFoundHttpException
的处理逻辑。
// app/Exceptions/Handler.php
namespace App\Exceptions;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Redirect; // 引入 Redirect门面
use App\Models\Redirection; // 引入你的重定向模型
class Handler extends ExceptionHandler
{
// ... 其他属性和方法 ...
/**
* Register the exception handling callbacks for the application.
*/
public function register(): void
{
$this->renderable(function (NotFoundHttpException $e, Request $request) {
// 检查是否是 web 请求 (如果你的重定向只针对 web)
// 或者根据其他条件判断是否需要执行重定向逻辑
if ($request->isMethod('GET')) { // 通常重定向只处理 GET 请求
// 获取请求路径
$path = '/' . trim($request->path(), '/');
// 查找重定向规则 (同上一个方案的逻辑)
$redirection = Redirection::where('source_url', $path)->where('is_active', true)->first();
if ($redirection) {
// 找到规则,返回重定向响应
return Redirect::to($redirection->target_url, $redirection->status_code ?? 301);
}
}
// 如果没有找到重定向规则,或者不满足执行重定向的条件,
// 不返回任何东西 (返回 null),让 Laravel 继续执行默认的 404 页面渲染
// 或者,如果你想完全自定义 404 页面,可以在这里返回一个自定义的视图响应
// return response()->view('errors.custom_404', [], 404);
});
// 可以链式调用 reportable() 或 renderable() 注册其他异常处理
// $this->reportable(function (Throwable $e) {
// // ...
// });
}
// ... 其他方法,比如 render 方法可能已经存在,可以保留 ...
}
原理与作用:
- 当 Laravel 捕获到
NotFoundHttpException
时,它会检查Handler
类中是否有专门处理这种异常的renderable
回调。 - 我们注册的回调函数被触发。在这个回调里,我们再次执行查找重定向规则的逻辑。
- 如果找到了规则,回调函数直接返回一个
RedirectResponse
。这个响应会替代默认的 404 页面响应。 - 如果没找到规则,回调函数不返回任何东西(或者说返回
null
),Laravel 会继续执行它原本为NotFoundHttpException
准备的默认处理流程(通常是渲染 404 错误视图)。
代码示例 (已包含在操作步骤中)
安全建议:
- 同样要注意开放重定向漏洞的风险。
- 在异常处理器里执行数据库查询要小心性能影响,虽然比全局中间件触发频率低(只在 404 时触发),但依然可能在遭受恶意扫描时被频繁调用。缓存策略同样适用。
- 避免在错误处理逻辑中暴露敏感信息。
进阶使用技巧:
- 你可以把重定向逻辑封装到一个单独的服务类里,然后在中间件和异常处理器里都注入并调用这个服务,避免代码重复。
- 可以结合请求的其他信息(如
Referer
头)来实现更复杂的 404 处理策略。
关于 Route::fallback()
的一点补充
为啥之前尝试 Route::fallback()->middleware('redirect')
可能不行?虽然 fallback 路由本身会在找不到其他路由时执行,但它执行的时机以及附加在其上的中间件的执行,依然是在 Laravel 已经初步判断“找不到路由”之后。某些版本的 Laravel 或者特定的中间件交互下,这个中间件可能仍然无法按预期在最终渲染 404 响应之前可靠地触发并中断流程。全局中间件(方案一)或者异常处理(方案二)介入的时机更早或更专门针对“异常状态”,因此对于“在 404 发生时执行特定逻辑(如重定向)”这个需求通常更可靠。
选择哪个方案取决于你的具体需求和偏好。如果你的重定向逻辑很简单,或者你希望它能覆盖所有请求(包括未来可能添加的非 web 请求),全局中间件可能更省事。如果你想让重定向逻辑只在确认为 404 时才执行,并且希望保持中间件组织的“语义纯净性”(即 web
组处理常规 Web 逻辑,错误处理归于 Handler
),那么自定义异常处理是更好的选择。