微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

使用 `GenerateSignedPostPolicyV4` 在 Google Cloud Storage 中上传文件时出现 Cors 错误

如何解决使用 `GenerateSignedPostPolicyV4` 在 Google Cloud Storage 中上传文件时出现 Cors 错误

我正致力于在 Cloud Storage 中使用 signed URL 以允许客户将文件直接上传到 Cloud Storage。

因为我想限制用户可以上传到 GCS 的最大文件大小,我在考虑使用 policy document 通过使用 content-length-range 条件来控制上传行为。我正在使用 GenerateSignedPostPolicyV4 生成发布策略文档。

如果我基于由 GenerateSignedPostPolicyV4 生成的发布策略文档构建 HTML 表单,那么我的设置工作得非常好。

以下是对我来说很好用的 sample form 副本。

<form action="https://storage.googleapis.com/[MY_BUCKET_NAME]/" method="POST" enctype="multipart/form-data">
    <input name="content-type" value="application/octet-stream" type="hidden">
    <input name="key" value="test" type="hidden">
    <input name="policy" value="[MY_POLICY]" type="hidden">
    <input name="x-goog-algorithm" value="GOOG4-RSA-SHA256" type="hidden">
    <input name="x-goog-credential" value="[MY_CREDENTIAL]" type="hidden">
    <input name="x-goog-date" value="20210624T194049Z" type="hidden">
    <input name="x-goog-signature" value="[MY_SIGNATURE]" type="hidden">
    <input type="file" name="file"><br>
    <input type="submit" value="Upload File" name="submit"><br>
</form>

现在,我有一个单页应用程序,如果可能的话,我希望在不使用 HTML 表单的情况下以 JavaScript/TypeScript 的方式以编程方式完成上传。例如,我想使用 fetchxhr 而不是使用标准 HTML 表单上传文件

奇怪的是,当我使用 xhrfetch 发出 POST 请求时,我遇到了 CORS 错误。我确实在我的存储桶中正确设置了 CORS,因为如果我使用标准 SignedUrl 生成 URL 并使用 PUT 方法上传文件上传通过 {{1} 工作正常} 或 xhr,这证明 CORS 已在我的存储桶中正确设置。

(我的 cors 如下所示)

fetch

但是……由于您无法通过 [{"maxAgeSeconds": 3600,"method": ["PUT","POST"],"origin": ["*"],"responseHeader": ["Content-Type","Access-Control-Allow-Origin"]}] 上传来强制执行文件大小限制,因此使用 PUT PUT 不是我的选择。

所以我的问题是,如果我使用基于 xhr/fetch上传,是否需要使用 html 表单将数据上传到 GCS? GCS 是否有任何理由决定对此类提交强制执行 CORS?

解决方法

使用 Signed Url 将对象(文件)上传到 Google Cloud Storage 时,PUT 文件大小限制确实可以强制执行。

这是通过设置 HTTP 标头 x-goog-content-length-range(查找文档 here)并指定您希望签名 URL 允许的字节范围来实现的。例如:

"x-goog-content-length-range":"0,24117249"

这指定将接受上传到该 URL 的文件,大小从 0B(字节)到 23MB(24117249 字节)。

您必须在创建签名 URL 以及访问该 URL(即上传文件)时使用此标头。


编辑

为了回应 Martin Zeitler 的评论,我对该主题进行了更多研究,并设法使用带可恢复上传的签名 URL 获得了一个有点工作的脚本。

它是如何工作的?首先,我们创建带有标头的 POST 方法签名 URL,指示存储桶启动可恢复的上传操作,作为交换,该标头响应 Location 标头,其中包含我们必须将文件发送到的 URI PUT 请求。

您希望在启动服务器之前设置您的凭据。详细了解如何执行此操作here

但是,为了获得调用签名 URL 和将文件上传到存储桶所需的权限,我们需要一个访问令牌。你可以得到它here。你也可以learn more about OAuth2 Authentication。获取上传 URI 和上传时,此访问令牌不必相同;然而,为了简单起见,我决定保持不变。

脚本本身您不想在制作中使用:它仅用于说明目的。

(您需要 flaskgoogle-cloud-storage Python 库才能工作)

ma​​in.py

