从 Apex 调用 S3 时 AWS 访问密钥无效

Invalid AWS Access Key when calling S3 from Apex

我正在尝试使用从 AssumeRole returned 的凭据从 Apex 访问 s3。但是,我收到以下错误:

<Message>The AWS Access Key Id you provided does not exist in our records.</Message>
<AWSAccessKeyId>ASIA********</AWSAccessKeyId>

我能够使用从 AssumeRole returned 的凭据从 CLI 成功调用此 s3 存储桶上的 GetObject,因此我可以合理地确定我的存储桶权限已设置好。我在 Apex 中有以下代码:

        Http http = new http();
        Profile p = [SELECT Id FROM Profile WHERE Profile.Name = 'S3 Test User' LIMIT 1];
        S3_Settings__c s3 = S3_Settings__c.getInstance(p.Id);
        String exp = String.valueOf(Cache.Session.get('expiration'));
        String sessionToken = String.valueOf(Cache.Session.get('token'));

        if(exp == null || exp == '' || (DateTime) JSON.deserialize('"' + exp + '"', DateTime.class) < System.now()) {
            requestSessionToken();
        }

        sessionToken = String.valueOf(Cache.Session.get('token'));
        DateTime expires = (DateTime) JSON.deserialize('"' + String.valueOf(Cache.Session.get('expiration')) + '"', DateTime.class);
        String accessKeyId =  String.valueOf(Cache.Session.get('accessKeyId'));
        String accessSecret = String.valueOf(Cache.Session.get('secret'));

        String bucketname = s3.Recording_Bucket__c;
        String host = 's3.amazonaws.com';  
        String formattedDateString = Datetime.now().formatGMT('EEE, dd MMM yyyy HH:mm:ss z');
        String method = 'GET';
        String filePath = 'https://' + bucketname + '.' + host + '/' + filename; 
        HttpRequest req = new HttpRequest();
        req.setMethod(method);
        req.setEndpoint(filePath);
        req.setHeader('Host', bucketname + '.' + host);
        req.setHeader('Connection', 'keep-alive');
        String stringToSign = 'GET\n\n' + 'x-amz-security-token=' + sessionToken + '&expiration=' + expires + '\n' + formattedDateString + '\n/' + '/' + bucketname + '/' + filename;
        System.debug('SIGN ' + stringToSign);
        String encodedStringToSign = EncodingUtil.urlEncode(stringToSign, 'UTF-8');
        Blob mac = Crypto.generateMac('HMACSHA1', blob.valueof(stringToSign),blob.valueof(accessSecret));
        String signedKey  = EncodingUtil.base64Encode(mac);

        String authHeader = 'AWS' + ' ' + accessKeyId + ':' + signedKey;
        req.setHeader('Date', formattedDateString);
        //req.setHeader('x-amz-security-token', sessionToken); //AWS returns 'invalid signature' if this is set

        req.setHeader('Authorization',authHeader);

        HttpResponse resp = http.send(req);

AWS 似乎正在读取 AccessKeyId/Secret,而不是 session 令牌。我也试过将 x-amz-security-token 设置为 header,但这会引发 403 错误——签名不匹配。我是否在我的 header 或签名中遗漏了一些可以使此请求成功 return 的东西?

原来我把 x-amz-security-token header 放在了错误的位置。它需要出现在规范的 AMZ Headers 部分中,紧跟在格式化日期之后,用逗号分隔名称和值:

String stringToSign = 'GET\n\n\n' + formattedDateString + '\n' + 'x-amz-security-token:' + sessionToken + '\n' + '/' + bucketname + '/' + filename;

此外,需要取消注释以下行:

req.setHeader('x-amz-security-token', sessionToken);

最后一点,请确保 header 和规范化的 AMZ Header 都没有大写。