React-Django使用签名URL上传GCS报403 SignatureDoesNotMatch错误求助
解决Google Cloud Storage签名URL上传时的SignatureDoesNotMatch错误
问题概述
生成签名URL用于客户端直接上传最大1GB的MP3/视频文件至Google Cloud Storage,但上传时触发以下错误:
return (<Code> SignatureDoesNotMatch </Code> <Message> Access denied. </Message> <Details> The request signature we calculated does not match the signature you provided. Check your Google secret key and signing method. </Details> )
生成的URL格式示例:
https://storage.googleapis.com/bucket-name/filename?X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=something.iam.gserviceaccount.com%2xyz%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20240207T120631Z&X-Goog-Expires=900&X-Goog-SignedHeaders=content-type%3Bhost&X-Goog-Signature=21...
前端React上传代码
const responseForSignedUrl = await axios.get(`${baseUrl}/api/posts/getuploadurl/`); if (responseForSignedUrl.status !== 200) { throw new Error("Failed to obtain signed URL."); } const signedUrl = responseForSignedUrl.data.url; // Upload video file to Cloud Storage using the signed URL const videoFormData = new FormData(); videoFormData.append("file", video_file); const uploadResponse = await axios.put(signedUrl, videoFormData, { headers: { "Content-Type": "video/mp4", }, onUploadProgress: (progressEvent) => { const percentCompleted = Math.round( (progressEvent.loaded * 100) / progressEvent.total ); setProgress(percentCompleted); }, }); if (!uploadResponse.ok) { throw new Error("Failed to upload video file."); }
后端Django生成签名URL代码
def generate_upload_signed_url_v4(request): """Generates a v4 signed URL for uploading a blob using HTTP PUT. Note that this method requires a service account key file. You can not use this if you are using Application Default Credentials from Google Compute Engine or from the Google Cloud SDK. """ bucket_name = 'production-bucket-name' blob_name = 'test1' # storage_client = storage.Client() current_directory = os.path.dirname(os.path.abspath(__file__)) # Navigate to the parent directory (folder A) parent_directory = os.path.dirname(current_directory) # Access file B within folder A file_b_path = os.path.join(parent_directory, "service-credentials.json") storage_client = storage.Client.from_service_account_json(file_b_path) bucket = storage_client.bucket(bucket_name) blob = bucket.blob(blob_name) print(storage_client) url = blob.generate_signed_url( version="v4", # This URL is valid for 15 minutes expiration=datetime.timedelta(minutes=15), # Allow PUT requests using this URL. method="PUT", content_type="video/mp4", ) return JsonResponse({'url': url})
已尝试操作
- 创建拥有有效权限的新服务账户并获取密钥,问题仍存在
- 更换Content-Type为
application/octet-stream和multipart/form-data
核心问题与修复方案
问题根源在于前端上传方式错误:
不要使用FormData封装文件
签名URL的PUT请求要求直接上传文件原始内容,而FormData会自动将请求的Content-Type修改为multipart/form-data; boundary=...,即使手动设置video/mp4也会被覆盖,导致签名验证时的Content-Type不匹配,触发SignatureDoesNotMatch错误。直接上传文件对象
去掉FormData封装,直接将文件对象作为PUT请求的主体。修改后的前端代码如下:
const responseForSignedUrl = await axios.get(`${baseUrl}/api/posts/getuploadurl/`); if (responseForSignedUrl.status !== 200) { throw new Error("Failed to obtain signed URL."); } const signedUrl = responseForSignedUrl.data.url; // 直接上传文件对象,无需FormData const uploadResponse = await axios.put(signedUrl, video_file, { headers: { "Content-Type": "video/mp4", // 必须与签名URL生成时指定的content_type完全一致 }, onUploadProgress: (progressEvent) => { const percentCompleted = Math.round( (progressEvent.loaded * 100) / progressEvent.total ); setProgress(percentCompleted); }, }); if (!uploadResponse.ok) { throw new Error("Failed to upload video file."); }
- 额外注意事项
- 确保生成签名URL时的
content_type与上传请求头的Content-Type完全一致,包括大小写、附加参数(例如video/mp4; codecs="avc1.42E01E, mp4a.40.2"需完全匹配) - 针对1GB大文件,后续可考虑启用GCS分块上传,但需先解决当前签名不匹配问题
- 再次确认服务账户拥有
storage.objects.create权限,避免权限遗漏
内容的提问来源于stack exchange,提问作者Vaibhav Rathod
相关产品推荐
相关产品推荐

