SalesforceのApexを使用して、REST APIでAmazon S3に署名バージョン4を使用してアクセスする方法の日本語記事があまりなかったので、メモとして残しておく。
前提条件
- AWSアカウントを所有しており、IAMユーザーを作成し、アクセスキーとシークレットキーを取得していること
- S3バケットのアクセス権やSalesforceの設定が適切にされていること
実装
以下の公式ドキュメントの手順にしたがって実装。詳細は以下を参照。
署名付き AWS API リクエストを作成する
secret, secretKey, region
はクラス変数等で定義しておく
手順1: 正規リクエストを作成する
private String generateCanonicalRequest(String method, String filename, String query, Map<String, String> headers, String hashedPayload) {
// HTTPMethod
String canonicalRequest = method + '\n';
// CanonicalURI
canonicalRequest += '/' + filename + '\n';
// CanonicalQueryString
canonicalRequest += query + '\n';
List<String> headersList = new List<String>(headers.keySet());
headersList.sort();
// CanonicalHeaders
for (String header : headersList) {
String headerContent = header.toLowerCase() + ':' + headers.get(header).trim();
canonicalRequest += headerContent + '\n';
}
canonicalRequest += '\n';
// SignedHeaders
for (String header : headersList) {
canonicalRequest += header.toLowerCase() + ';';
}
canonicalRequest = canonicalRequest.removeEnd(';');
canonicalRequest += '\n';
// HashedPayload
canonicalRequest += hashedPayload;
return canonicalRequest;
}
手順2: 正規リクエストのハッシュを作成する
以下の手順3で実行
手順3: 署名文字列を作成する
private String generateStringToSign(String algorithm, String requestDateTime, String credentialScope, String canonicalRequest) {
// Algorithm
String stringToSign = algorithm + '\n';
// RequestDateTime
stringToSign += requestDateTime + '\n';
// CredentialScope
stringToSign += credentialScope + '\n';
// HashedCanonicalRequest
stringToSign += EncodingUtil.convertToHex(Crypto.generateDigest('SHA-256', Blob.valueOf(canonicalRequest)));
return stringToSign;
}
手順4: 署名を計算する
private Blob generateSigningKey(String date, String service) {
Blob dateKey = Crypto.generateMac('HMACSHA256', Blob.valueOf(date), Blob.valueOf('AWS4' + this.secret));
Blob dateRegionKey = Crypto.generateMac('HMACSHA256', Blob.valueOf(this.region), dateKey);
Blob dateRegionServiceKey = Crypto.generateMac('HMACSHA256', Blob.valueOf(service), dateRegionKey);
Blob signingKey = Crypto.generateMac('HMACSHA256', Blob.valueOf('aws4_request'), dateRegionServiceKey);
return signingKey;
}
手順4.5: 署名の作成
手順5で使用する
private Blob generateSignature(String stringToSign, Blob signingKey) {
return Crypto.generateMac('HMACSHA256', Blob.valueOf(stringToSign), signingKey);
}
手順5: リクエストヘッダーに署名を追加する
private String generateAuthorizationHeader(String credentialScope, String algorithm, Map<String, String> headers, Blob signature, String requestDateTime) {
String signedHeaders = '';
List<String> headersList = new List<String>(headers.keySet());
headersList.sort();
for (String header : headersList) {
signedHeaders += header.toLowerCase() + ';';
}
signedHeaders = signedHeaders.removeEnd(';');
String authorizationHeader = algorithm + ' ';
authorizationHeader += 'Credential=' + this.secretKey + '/' + credentialScope + ', ';
authorizationHeader += 'SignedHeaders=' + signedHeaders + ', ';
authorizationHeader += 'Signature=' + EncodingUtil.convertToHex(signature).toLowerCase();
return authorizationHeader;
}
手順6: HTTPリクエストを実行する
GETメソッド
public HTTPResponse getObject(String filename) {
String method = 'GET';
String service = 's3';
String algorithm = 'AWS4-HMAC-SHA256';
Datetime now = Datetime.now();
String date = now.formatGMT('yyyyMMdd');
String requestDateTime = now.formatGMT('yyyyMMdd\'T\'HHmmss\'Z\'');
String credentialScope = date + '/' + this.region + '/' + service + '/aws4_request';
String bucketName = '';
String hostName = '';
String endpointUrl = 'https://' + bucketName + '.' + hostName + '/' + filename;
// リクエストペイロードのハッシュを生成
String hashedPayload = EncodingUtil.convertToHex(Crypto.generateDigest('SHA-256', Blob.valueOf('')));
// ヘッダーを定義
Map<String, String> headers = new Map<String, String>{
'host' => bucketName + '.' + hostName,
'x-amz-date' => requestDateTime,
'x-amz-content-sha256' => hashedPayload
};
// 正規リクエストを作成
String canonicalRequest = generateCanonicalRequest(method, filename, '', headers, hashedPayload);
// 署名文字列を作成
String stringToSign = generateStringToSign(algorithm, requestDateTime, credentialScope, canonicalRequest);
// 署名を計算
Blob signingKey = generateSigningKey(date, service);
// 署名の作成
Blob signature = generateSignature(stringToSign, signingKey);
// リクエストヘッダーを作成
String authorizationHeader = generateAuthorizationHeader(credentialScope, algorithm, headers, signature, requestDateTime);
HttpRequest req = new HttpRequest();
req.setMethod(method);
req.setEndpoint(endpointUrl);
for (String header : headers.keySet()) {
req.setHeader(header, headers.get(header));
}
req.setHeader('Authorization', authorizationHeader);
Http http = new Http();
HTTPResponse res = http.send(req);
return res;
}
PUTメソッド
基本的にはGETメソッドと同様のため、変更箇所以外は省略
テキストファイルをアップロードする場合
- public HTTPResponse getObject(String filename) {
- String method = 'GET';
+ public void putTextObject(String filename, String body) {
+ String method = 'PUT';
- String hashedPayload = EncodingUtil.convertToHex(Crypto.generateDigest('SHA-256', Blob.valueOf('')));
+ String hashedPayload = EncodingUtil.convertToHex(Crypto.generateDigest('SHA-256', Blob.valueOf(body)));
+ req.setHeader('Content-Length', String.valueOf(body.length()));
+ req.setHeader('Content-Encoding', 'UTF-8');
+ req.setHeader('Content-type', ContentType('csv'));
+ req.setHeader('Connection', 'keep-alive');
+ req.setHeader('ACL', 'public-read');
+ req.setBodyAsBlob(Blob.valueOf(body));
- return res;
イメージファイルをアップロードする場合
- public HTTPResponse getObject(String filename) {
- String method = 'GET';
+ public void putImageObject(String filename, Blob body) {
+ String method = 'PUT';
- String hashedPayload = EncodingUtil.convertToHex(Crypto.generateDigest('SHA-256', Blob.valueOf('')));
+ String hashedPayload = EncodingUtil.convertToHex(Crypto.generateDigest('SHA-256', body));
+ req.setHeader('Content-Length', String.valueOf(EncodingUtil.base64Encode(body).length()));
+ req.setHeader('Content-type', ContentType('jpeg'));
+ req.setHeader('Connection', 'keep-alive');
+ req.setHeader('ACL', 'public-read');
+ req.setBodyAsBlob(body);
- return res;
オブジェクトコピーの場合
- public HTTPResponse getObject(String filename) {
- String method = 'GET';
+ public HTTPResponse copyObject(String fromFilename, String filename) {
+ String method = 'PUT';
- Map<String, String> headers = new Map<String, String>{
- 'host' => bucketName + '.' + hostName,
- 'x-amz-date' => requestDateTime,
- 'x-amz-content-sha256' => hashedPayload
- };
+ Map<String, String> headers = new Map<String, String>{
+ 'host' => bucketName + '.' + hostName,
+ 'x-amz-date' => requestDateTime,
+ 'x-amz-content-sha256' => hashedPayload
+ 'x-amz-copy-source' => '/' + bucketName + '/' + fromFilename
+ };