使用Next.js客户端上传至GCloud签名URL时出现403 Forbidden错误
GCS签名URL前端上传403问题排查与修复
问题根源分析
- Content-Type不匹配:签名URL生成时若指定了
Content-Type约束,前端必须完全匹配该值(包括大小写、参数细节);若生成时未指定,前端手动设置Content-Type会触发签名校验失败。 - 错误的请求体格式:纯JS代码中使用
FormData作为PUT请求体,会自动将Content-Type改为multipart/form-data,与签名URL预期的文件原生MIME类型不符。 - 响应解析错误:GCS的PUT成功响应为空,前端尝试解析JSON会抛出不必要的错误。
修复方案
1. 调整Next.js上传代码
移除手动设置的Content-Type,直接传递文件对象,并修正响应处理逻辑:
"use client" import { getSignedURLs } from "../../upload/upload" export default function Images(props) { const submitImages = async (e) => { e.preventDefault() const form = e.currentTarget const data = new FormData(form) const file = data.get("file") try { const signedUrls = await getSignedURLs(1) const signedUrl = signedUrls[0] const response = await fetch(signedUrl, { method: "PUT", body: file // 不手动设置Content-Type,由浏览器自动匹配文件类型 }) if (response.ok) { console.log("文件上传成功") // 此处处理上传成功后的业务逻辑 } else { const errorDetails = await response.text() throw new Error(`上传失败: ${response.status} - ${errorDetails}`) } } catch (error) { console.error("上传出错:", error) } } return ( <form id="uploadForm" onSubmit={submitImages}> <input type="file" name="file" /> <button type="submit">Upload</button> </form> ) }
2. 修复纯JS上传代码
移除FormData包装,直接传递文件对象:
document.getElementById("uploadForm").addEventListener("submit", async function (event) { event.preventDefault(); const file = this.file.files[0]; try { const signedUrls = await getSignedURLs(1); const signedUrl = signedUrls[0]; const response = await fetch(signedUrl, { method: "PUT", body: file // 不设置Content-Type }); if (response.ok) { console.log("文件上传成功"); } else { const errorDetails = await response.text(); throw new Error(`上传失败: ${response.status} - ${errorDetails}`); } } catch (error) { console.error("上传出错:", error); } });
3. 后端签名URL生成校验
- 若不需要限制上传文件类型,生成签名URL时不要添加
Content-Type条件; - 若需要限制,确保前端实际发送的
Content-Type与生成时指定的值完全一致(比如image/jpeg不能写成image/JPEG); - 检查签名URL的有效期,避免上传时已过期。
4. 存储桶CORS配置检查
确保存储桶允许当前域名的PUT请求,示例CORS配置:
[ { "origin": ["https://你的Next.js域名"], "method": ["PUT"], "responseHeader": ["Content-Type"], "maxAgeSeconds": 3600 } ]
内容的提问来源于stack exchange,提问作者Blagoy Simandoff
相关产品推荐
相关产品推荐

