Laravel 中 Square webhook 无法获取 Auth::user()
结论
Auth::user() 返回 null 是正常的。Square webhook 是 Square 服务器发起的独立 HTTP 请求,不会携带付款用户浏览器中的 Laravel Session、Sanctum Cookie 或访问令牌,所以 Laravel 无法从 webhook 请求中还原登录用户。
处理这类请求时,应在创建 Square Checkout、Order 或 Payment 的同时,保存 Square 付款标识与本地用户之间的关联。收到 payment.updated 事件后,从中读取 payment.id、order_id、reference_id 或 customer_id,再查询本地数据库并更新对应用户,不要依赖 Auth::user()。
为什么 webhook 中没有登录用户
用户发起付款和 Square 发送 webhook,是两个彼此独立的请求:
- 用户通过浏览器请求 Laravel 应用,请求中可能带有 Session Cookie 或 Sanctum 凭证。
- Laravel 创建 Square Checkout、Order 或 Payment。
- Square 处理付款。
- Square 服务器请求你的 webhook 地址。
第 4 步的请求来自 Square 服务器,不是用户的浏览器。即使用户仍处于登录状态,Square 也拿不到该用户的 Laravel Session Cookie。因此:
Auth::check(); // false
Auth::user(); // null
这和 CSRF 排除、Session 持久化或 Sanctum 配置没有直接关系。CSRF 排除只是允许第三方服务器访问该路由,不会为请求附加用户身份。
日志中的错误:
Attempt to assign property "subscription_status" on null
说明代码正在给一个值为 null 的查询结果赋值,例如:
$user = Auth::user();
$user->subscription_status = 'active';
赋值操作本身没有问题,问题是 $user 并不存在。
推荐的数据关联方式
可以建立一张本地付款表,用来记录本地用户与 Square 付款对象之间的关系:
Schema::create('payments', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->string('square_payment_id')->nullable()->unique();
$table->string('square_order_id')->nullable()->index();
$table->string('square_checkout_id')->nullable()->index();
$table->string('square_customer_id')->nullable()->index();
$table->string('status')->default('pending');
$table->timestamps();
});
需要保存哪些字段,取决于项目使用的 Square API 流程。通常可以按以下顺序选择:
payment.id:最直接的付款标识。payment.order_id:适合通过 Square Order 关联本地订单。reference_id:如果当前 Square 请求支持,可以写入本地订单号等非敏感业务标识。customer_id:可以辅助关联,但前提是已经保存 Square Customer 与本地用户的映射。
Square 的 customer_id 和 Laravel 的 users.id 来自两个不同的系统,不能默认认为它们相等。
创建付款时保存关联
用户发起创建付款的请求时仍处于登录状态,可以在此时取得用户 ID,并创建本地付款记录:
use Illuminate\Http\Request;
use Illuminate\Support\Str;
public function createCheckout(Request $request)
{
$user = $request->user();
$localPayment = Payment::create([
'user_id' => $user->id,
'status' => 'pending',
]);
$referenceId = 'payment_'.$localPayment->id;
// 按当前使用的 Square SDK 构造 Checkout、Order 或 Payment 请求。
// 如果对应接口支持 reference_id,可将 $referenceId 写入请求。
$squareResponse = $this->squareService->createCheckout(
user: $user,
referenceId: $referenceId,
idempotencyKey: (string) Str::uuid(),
);
$localPayment->update([
'square_checkout_id' => $squareResponse->getCheckoutId(),
'square_order_id' => $squareResponse->getOrderId(),
]);
return response()->json([
'checkout_url' => $squareResponse->getCheckoutUrl(),
]);
}
这里的 Square SDK 返回方法仅用于说明代码结构,实际方法名要以项目使用的 SDK 版本和 API 响应为准。重点是在创建 Square 对象时保存本地 user_id,不要等到 webhook 到达后再查找当前登录用户。
如果 Payment ID 只有在付款完成后才会出现,可以先通过 order_id 或业务 reference_id 建立关联,收到第一次 webhook 后,再将 payment.id 写入本地记录。
在 webhook 中查找用户
Square 的 payment.updated 事件通常可以从以下位置取得 Payment 对象:
$payload['data']['object']['payment']
控制器可以这样处理:
use App\Models\Payment;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
public function handlePaymentWebhook(Request $request)
{
$payload = $request->json()->all();
if (($payload['type'] ?? null) !== 'payment.updated') {
return response()->json(['received' => true]);
}
$squarePayment = data_get($payload, 'data.object.payment');
if (!is_array($squarePayment)) {
Log::warning('Square webhook does not contain a payment object', [
'event_id' => $payload['event_id'] ?? null,
]);
return response()->json(['received' => true]);
}
$squarePaymentId = $squarePayment['id'] ?? null;
$squareOrderId = $squarePayment['order_id'] ?? null;
$status = $squarePayment['status'] ?? null;
$localPayment = Payment::query()
->when($squarePaymentId, function ($query) use ($squarePaymentId) {
$query->where('square_payment_id', $squarePaymentId);
})
->when(
!$squarePaymentId && $squareOrderId,
function ($query) use ($squareOrderId) {
$query->where('square_order_id', $squareOrderId);
}
)
->first();
// 如果尚未保存 payment ID,可使用 order ID 回退查找。
if (!$localPayment && $squareOrderId) {
$localPayment = Payment::where(
'square_order_id',
$squareOrderId
)->first();
}
if (!$localPayment) {
Log::warning('No local payment matches Square webhook', [
'event_id' => $payload['event_id'] ?? null,
'square_payment_id' => $squarePaymentId,
'square_order_id' => $squareOrderId,
]);
// 返回 2xx,避免无法处理的数据被持续重试;
// 如果该情况属于暂时性故障,也可以按业务策略返回非 2xx。
return response()->json(['received' => true]);
}
DB::transaction(function () use (
$localPayment,
$squarePaymentId,
$status
) {
$localPayment->update([
'square_payment_id' =>
$localPayment->square_payment_id ?? $squarePaymentId,
'status' => $status ?? $localPayment->status,
]);
if ($status === 'COMPLETED') {
$localPayment->user()->update([
'subscription_status' => 'active',
]);
}
});
return response()->json(['received' => true]);
}
关联模型还需要定义用户关系:
use Illuminate\Database\Eloquent\Relations\BelongsTo;
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
是否启用订阅,应由具体业务规则决定。不能只要收到 payment.updated 就直接开通,因为同一种事件可能包含不同的付款状态。至少要检查 payment.status,并根据业务需要校验金额、货币、门店、订单内容,以及付款是否已退款或取消。
使用 reference_id 关联
如果创建的 Square Order 支持 reference_id,可以在其中写入本地付款编号:
$referenceId = 'local-payment-'.$localPayment->id;
收到 webhook 后,如果 Payment 中包含 order_id,可以查询对应的 Square Order,读取其 reference_id,再查找本地记录:
$localPaymentId = Str::after(
$squareOrder->getReferenceId(),
'local-payment-'
);
$localPayment = Payment::find($localPaymentId);
即使编号来自自己创建的 reference_id,也要检查格式,并确认本地付款记录中保存的 Square order_id 与 webhook 一致。不要把未经验证的外部字段直接当作可信的用户 ID。
customer_id 能否直接使用
如果每位 Laravel 用户都有对应的 Square Customer,可以在用户表中保存这项映射:
Schema::table('users', function (Blueprint $table) {
$table->string('square_customer_id')->nullable()->unique();
});
然后在 webhook 中查询用户:
$squareCustomerId = $squarePayment['customer_id'] ?? null;
$user = User::where(
'square_customer_id',
$squareCustomerId
)->first();
只有在付款确实绑定了 Customer 时,这种方式才可靠。访客付款或未关联 Customer 的 Payment 可能不包含 customer_id。一个用户也可能有多笔付款,因此仍建议保留独立的本地付款或订阅记录。
webhook 签名验证
验证签名时必须使用原始请求体,不能先解析 JSON 再重新编码:
$body = $request->getContent();
$signature = $request->header(
'x-square-hmacsha256-signature'
);
如果手动比较签名,至少要使用常量时间比较:
private function isFromSquare(
?string $signature,
string $body
): bool {
if (!$signature) {
return false;
}
$hash = hash_hmac(
'sha256',
self::NOTIFICATION_URL.$body,
self::SIGNATURE_KEY,
true
);
return hash_equals(base64_encode($hash), $signature);
}
NOTIFICATION_URL 必须与 Square 后台配置的 webhook URL 完全一致,包括协议、域名、路径和可能存在的尾部斜杠。如果当前 Square SDK 提供官方的 webhook 签名验证辅助方法,应优先使用,并按照所用 SDK 版本的接口调用。
签名密钥也不应硬编码在类中:
private function signatureKey(): string
{
return config('services.square.webhook_signature_key');
}
环境变量可以这样配置:
SQUARE_WEBHOOK_SIGNATURE_KEY=...
SQUARE_WEBHOOK_NOTIFICATION_URL=https://example.com/payment-plan-webhook
还需要处理重复和乱序事件
webhook 可能重复发送,也不一定严格按照业务顺序到达。生产环境应保存 Square 的 event_id,已经处理过的事件可以直接返回成功:
if (WebhookEvent::where(
'provider_event_id',
$payload['event_id']
)->exists()) {
return response()->json(['received' => true]);
}
最好在同一个数据库事务中更新事件记录、付款状态和订阅状态,并为 provider_event_id 添加唯一索引。这样即使两个相同的 webhook 同时到达,也不会重复开通订阅。
需要避免的做法
不要通过以下方式识别付款用户:
Auth::user();
session('user_id');
config('user_id');
config() 读取的是应用配置,不是当前付款人的运行时身份。把用户 ID 临时写入全局配置、缓存固定键或静态变量,在并发请求中还可能把付款关联到错误的用户。
可靠的关联链路是:
Laravel user
-> local payment/order
-> Square checkout/order/payment
-> Square webhook
-> local payment/order
-> Laravel user
只要在创建付款时持久化这条关联,webhook 就不需要依赖 Laravel 的登录状态,也不应该这样做。
备注:内容仅供参考。
