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-Length、Content-MD5、Content-Type、Host这几个头加入了签名范围,但你在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试试能不能上传——如果能成功,就坐实了是签名头没包含这个头的问题,再按上面的方法修改预签名生成代码就行。




