使用 S3 预签名 URL 执行 PUT 文件时访问被拒绝



>问题:如何成功对存储桶具有以下权限的预签名 URL 执行 PUT 文件请求:

  • 阻止公共访问 = 关闭所有 4 个选项
  • 访问控制列表 = 公共读取
  • 存储桶策略 = 空
  • CORS 配置 = 放置

这是我所做的事情的一个具体例子...

我对我的presign端点执行了开机自检...

> POST /dev/presign HTTP/2
> Host: 9xugkdzvch.execute-api.ap-southeast-1.amazonaws.com
> user-agent: insomnia/2020.2.2
> content-type: application/json
> accept: */*
> content-length: 51
* Connection state changed (MAX_CONCURRENT_STREAMS == 128)!
| {
|   "name": "profile_1.png",
|   "type": "image/png"
| }

它返回了

{
"statusCode": 200,
"headers": {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Credentials": true
},
"body": {
"error": false,
"data": "https://profilephoto-upload-dev.s3.ap-southeast-1.amazonaws.com/profiles/profile_1.png?AWSAccessKeyId=ASIA4IWFXQ2GLT2FGUUA&Content-Type=image%2Fpng&Expires=1593475190&Signature=JO4pNtbXmb56OYiClZhil4hse0c%3D&x-amz-meta-username=foobar&x-amz-security-token=IQoJb3JpZ2luX2VjEIj%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEaDmFwLXNvdXRoZWFzdC0xIkcwRQIhAP5kW%2Fj6Y5BiLgAhec3w6bv17CL8QuEabOy7hhhtK81JAiA7pS6IkGE3WtnIcD78%2BOytl1luSdLBkI9fg%2F2GzxpApirUBQghEAAaDDg0MzMwMjAxMjU1NiIMdbMwBimx3k0miN5lKrEFfUAFtMJep90tfYQqwW09Od1HTSaUKkVUqP0eOaQg89vEnplJ%2BCQwKopOF8Ed8AiFAFQ9qJcbtmg39wEqIBa5Z0oOdfDEphRXDmVWX%2BCQvdCCoPbbZeaK2tBg4PN2f%2FL8OBa4AebUMCwnVJVneA7Pf5p7hVKKeP%2BePDfMWYOpChS9smp0UjfoVSKYDDA1GDS4eE%2FTN02KjnLdVOWPqoAOm%2FurDzbs%2FYe8B2xnC6R0%2FpbMrj2j59bHB9d8f2GcKXOfiHpeKff%2BLNYYsSmEAApsiW2N05ZMTAWBZQTUyWy53jFhYrZU11uo8RlzKoJOiI6pNoufk4CtbdtVYLxdvQZuxQm7TRJNMLDAQQcL6qR07N0sxHMeo7IIe5QNLT%2B%2FUwqG4X3FJxhYHnSvP7CRT%2FtlS%2Fxutl3rNpthpxDplRBDMtc5rSig6H%2Bb9O1vFZKAG%2B%2F9J%2BE4sCWfckh7kANCYA%2BY3bDHvkTj%2BKKWckzpQB4yLyPD4L%2FbiUDm9T520C9ROwXXhMAun%2BI2hxidauOTkyx%2Bzny5M8zTMN8gvVcxlIHgQHCYy6776WdI2qk%2FmKxYM814s74zt4o3AhtIDTJfEJV0NriqcwxOzZqsnGXPBP9fxri5t57M%2B%2BeONpjcFJpRhj%2B1Os1omCe3CL1R84RsrVPfc8FzGu12yM15FeI0Qb5duqgH3kWdc6wEflRsGPWPfVqHdWfj8euhGQXwsE2m4tcglZdYCpxWfMnbF%2Foz8TOJv9b3ODFrYNTtaeG9arUbLZ8CPqWH8Nyag4MNRgZAZRJNHbc4eBKqi9YHp1p%2FCillnFgmoPMwccyziv%2BGAPkjtS9KCLb0XWEwJwJLgLWNllVka9g%2BlODt6HK2mPb0zPLpsQuDI5qQwnBlSWPbhqwpoWyatvmV7aPaeF3dJm5yBVKAMyAwqPbp9wU6zQLBnlCtwUNRh0HMSUwfPWoFlpweESBTUzjEQFRui7g874ksQtlF51dpUsdXF7JI1kV%2FWr8A%2BxhxlMEjxyhoTj70hUVO7V55I7Ppx3Iyz5AlBAoh2nu4NZccHY4JYjU3de%2FYWVa0jfBSxP0WU%2F5GHbwbENjCpgEMhpDVcFf409UN8ecQi53nQh8ytjsDJMTwYalBM8ys1BfXdvLMkL2dtBFTfHtVjIGtrOkTxfH3CtbT24CzAtWrXxBCY%2BDd97o3oi6ztloybTJtxhI8Zdb6vojsFm95QVmVAVO5PaeZ%2BgHOoqHFHJ%2BXG1kbV8OexmT1S1bAXuu%2Fa07HDjJzqBYovvzWa%2BaG9pG8SKiw0N%2Bb5mdxMDdcGEqVIMwUK7mf7sY06Z%2Fbl1SQvErfpEsgA67C1Qtrgb0fKetVMlQR4Zq%2F53H%2FrFrys2OAnPXUfVLiqWY%3D",
"message": null
}
}

然后,我使用data中的 url 执行 PUT 请求,它因访问被拒绝而失败

PUT /profiles/profile_1.png?AWSAccessKeyId=ASIA4IWFXQ2GPP2VWPFE&Content-Type=image%2Fpng&Expires=1593513254&Signature=vmXn5%2Fk%2Bk3hh98B8OeNlU%2F8XetY%3D&x-amz-meta-username=foobar&x-amz-security-token=IQoJb3JpZ2luX2VjEJL%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEaDmFwLXNvdXRoZWFzdC0xIkcwRQIhAK6oeGhiy%2BuWwonWYViWa5pzJxZlSwB5MWjuLcZas6NvAiBmArs%2B8noZacruG0dlWrUyIUm30WFLKUYACVW2xMCFmyrUBQgrEAAaDDg0MzMwMjAxMjU1NiIMgQFekIbk3lBaS%2BmyKrEFBrdMyiCwa1iKVcgFxO%2BW1RTCu5M9aKotfJtBCxtW8gGkUY8VsMsVLPYUDWrJU07z7QjdRIK5xR3%2BdWtkK%2FvW6ZULjqrtVwiDbwnncug7FvvzWBCzc2oOVUFenSBNXhr%2FGnlCbqk6mFyqkOtCeFffOYtls19tU56GsSa1aaTwdwQcRld02G1TJCgj6egjgxSoHqDFbntlHU84vWcW5wi1O4YX4NwAEhWmiLC%2BU6RsfbR0NA%2BzJHlRGShJhEAY1pxOmm3533DAzSjlhj3yvTJN8whT4jFUEvn0jWFCLb3u9rC99%2FwjgtPH0jlU%2BqESTP0VlATuCU%2F5by1iTyX4TLRFyF0vCqx5%2BEePH0SdZeVDEqi%2BIgXIkOGG8fzJQkxLu536uCioSojmRYqiG16Osu9mAiP08LEGsIquzsIoIZmWve3P56gwk0pM28TWYz%2BhZtynJCXAIDOtm1gZ7j14ho1SrpIazM6fJ95NgmuqJfud%2BIGbXdytPZQj1jRsj2fH3vlw66LTXN6AKBVPy6yTf45fMBfP2mywSZwkQtjEQ6hVN76nKD6Z1jZ%2F2Pbk%2FpFUwtcfYbGeKDIEpnV6h2%2BvsiaaNmos9eIYLkt6R9glYvInnIYvp6TOKJoicZgv%2BChr1lY8L728wCXnKLXxHTnv1YTnE5z%2FmtMbG4ZxYkj0ioaJt6kS2%2B3Lh%2BXdrbnwK7gbn3rFGT9EaTyeLSOymIB%2BHzAAfxB902g0c4jV6Em6B9xUxUgfXjHY1I5Gla1t36lEgAe7rlfngUlhoomJjDpMCw4IbzFnjvjXdAIzj7cJjmY%2FPxhdy4gp3pRvcgTkJ%2BV9DV8x2vMSgZS1qX5v%2FMQ%2BSQLStK5aY%2FjKqYkBt03102M1NovHpYiaV4XdE6%2F53unpdEyKtsKjUwYGLebd21kUhHkfoX4woqPs9wU6zQJwdh2wOJcRW9Wp6GmVLOyxfI23RxkAgygOjN5R1AUr%2F%2BehPrSr9iQsYjI%2FWzJqi7Ohzr3kZqjXuL4Yi1UKyRF6vyKckSDMQb371HypXkopMuoPxmNpZqI42IwO8%2BM0ktrJfnJ7Kp4i3EDw9eujy6Cuau5fsD8v6s0c3m5hNu1gmESfdmZr1%2BR%2FCVbifqgeLLwyGak96UyO%2BapQ4kOU6tu5uNskL1ewx%2F2K0esKq6qEz1IXA%2Fx5n5dioiom3GI9QVRfrtF7Z%2BhqBLipVfHMR2Pb4N3egoqBj80ux9u0UzYZ8rLst7Ns0tlJzmCHGxh0py9M7kUPAQmVMdQE4rAlP2FjMQiMWpOCiqo4uwADjKrlwinhSc98LPGYTxJ%2F8QveSfixbeZmgdC1gJkOAMUEdgnoh%2BeRB1dHR%2B3SavrFp1EIudMDIUxd4AxZSDYN%2F%2FY%3D HTTP/1.1
> Host: profilephoto-upload-dev.s3.ap-southeast-1.amazonaws.com
> User-Agent: insomnia/2020.2.2
> Content-Type: image/png
> Accept: */*
> Content-Length: 578256
<?xml version="1.0" encoding="UTF-8"?>
<Error>
<Code>AccessDenied</Code>
<Message>Access Denied</Message>
<RequestId>A6037105527AC3DE</RequestId>
<HostId>KqRDTOrRWDTjnQesY9ZIbBb95/lmqLmro7YLETogaA6qu4n0Jq8znwtExBxdpjDhgXr2Lg/MyqU=</HostId>
</Error>

