使用“GenerateSignedPostPolicyV4”上传 Google 云存储中的文件时出现 Cors 错误

Cors error when using `GenerateSignedPostPolicyV4` to upload a file in Google Cloud Storage

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

因为我想限制用户可以上传到 GCS 的文件的最大大小,我正在考虑使用 policy document to control the upload behavior by using content-length-range condition. I am using GenerateSignedPostPolicyV4 来生成 post 政策文件。

如果我根据由 GenerateSignedPostPolicyV4.

生成的 post 策略文档构建 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>

现在,我有单页应用程序,如果可能,我想在 JavaScript/TypeScript 中以编程方式完成上传,而不使用 HTML 表单。例如,我想使用 fetchxhr 来上传文件,而不是使用标准的 HTML 格式。

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

(我的 cors 如下所示)

[{"maxAgeSeconds": 3600, "method": ["PUT", "POST"], "origin": ["*"], "responseHeader": ["Content-Type", "Access-Control-Allow-Origin"]}]

但是.. 因为你不能通过 PUT 上传强制文件大小限制,所以使用 PUT xhr/fetch 对我来说不是一个选择。

所以我的问题是,如果我使用基于post policy doc的上传方式,是否需要使用html形式将数据上传到GCS? GCS 决定对此类提交强制执行 CORS 有什么原因吗?

使用 Signed Url 将 object(文件)上传到 Google 云存储时,PUT 文件大小限制确实是强制执行的。

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

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

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

您必须在创建签名 URL 和访问 URL 也就是上传文件时使用此 header。


编辑:

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

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

您想在启动服务器之前设置凭据。详细了解如何操作 here

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

脚本本身不是您想要在生产中使用的东西:它仅用于说明目的。

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

main.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(了解更多 here Flask 模板):

<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 headers 和方法。 如果设置不正确,将引发 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>

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

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

现在您可能想尝试上传一个大于 x-goog-content-length-range header 上设置的上限的文件,并观察上传失败并出现 EntityTooLarge 错误的情况。