Laravel Guzzle POST请求返回403 Forbidden问题排查与代码修正
问题
使用Laravel Livewire控制器代码通过Guzzle发送POST请求时返回403 Forbidden,但Postman发送相同参数的请求可正常响应。签名已通过签名板生成并正确保存到存储目录,但无法通过file_variables参数将文件传递给API。
环境:PHP 8.2.0 / Laravel 10.6.2
原控制器代码
public function next() { $this->header = [ 'Accept' => 'application/json', 'X-API-KEY' => $this->user->api_key, 'Content-Type' => 'multipart/form-data', ]; $body = [ [ 'name' => 'u', 'contents' => $this->task->action->systask, ], [ 'name' => 'session', 'contents' => $this->task->session, ], [ 'name' => 'name', 'contents' => $this->currentQ['qName'], ], ]; if (isset($this->currentQ['qType'])) { if ($this->currentQ['qType'] == 'signature') { //upload signature file Storage::disk("docs")->put('signatures/signature' . $this->user->da_id . '.png', base64_decode(Str::of($this->currentVariables[html_entity_decode($this->currentQFields[0]['variable_name'])])->after(','))); $sign = fopen(storage_path('docs/signatures/signature' . $this->user->da_id . '.png'), 'r'); array_push( $body, [ 'name' => 'file_variables', 'contents' => json_encode([$this->currentQFields[0]['variable_name'] => $this->currentQFields[0]['variable_name']]) ], [ 'name' => 'signature1', 'contents' => $sign, ], ); if (isset($this->currentVariables[html_entity_decode($this->currentQFields[0]['variable_name'])])) { unset($this->currentVariables[html_entity_decode($this->currentQFields[0]['variable_name'])]); } } else { array_push( $body, [ 'name' => 'variables', 'contents' => json_encode($this->currentVariables), ], ); } } $api_url = config('hrda.base_uri') . config('hrda.resume_session'); $client = new Client(); try { $response = $client->request('POST', $api_url, [ ['headers' => $this->header], 'multipart' => $body, ]); } catch (RequestException $e) { return back()->with('error', $e->getMessage()); } // ... }
已尝试操作
- 使用
file_get_contents、fopen、Base64编码/解码方式发送文件,均无效 - API已允许当前服务器IP(Postman可正常访问)
Postman请求信息
请求头
x-api-key: oNV9KwVRCSVG5asdWEXwlEG2nf2
Accept: application/json表单数据(form-data)
Session: b2QWUlCEOWJLr7dnLLlgnGzpwaIiZ9Ax
u: frontend.yml
signature: C:\users/xyz/storage/userDocs/signatures/signature7.png
name: Question_2
排查与修正方案
1. 移除手动设置的Content-Type头
Guzzle处理multipart请求时会自动生成包含boundary的正确Content-Type,手动设置会覆盖该值,导致请求格式错误,触发API的403校验。
修正:删除$this->header中的'Content-Type' => 'multipart/form-data',
2. 修复Guzzle请求参数格式
原代码中request方法的第三个参数错误地将headers嵌套在额外数组中,导致请求头未正确传递。
修正:将
$response = $client->request('POST', $api_url, [ ['headers' => $this->header], 'multipart' => $body, ]);
改为
$response = $client->request('POST', $api_url, [ 'headers' => $this->header, 'multipart' => $body, ]);
3. 对齐表单字段名
Postman中文件字段名为signature,但代码中使用signature1,字段名不匹配会导致API无法识别文件,引发校验失败。
修正:将'name' => 'signature1',改为'name' => 'signature',
4. 优化文件读取逻辑
使用Laravel Storage的path()方法获取文件路径更可靠,同时需手动关闭文件句柄避免资源泄漏。
修正:
// 替换原文件读取代码 $filePath = Storage::disk('docs')->path('signatures/signature' . $this->user->da_id . '.png'); $sign = fopen($filePath, 'r'); // 请求完成后添加关闭语句 fclose($sign);
5. 修正file_variables的JSON格式
原代码中file_variables的JSON内容为[变量名=>变量名],不符合API预期,应映射为[变量名=>文件名]。
修正:
[ 'name' => 'file_variables', 'contents' => json_encode([ $this->currentQFields[0]['variable_name'] => 'signature' . $this->user->da_id . '.png' ]) ]
6. 添加文件名参数到文件字段
Guzzle上传文件时指定filename参数,确保API能正确识别文件名,避免解析错误。
修正:
[ 'name' => 'signature', 'contents' => $sign, 'filename' => 'signature' . $this->user->da_id . '.png' ]
修正后的完整代码
public function next() { $this->header = [ 'Accept' => 'application/json', 'X-API-KEY' => $this->user->api_key, ]; $body = [ [ 'name' => 'u', 'contents' => $this->task->action->systask, ], [ 'name' => 'session', 'contents' => $this->task->session, ], [ 'name' => 'name', 'contents' => $this->currentQ['qName'], ], ]; if (isset($this->currentQ['qType'])) { if ($this->currentQ['qType'] == 'signature') { // 保存签名文件 $signatureVarName = html_entity_decode($this->currentQFields[0]['variable_name']); $signatureFileName = 'signature' . $this->user->da_id . '.png'; Storage::disk("docs")->put( 'signatures/' . $signatureFileName, base64_decode(Str::of($this->currentVariables[$signatureVarName])->after(',')) ); // 读取文件 $filePath = Storage::disk('docs')->path('signatures/' . $signatureFileName); $sign = fopen($filePath, 'r'); // 添加表单字段 array_push( $body, [ 'name' => 'file_variables', 'contents' => json_encode([$signatureVarName => $signatureFileName]) ], [ 'name' => 'signature', 'contents' => $sign, 'filename' => $signatureFileName ], ); // 移除变量 if (isset($this->currentVariables[$signatureVarName])) { unset($this->currentVariables[$signatureVarName]); } // 关闭文件句柄 fclose($sign); } else { array_push( $body, [ 'name' => 'variables', 'contents' => json_encode($this->currentVariables), ], ); } } $api_url = config('hrda.base_uri') . config('hrda.resume_session'); $client = new Client(); try { $response = $client->request('POST', $api_url, [ 'headers' => $this->header, 'multipart' => $body, ]); } catch (RequestException $e) { return back()->with('error', $e->getMessage()); } // ... }
内容的提问来源于stack exchange,提问作者Salman