对于给定的: 这是我presign代码...

import S3 = require("aws-sdk/clients/s3")
import * as mime from "mime"
const createPresignedPost = async (key: string, contentType: string): Promise<string> => {
const s3 = new S3()
const params: S3.Types.PutObjectRequest = {
Bucket: "profilephoto-upload-dev",
Key: `profiles/${key}`,
ContentType: contentType,
Metadata: {username: "foobar"}
}
return s3.getSignedUrlPromise("putObject", params)
.then(some => {
console.log(`returned: ${some}`)
return some
})
}
app.post('/presign', async (req, res) => {
const headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Credentials": true
}
try {
const key = `${req.body['name']}`
const mimeType = mime.getType(req.body['name'])!
const presignedPostData = await createPresignedPost(key, mimeType)
res.status(200).send({
statusCode: 200,
headers,
body: {
error: false,
data: presignedPostData,
message: null
}
})
} catch (error) {
console.trace(JSON.stringify(error))
res.status(500).send({
statusCode: 500,
headers,
body: {
error: true,
data: null,
message: error.message
}
})
}
})

这条快速路由属于这里创建的 lambda...

presignedpostdata:
handler: ${self:custom.${self:custom.stage}.handler}
role: arn:aws:iam::843302012556:role/service-role/auto_presignedpost_dev-role-w7qof39g
events:
- http:
path: /presign
method: post
cors: true

