You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Ruby on Rails中使用aws-sdk-s3预签名URL上传图片报Access Denied,但aws s3 cp可正常上传的问题

Ruby on Rails中使用aws-sdk-s3预签名URL上传图片报Access Denied,但aws s3 cp可正常上传的问题

我之前做Rails API的S3预签名上传时,踩过几乎一模一样的坑!咱们一步步排查,大概率是请求头和预签名时指定的签名头不匹配导致的——这也是S3预签名URL最容易踩的坑之一。

最可能的原因:请求头超出预签名的签名范围

你看预签名URL里的X-Amz-SignedHeaders参数:

content-length%3Bcontent-md5%3Bcontent-type%3Bhost

这说明生成预签名URL时,只把Content-LengthContent-MD5Content-TypeHost这几个头加入了签名范围,但你在curl请求里额外加了Content-Disposition头:

-H "Content-Disposition: inline; filename=\"test.png\"; filename*=UTF-8''test.png"

S3的预签名机制有个严格要求:所有在请求中发送的非默认头,必须在生成预签名时就被包含到签名头列表里,否则S3会判定请求的签名无效,直接返回Access Denied。

aws s3 cp之所以能成功,是因为它默认不会额外添加Content-Disposition这类头,或者它在签名时自动处理了自己用到的头,和你手动curl的请求头结构完全不一样。

解决方法:生成预签名URL时包含所有要用到的请求头

在Rails里用aws-sdk-s3生成预签名URL时,要把你计划在上传请求中使用的所有头都传入,并且确保这些头被包含到签名头里。比如:

s3 = Aws::S3::Resource.new
bucket = s3.bucket('my-bucket')

# 定义所有要在上传时使用的头
upload_headers = {
  'content-type' => 'image/png',
  'content-md5' => '+ZXlSNL39jkbzU5158hezw==',
  'content-length' => '42640',
  'content-disposition' => 'inline; filename="test.png"; filename*=UTF-8\'\'test.png'
}

# 生成预签名URL,指定headers和要签名的头
presigned_url = bucket.object('r5ia6v5i3k8gypfath4gm2xfc8it').presigned_url(
  :put,
  headers: upload_headers,
  expires_in: 300,
  signed_headers: upload_headers.keys.map(&:downcase) # 注意转成小写,S3签名时头名是小写的
)

这样生成的预签名URL的X-Amz-SignedHeaders就会包含content-disposition,再用你原来的curl命令上传应该就能成功了。


其他需要排查的补充点(如果上面的方法没解决)

1. 验证Content-MD5的正确性

虽然你说文件信息正确,可以用命令行再核对一遍:

openssl dgst -md5 -binary test.png | base64

把输出和你curl里的Content-MD5值对比,确保完全一致(包括base64编码格式,不能有多余空格)。

2. 检查服务器时间同步

S3的签名对时间非常敏感,如果你的Rails服务器系统时间和UTC时间偏差超过15分钟,预签名URL会直接失效。可以用date -u查看服务器UTC时间,和在线UTC时间对比,偏差大的话同步一下服务器时间即可。

3. 简化请求测试

先去掉curl里的Content-Disposition头,用原来的预签名URL试试能不能上传——如果能成功,就坐实了是签名头没包含这个头的问题,再按上面的方法修改预签名生成代码就行。

火山引擎 最新活动