我正在开发一个用户应该能够将视频文件上传到AWS的网站。为了避免不必要的流量,我希望用户直接上传到AWS(而不是通过API服务器)。为了不在JavaScript中暴露我的秘密密钥我试图在API中生成签名。但是,它告诉我,当我尝试上传时,签名不匹配。
对于签名生成,我一直在使用http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-UsingHTTPPOST.html
在后端我正在运行C#。
我使用
生成签名string policy = $@"{{""expiration"":""{expiration}"",""conditions"":[{{""bucket"":""dennisjakobsentestbucket""}},[""starts-with"",""$key"",""""],{{""acl"":""private""}},[""starts-with"",""$Content-Type"",""""],{{""x-amz-algorithm"":""AWS4-HMAC-SHA256""}}]}}";
生成以下内容
{"expiration":"2016-11-27T13:59:32Z","conditions":[{"bucket":"dennisjakobsentestbucket"},["starts-with","$key",""],{"acl":"private"},["starts-with","$Content-Type",""],{"x-amz-algorithm":"AWS4-HMAC-SHA256"}]}
基于http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-HTTPPOSTConstructPolicy.html(我对base64编码策略)。我试图让它变得非常简单,只是作为一个起点。
为了生成签名,我使用AWS网站上的代码。
static byte[] HmacSHA256(String data, byte[] key)
{
String algorithm = "HmacSHA256";
KeyedHashAlgorithm kha = KeyedHashAlgorithm.Create(algorithm);
kha.Key = key;
return kha.ComputeHash(Encoding.UTF8.GetBytes(data));
}
static byte[] GetSignatureKey(String key, String dateStamp, String regionName, String serviceName)
{
byte[] kSecret = Encoding.UTF8.GetBytes(("AWS4" + key).ToCharArray());
byte[] kDate = HmacSHA256(dateStamp, kSecret);
byte[] kRegion = HmacSHA256(regionName, kDate);
byte[] kService = HmacSHA256(serviceName, kRegion);
byte[] kSigning = HmacSHA256("aws4_request", kService);
return kSigning;
}
我使用的是这样的:
byte[] signingKey = GetSignatureKey(appSettings["aws:SecretKey"], dateString, appSettings["aws:Region"], "s3");
byte[] signature = HmacSHA256(encodedPolicy, signingKey);
其中dateString的格式为yyyymmdd
我使用
从JavaScript发布信息let xmlHttpRequest = new XMLHttpRequest();
let formData = new FormData();
formData.append("key", "<path-to-upload-location>");
formData.append("acl", signature.acl); // private
formData.append("Content-Type", "$Content-Type");
formData.append("AWSAccessKeyId", signature.accessKey);
formData.append("policy", signature.policy); //base64 of policy
formData.append("x-amz-credential", signature.credentials); // <accesskey>/20161126/eu-west-1/s3/aws4_request
formData.append("x-amz-date", signature.date);
formData.append("x-amz-algorithm", "AWS4-HMAC-SHA256");
formData.append("Signature", signature.signature);
formData.append("file", file);
xmlHttpRequest.open("post", "http://<bucketname>.s3-eu-west-1.amazonaws.com/");
xmlHttpRequest.send(formData);
我一直按照AWS的规定在任何地方使用UTF8。在他们的例子中,签名是十六进制格式,我也尝试过。 无论我尝试什么,我都会收到错误403
The request signature we calculated does not match the signature you provided. Check your key and signing method.
我在AWS上的政策有“s3:Get *”,“s3:Put *”
我是否遗漏了某些东西,或者它与我的期望完全不同?
编辑:以下答案是其中一个步骤。另一个是AWS区分大写和小写的十六进制字符串。在AWS眼中,0xFF!= 0xff。他们想要全部小写的签名。
答案 0 :(得分:4)
您正在使用签名版本4生成签名,但您正在构建表单,就好像您使用的是签名版本2 ......好吧,等等。
formData.append("AWSAccessKeyId", signature.accessKey);
那个V2。它根本不应该在这里。
formData.append("x-amz-credential", signature.credentials); // <accesskey>/20161126/eu-west-1/s3/aws4_request
这是V4。请注意此处及以上的AWS Access Key ID的冗余提交。这个可能是正确的,尽管这些示例的大小写类似于X-Amz-Credential
。
formData.append("x-amz-algorithm", "AWS4-HMAC-SHA256");
这也是正确的,除了它可能需要X-Amz-Algorithm
。 (这个例子似乎暗示大写被忽略了。)
formData.append("Signature", signature.signature);
这个不正确。这应该是X-Amz-Signature
。 V4签名是十六进制的,所以这就是你应该拥有的。 V2签名是base64。
有一个完整的V4示例here,它甚至为您提供了一个示例aws密钥和秘密,日期,区域,存储桶名称等,您可以将它们与您的代码一起用来验证您确实得到了相同的回应。该表单实际上不会工作,但重要的问题是您的代码是否可以生成相同的表单,策略和签名。
对于任何给定的请求,只有一个正确的签名;但是,对于任何给定的策略,可能存在多个有效的JSON编码(由于JSON具有空白的灵活性) - 但对于任何给定的JSON编码,只有一个可能的有效base64编码的策略。这意味着,如果代码生成与示例中显示的完全相同的表单和签名,则使用示例数据的代码被认证为正常工作 - 这意味着如果代码生成相同的表单和策略,则证明代码无效一个不同的签名 - 但还有第三种可能性:如果你的代码生成了不同的base64编码,那么测试实际上证明你的代码没有任何结论,因为这必然会改变签名不匹配,但可能仍然是有效的政策。
请注意,Signature V2仅支持较旧的S3区域,而所有 S3区域支持Signature V4,因此,即使您可以通过使整个签名过程使用V2来交替修复此问题,这不会被推荐。
另请注意,The request signature we calculated does not match the signature you provided. Check your key and signing method
并未告诉您有关存储桶策略或任何用户策略是允许还是拒绝该请求的任何信息。此错误不是权限错误。它将在权限检查之前抛出,仅基于签名的有效性,而不是AWS Access Key id是否被授权执行所请求的操作,这是仅在签名验证后才进行测试的。
答案 1 :(得分:0)
我建议您创建一个只有POST
权限的身份验证令牌,并发送如下的http请求:
require 'rest-client'
class S3Uploader
def initialize
@options = {
aws_access_key_id: "ACCESS_KEY",
aws_secret_access_key: "ACCESS_SECRET",
bucket: "BUCKET",
acl: "private",
expiration: 3.hours.from_now.utc,
max_file_size: 524288000
}
end
def fields
{
:key => key,
:acl => @options[:acl],
:policy => policy,
:signature => signature,
"AWSAccessKeyId" => @options[:aws_access_key_id],
:success_action_status => "201"
}
end
def key
@key ||= "temp/${filename}"
end
def url
"http://#{@options[:bucket]}.s3.amazonaws.com/"
end
def policy
Base64.encode64(policy_data.to_json).delete("\n")
end
def policy_data
{
expiration: @options[:expiration],
conditions: [
["starts-with", "$key", ""],
["content-length-range", 0, @options[:max_file_size]],
{ bucket: @options[:bucket] },
{ acl: @options[:acl] },
{ success_action_status: "201" }
]
}
end
def signature
Base64.encode64(
OpenSSL::HMAC.digest(
OpenSSL::Digest.new("sha1"),
@options[:aws_secret_access_key], policy
)
).delete("\n")
end
end
uploader = S3Uploader.new
puts uploader.fields
puts uploader.url
begin
RestClient.post(uploader.url, uploader.fields.merge(file: File.new('51bb26652134e98eae931fbaa10dc3a1.jpeg'), :multipart => true))
rescue RestClient::ExceptionWithResponse => e
puts e.response
end