ARN 提到的角色具有以下权限...

{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "*"
},
{
"Sid": "VisualEditor1",
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:PutObjectAcl"
],
"Resource": "arn:aws:s3:::profilephoto-upload-dev/*"
},
{
"Sid": "VisualEditor2",
"Effect": "Allow",
"Action": "logs:CreateLogGroup",
"Resource": "*"
}
]
}

最后,S3 存储桶是这样创建的...

ProfilePhotoBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: profilephoto-upload-${self:custom.stage}
AccessControl: PublicRead
CorsConfiguration:
CorsRules:
-   AllowedHeaders: ['*']
AllowedMethods: ['PUT']
AllowedOrigins: ['*']

就我而言,问题是因为浏览器正在发送其他标头,例如"内容类型">,但在生成预签名 URL 时未配置此标头。

我选择的解决方案是在生成预签名 URL 时将标头设置为application/octet-stream,并在浏览器上强制application/octet-streamContent-Type标头

s3_url = s3_client.generate_presigned_url(
ClientMethod="put_object",
Params={
"Bucket": "my-bucket",
"Key": f"some-key",
"ContentType": "application/octet-stream"
},
ExpiresIn=24 * 60 * 60
)

显然,这个解决方案目前对我来说是最好的。

我相信问题是你应该使用s3.createPresignedPost而不是s3.getSignedUrlPromise,然后你必须使用 POST 方法而不是 发送请求时的 PUT。

我认为s3.getSignedUrlPromise要求您在签署请求时提供对象的Body,而s3.createPresignedPost允许发送任何正文。使用s3.createPresignedPost您还可以添加Conditions来执行某些操作,例如限制对象的最大大小。

看:

  • https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#createPresignedPost-property
  • https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-UsingHTTPPOST.html
  • https://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectPOST.html

最新更新