使用“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 表单。例如,我想使用 fetch
或 xhr
来上传文件,而不是使用标准的 HTML 格式。
奇怪的是,当我使用 xhr
或 fetch
发出 POST
请求时,我遇到了 CORS
错误。我确实在我的存储桶中正确设置了 CORS
,因为如果我使用标准 SignedUrl 生成 URL 并使用 PUT
方法上传文件,上传工作正常xhr
或fetch
,证明我的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 和上传时,此访问令牌不必相同;但是,为了简单起见,我决定保持不变。
脚本本身不是您想要在生产中使用的东西:它仅用于说明目的。
(你需要 flask
和 google-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
错误的情况。
我正在努力在 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
.
下面是 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 表单。例如,我想使用 fetch
或 xhr
来上传文件,而不是使用标准的 HTML 格式。
奇怪的是,当我使用 xhr
或 fetch
发出 POST
请求时,我遇到了 CORS
错误。我确实在我的存储桶中正确设置了 CORS
,因为如果我使用标准 SignedUrl 生成 URL 并使用 PUT
方法上传文件,上传工作正常xhr
或fetch
,证明我的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 和上传时,此访问令牌不必相同;但是,为了简单起见,我决定保持不变。
脚本本身不是您想要在生产中使用的东西:它仅用于说明目的。
(你需要 flask
和 google-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
错误的情况。