PAYATHON 2026

支付成功返回后出现 405 Method Not Allowed

支付小周

结论

支付平台自动返回时,很可能使用了 POST,或者中间经过 307/308 重定向并保留了原请求方法。Laravel 目前只注册了 GET /success/{order},无法接收这个请求,因此返回 405 Method Not Allowed。

手动在浏览器地址栏访问同一个 URL 时,浏览器发送的是 GET,所以页面可以正常打开。

这个问题通常与 Livewire、trustProxies() 或 Spatie Permission 无关。建议把“支付结果通知”和“用户成功页面”分开处理:通知接口接收 POST 并验证支付结果,处理完成后通过 303 重定向到只接受 GET 的成功页面。

为什么相同 URL 会有不同结果

当前路由只允许 GET:

Route::get('/success/{order:id}', Success::class)->name('success');

通过浏览器地址栏访问时,请求类似:

GET /success/123 HTTP/1.1

但部分支付平台提交 returnURL 时,可能发送:

POST /success/123 HTTP/1.1

Laravel 可以匹配 /success/{order} 这个 URI,却找不到允许 POST 的对应路由,于是返回 405。只有在 URI 完全无法匹配时,通常才会返回 404。

还有一种可能:支付平台先向某个地址发送 POST,服务器、反向代理或支付平台随后通过 307 Temporary Redirect 或 308 Permanent Redirect 跳转到成功页面。由于这两种状态码会保留 POST 方法,最终仍会触发 405。

先确认实际请求方法

执行以下命令:

php artisan route:list --path=success

确认该地址目前只注册了 GET|HEAD。

然后在浏览器开发者工具的 Network 面板、Web 服务器访问日志或支付平台的请求日志中检查自动返回请求:

  • Request Method 是否为 POST
  • 是否经过了 301、302、303、307 或 308
  • 最终请求地址是否完全一致
  • 是否存在域名规范化、HTTP 到 HTTPS、尾部斜杠等额外跳转

不能只根据浏览器最终显示的 URL 判断请求方法。

推荐方案:分开处理回调接口和成功页面

成功页面继续只接受 GET:

use App\Livewire\Success;
use Illuminate\Support\Facades\Route;

Route::get('/success/{order:id}', Success::class)
    ->name('success');

另建一个控制器路由,用来接收支付返回或回调:

use App\Http\Controllers\PaymentReturnController;
use Illuminate\Support\Facades\Route;

Route::post('/payments/return', PaymentReturnController::class)
    ->name('payments.return');

支付平台配置为:

'returnURL' => route('payments.return'),

控制器验证支付平台返回的数据,再跳转到成功页面:

<?php

namespace App\Http\Controllers;

use App\Models\Order;
use Illuminate\Http\Request;

class PaymentReturnController
{
    public function __invoke(Request $request)
    {
        // 必须在这里验证支付平台的签名、订单号和支付状态。
        $order = Order::findOrFail($request->input('order_id'));

        // 根据支付平台文档处理并更新订单状态。

        return redirect()
            ->route('success', ['order' => $order])
            ->setStatusCode(303);
    }
}

303 See Other 会明确要求客户端使用 GET 请求跳转后的页面,适合在提交支付结果后展示成功页。

如果支付平台可以分别配置浏览器返回地址和服务器通知地址,建议这样配置:

'returnURL' => route('success', ['order' => $this->order]),
'notifyURL' => route('payments.notify'),

具体参数名以支付平台文档为准,也可能是 callback_url、webhook_url、notify_url 或其他名称。服务器通知接口负责确认支付,returnURL 只负责向用户展示页面。不能因为用户跳转到了成功页,就把订单标记为已支付。

如果支付平台允许指定 GET 返回

如果平台文档明确说明 returnURL 可以通过 GET 打开,可以继续使用当前 Livewire 路由,并由 Laravel 生成 URL:

'returnURL' => route('success', [
    'order' => $this->order,
]),

这比手动拼接 URL 更稳妥:

'https://wearlarimo.com/success/' . $this->order->id

同时检查平台是否提供类似以下配置:

'returnMethod' => 'GET',

这个参数仅作示意。只有支付平台确实提供对应选项时才能使用,不能自行添加。

必须兼容同一路径的 POST 时

如果无法修改支付平台的返回地址,可以为同一个 URI 分别定义 GET 和 POST:

use App\Http\Controllers\PaymentReturnController;
use App\Livewire\Success;
use Illuminate\Support\Facades\Route;

Route::get('/success/{order:id}', Success::class)
    ->name('success');

Route::post('/success/{order:id}', PaymentReturnController::class)
    ->name('success.return');

控制器完成验证后再重定向:

public function __invoke(Request $request, Order $order)
{
    // 验证签名,并向支付平台服务端核实支付状态。

    return redirect()
        ->route('success', ['order' => $order])
        ->setStatusCode(303);
}

不建议直接改成:

Route::any('/success/{order:id}', Success::class);

Route::any() 会允许所有 HTTP 方法,而 Livewire 页面组件主要负责页面展示,不适合直接承担支付回调验证。排查问题时可以临时使用 Route::match(['get', 'post'], ...),正式实现仍应拆分处理逻辑。

CSRF 配置

如果外部支付平台直接向 Laravel 的 web 路由发送 POST,请求通常不会携带 Laravel CSRF Token。解决 405 后,可能还会遇到 419 Page Expired。

Laravel 11 可以在 bootstrap/app.php 中,只为明确的支付回调路径排除 CSRF 检查:

use Illuminate\Foundation\Configuration\Middleware;

->withMiddleware(function (Middleware $middleware) {
    $middleware->validateCsrfTokens(except: [
        'payments/return',
        'payments/notify',
    ]);

    $middleware->web(append: [
        CartMiddleware::class,
    ]);
})

排除 CSRF 检查后,仍必须验证支付平台提供的签名、时间戳、订单金额、商户编号和支付状态。排除 CSRF 并不代表请求可信。

如果回调不依赖 Session,也可以根据项目的路由结构,把服务器通知接口放到不启用 CSRF 的 API 路由中,但签名验证、防重放和幂等处理仍然不可省略。

Middleware 配置是否有问题

现有的 trustProxies() 配置不是此次 405 的主要原因。405 一般出现在路由方法匹配阶段,与代理信任或权限中间件无关。

当前代码连续调用了两次:

$middleware->trustProxies(headers: ...);
$middleware->trustProxies('*');

建议合并成一次,避免配置含糊:

use Illuminate\Http\Request;

$middleware->trustProxies(
    at: '*',
    headers: Request::HEADER_X_FORWARDED_FOR
        | Request::HEADER_X_FORWARDED_HOST
        | Request::HEADER_X_FORWARDED_PORT
        | Request::HEADER_X_FORWARDED_PROTO
        | Request::HEADER_X_FORWARDED_AWS_ELB,
);

只有应用确实部署在可信反向代理之后时,才应信任相应代理。这个配置可能影响 URL Scheme、Host 和 HTTPS 判断,但通常不会把 POST 变成 GET。

还要检查 CartMiddleware 是否会重定向支付返回地址。如果它返回 307/308,原始 POST 方法会被保留。需要跳转到展示页面时,应使用 303,或者将支付回调路径排除在购物车页面的中间件逻辑之外。

各个接口最好保持以下职责边界:

  • POST /payments/notify:接收支付平台的服务器通知,验证签名并更新订单。
  • POST /payments/return:兼容支付平台的浏览器表单返回,再通过 303 跳转。
  • GET /success/{order}:只负责展示 Livewire 成功页面。
  • 成功页读取服务器已经确认的订单状态,不能把访问成功页本身视为支付成功。

备注:内容仅供参考。