from flask import Flask,render_template
import datetime,requests
from google.cloud import storage
#----------------------------------------------------------------------
#----------------------------------------------------------------------
def generate_upload_signed_url_v4(bucket_name,blob_name):
    storage_client = storage.Client()
    bucket = storage_client.bucket(bucket_name) #Sets name of the target bucket
    blob = bucket.blob(blob_name) #Sets the filename our object will have once uploaded to the bucket
    headers = {
        "x-goog-resumable":"start",#Needed for creating a resumable upload: https://cloud.google.com/storage/docs/xml-api/reference-headers#xgoogresumable
    }
    url = blob.generate_signed_url(
        version="v4",expiration=datetime.timedelta(minutes=15),headers=headers,method="POST",)
    return url
#----------------------------------------------------------------------
#----------------------------------------------------------------------
bucket_name = 'sample-bucket' #INSERT YOUR BUCKET NAME HERE
blob_name = 'your-desired-filename' #INSERT THE NAME OF THE FILE HARE
url = generate_upload_signed_url_v4(bucket_name,blob_name) #Instantiates the Signed URL to get the Session ID to upload the file

app = Flask(__name__) #Flask

token = "access-token" #Insert access token here
headers = { #Must have the same headers used in the generation of the Signed URL + the Authorization header
    "Authorization":f"Bearer {token}","x-goog-resumable":"start",}
#Get Session ID from the `Location` response header and store it in the `session_url` variable
r = requests.post(url,data="",headers=headers)
if r.status_code == requests.codes.created:
    session_url = r.headers["Location"]
else:
    session_url = "None"
#----------------------------------------------------------------------
#----------------------------------------------------------------------
@app.route("/gcs",methods=["PUT","GET","POST"])
def main():
    return render_template("index.html",token=token,url=session_url) # Sends token and session_url to the template

if __name__ == "__main__":
    app.run(debug=True,port=8080,host="0.0.0.0") #Starts the server on port 8080 and sets the host to 0.0.0.0 (available to the internet)

templates/index.html(了解有关 Flask 模板的更多here):

<html>
   <head>
   </head>
   <body>
      <input type="file" id="fileinput" />
      <script>
         // Select your input type file and store it in a variable
         const input = document.getElementById('fileinput');
         
         // This will upload the file after having read it
         const upload = (file) => {
                 fetch('{{ url }}',{ // Your PUT endpoint -> On this case,the Session ID URL retrieved by the Signed URL
         method: 'PUT',body: file,headers: {
         "Authorization": "Bearer {{ token }}",//I don't think it's a good idea to have this publicly available.
         "x-goog-content-length-range":"0,24117249" //Having this on the front-end may allow users to tamper with your system.
         }
         }).then(
         response => response.text()
         ).then(str => (new window.DOMParser()).parseFromString(str,"text/xml")
         ).then(data => console.log(data) //Prints response sent from server in an XML format
         ).then(success => console.log(success) // Handle the success response object
         ).catch(
         error => console.log(error) // Handle the error response object
         );
         };
         
         const onSelectFile = () => upload(input.files[0]);
         
         input.addEventListener('change',onSelectFile,false); //Whenever a  file is selected,the EventListener is triggered and executes the `onSelectFile` function
      </script>
   </body>
</html>

现在我们必须为我们的存储桶配置 CORS 设置。我们必须通过更改 origin 值来允许我们的服务器。然后,我们必须明确说明我们想要允许的 HTTP 标头和方法。 如果未正确设置,将引发 CORS 错误。

cors.json

[
  {
    "origin": ["http://<your-ip-here>:<yourport-here>"],"responseHeader": [
      "Content-Type","Authorization","Access-Control-Allow-Origin","X-Upload-Content-Length","X-Goog-Resumable","x-goog-content-length-range"
    ],"method": ["PUT","OPTIONS","POST"],"maxAgeSeconds": 3600
  }
]

正确配置后,我们可以使用命令将此配置应用到我们的存储桶

gsutil cors set <name-of-configfile> gs://<name-of-bucket>

要尝试此操作,请转到您的浏览器并输入以下网址:http://<your-ip>:<your-port>/gcs

选择您选择的文件(小于 23MB 或您可以设置的上限),然后观察它实际上如何上传到您的存储桶。

现在您可能想尝试上传大于 x-goog-content-length-range 标头上设置的上限的文件,并观察上传如何失败并出现 EntityTooLarge 错误。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。