Laravel 5 如何从 controller 携带数据 POST 重定向
结论
Laravel 的 redirect() 无法生成携带自定义数据的外部 POST 请求。HTTP 重定向通常只会让浏览器向目标地址发起 GET 请求,不能像表单那样附带 MNT_ID 等 POST 字段。
一般可以这样处理:
- Controller 保存订单和配送信息。
- 生成支付参数和签名。
- 返回一个带有隐藏字段的中转页面。
- 中转页面自动向支付网关提交 POST 表单。
这样可以先保存本地数据,再让用户的浏览器通过 POST 请求进入支付网关。
Controller 中保存订单并准备支付参数
下面假设支付网关要求提交 MNT_ID、MNT_TRANSACTION_ID、MNT_AMOUNT 和 MNT_CURRENCY_CODE。具体字段名称、签名算法和金额格式应以支付平台的接口文档为准。
<?php
namespace App\Http\Controllers;
use App\Order;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class PaymentController extends Controller
{
public function pay(Request $request)
{
$this->validate($request, [
'delivery_address' => 'required|string|max:500',
'payment_method' => 'required|string',
]);
$order = DB::transaction(function () use ($request) {
return Order::create([
'delivery_address' => $request->input('delivery_address'),
'payment_method' => $request->input('payment_method'),
'status' => 'pending',
]);
});
// 金额应从服务器端订单数据中取得,不能信任客户端提交的金额。
$amount = number_format($order->total_amount, 2, '.', '');
$paymentData = [
'MNT_ID' => config('services.payment.merchant_id'),
'MNT_TRANSACTION_ID' => (string) $order->id,
'MNT_AMOUNT' => $amount,
'MNT_CURRENCY_CODE' => 'USD',
];
// 如果网关要求签名,应按其文档生成。
$paymentData['MNT_SIGNATURE'] = $this->makeSignature($paymentData);
return response()->view('payments.redirect', [
'gatewayUrl' => config('services.payment.gateway_url'),
'paymentData' => $paymentData,
]);
}
private function makeSignature(array $data)
{
$payload = implode('|', [
$data['MNT_ID'],
$data['MNT_TRANSACTION_ID'],
$data['MNT_AMOUNT'],
$data['MNT_CURRENCY_CODE'],
]);
return hash_hmac(
'sha256',
$payload,
config('services.payment.secret')
);
}
}
对应的路由可以写成:
Route::post('/payment/pay', 'PaymentController@pay');
这里通过数据库事务保存订单。事务提交后,Controller 不调用 redirect(),而是返回一个用于自动提交表单的 Blade 页面。
创建自动提交的 Blade 页面
创建 resources/views/payments/redirect.blade.php:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>正在跳转到支付页面</title>
</head>
<body>
<p>正在进入支付页面,请稍候……</p>
<form id="payment-form"
method="POST"
action="{{ $gatewayUrl }}">
@foreach ($paymentData as $name => $value)
<input type="hidden"
name="{{ $name }}"
value="{{ $value }}">
@endforeach
<noscript>
<p>浏览器未启用 JavaScript,请点击下面的按钮继续。</p>
<button type="submit">继续支付</button>
</noscript>
</form>
<script>
document.getElementById('payment-form').submit();
</script>
</body>
</html>
浏览器收到页面后,会立即向支付网关提交表单。最终请求类似:
POST /payment HTTP/1.1
Host: gateway.example.com
Content-Type: application/x-www-form-urlencoded
MNT_ID=merchant123&MNT_TRANSACTION_ID=1001&MNT_AMOUNT=99.00
为什么不能直接使用 redirect()
下面的写法只能将参数放入 URL 查询字符串,不能创建 POST 请求:
return redirect()->away(
'https://gateway.example.com/payment?MNT_ID=merchant123'
);
浏览器向目标地址发出的仍然是 GET 请求:
GET /payment?MNT_ID=merchant123 HTTP/1.1
redirect()->with() 同样解决不了这个问题:
return redirect()->away($gatewayUrl)->with('MNT_ID', $merchantId);
with() 通常会将数据写入当前 Laravel 应用的 Session。外部支付网站无法读取这个 Session,也就收不到其中的支付字段。
HTTP 的 307 和 308 状态码可以要求客户端保留原请求方法,但它们会重放原始请求及其请求体,无法用来重新组织支付参数。支付平台也未必支持这种接入方式。支付场景应采用网关明确要求的表单跳转方案。
另一种情况:由服务器直接调用支付接口
如果支付平台提供的是服务端 API,而非浏览器表单跳转接口,Controller 可以发起服务器间 POST 请求:
$response = $httpClient->post($gatewayUrl, [
'form_params' => $paymentData,
]);
该请求发生在 Laravel 服务器和支付网关之间,不会将用户的浏览器带到支付页面。通常还要从响应中取得支付地址或支付凭证,再执行下一步:
$paymentUrl = $result['payment_url'];
return redirect()->away($paymentUrl);
自动提交表单和服务端 API 是两种不同的接入方式,具体选择取决于支付网关的协议,不能混用。
注意事项
- 金额、币种和订单号等数据必须根据服务器端订单生成,不能直接使用浏览器提交的值。
- 商户密钥只能保存在服务器配置中,不能输出到 Blade 页面。
- 为支付参数生成签名时,字段顺序、字符编码和金额格式必须严格遵守网关文档。
- 提交到外部网关的表单通常不需要 Laravel 的
@csrf。@csrf是当前 Laravel 应用的防跨站请求令牌,支付网关一般无法识别。 - 用户从支付页面返回并不代表付款成功。支付状态应通过网关的服务端通知、回调签名或订单查询接口确认。
- 回调处理需要具备幂等性,防止支付平台重复通知时重复发货或重复更新订单。
- 支付网关地址应使用 HTTPS,并通过配置文件或环境变量管理。
- 自动提交页面应保留手动提交按钮,以便在 JavaScript 被禁用或自动提交失败时继续操作。
备注:内容仅供参考。
