署名付き URL の説明

署名付き URL は、AWS S3(Simple Storage Service)のリソースに対して一時的なアクセス許可を付与するための URL です。通常、S3 バケット内のオブジェクト(ファイル)に対して生成されます。

一般的な使用例は、以下のような場合です:

  • ユーザーがプライベートな S3 オブジェクトに直接アクセスできるようにする場合(ダウンロード、表示など)。
  • S3 オブジェクトに対して一時的なアップロードポリシーを提供する場合。

署名付き URL は、AWS の認証情報(アクセスキー、シークレットアクセスキー)に基づいて生成されます。これにより、リクエストが認証され、許可された期間内に限定されます。また、HTTPS 経由でアクセスされるため、セキュアな通信が確保されます。

署名付き URL の生成には、以下の要素が含まれます:

  • リクエストの HTTP メソッド(GET、PUT、DELETE など)
  • リクエストのヘッダー(任意)
  • リクエストのパラメーター(任意)
  • リソース(S3 オブジェクト)のパス
  • アクセスキー、シークレットアクセスキー

これらの要素を使用して、署名付き URL を生成するための署名アルゴリズムが適用されます。この署名アルゴリズムは、AWS アカウントの認証情報に基づいて計算され、リクエストに付加されます。

署名付き URL は、一時的なアクセス許可を持つため、有効期限が設定されます。有効期限が切れると、その URL は無効になります。

生成された署名付き URL を使用すると、ユーザーは有効期限内に S3 オブジェクトにアクセスできます。これにより、AWS アクセスキーとシークレットアクセスキーを直接公開せずに、セキュアなファイルの共有や制御が可能になります。

署名付き URL アップロード制限

AWS S3 における署名付き URL の最大アップロードサイズは、1 回の PUT リクエストでの制限に依存します。一般的に、署名付き URL を使用してアップロードするファイルのサイズには最大サイズが 5GB となっています。そのため、署名付き URL を使用してファイルをアップロードする場合も、1 つのファイルのサイズは 5GB 以下に制限されます。

大容量のファイルを S3 にアップロードする場合、複数のパートに分割してアップロードするマルチパートアップロードという方法を使用することが一般的です。マルチパートアップロードを使用すると、大きなファイルを分割し、並行してアップロードすることができます。この場合、各パートの最小サイズは 5MB、最大サイズは 5GB となります。

したがって、署名付き URL を使用して S3 にファイルをアップロードする場合、1 回の PUT リクエストでの最大サイズは 5GB です。大容量のファイルをアップロードする際は、マルチパートアップロードを検討することをおすすめします。

署名付き URL ダウンロード制限

AWS S3 の署名付き URL を使用してファイルをダウンロードする際の制限は、特定の制約はありません。ただし、以下の点に留意する必要があります。

  • 有効期限: 署名付き URL には有効期限が設定されます。有効期限が切れた URL はアクセスできなくなります。有効期限は生成時に指定され、期限が切れると URL は無効になります。

  • アクセス許可: ダウンロード対象のオブジェクトに対して、アクセス許可が必要です。オブジェクトがプライベートである場合、ダウンロードするためには署名付き URL を持つユーザーに対して適切なアクセス許可が必要です。

  • ダウンロードサイズ: S3 のダウンロード制限は、署名付き URL に直接関連するものではありません。S3 のダウンロードには一般的に特定の制限はありませんが、ネットワークの帯域幅やダウンロード元・ダウンロード先の制約によって制限が発生する可能性があります。

署名付付き URL 発行方法

以下は、Apex で署名付き URL を生成するためのコード例です。

public class AwsS3Util {
    private static final String AMZ_ALGORITHM = 'AWS4-HMAC-SHA256';
    private static final String ACCESS_KEY = 'アクセスキー';
    private static final String SECRET_KEY = 'シークレットキー';
    private static final String BUCKET_NAME = 'バケット名';
    private static final String REGION = '地域';
    private static final String EXPIRES = '有効時間(ms)';

    //請求メソッド
    public enum RequestMethod {
        GET, PUT
    }
    /**
     * S3の署名付きURLを生成
     * @param {RequestMethod} method アップロードの場合 : PUT, ダウンロードの場合 : GET
     * @param {String} fileName ファイル名
     * @return 署名付きURL
     */
    public static String generateS3PreSignedURL(RequestMethod method, String fileName) {
        String s3key = fileName.replaceAll('\\s+', '');
        Datetime currentDateTime = Datetime.now();
        String dateOnly = currentDateTime.formatGmt('yyyyMMdd');
        String req = dateOnly + '/' + REGION + '/s3/aws4_request';
        String xAmzCredentialStr = ACCESS_KEY + '/' + req;
        String xAmzDate = currentDateTime.formatGmt('yyyyMMdd\'T\'HHmmss\'Z\'');
        String xAmzSignedHeaders = 'host';
        String host = BUCKET_NAME + '.s3.' + REGION + '.amazonaws.com';
        String canonicalRequest =
            method.name() +
            '\n' +
            '/' +
            // UriEncode(s3key, false) +
            EncodingUtil.urlEncode(s3key, 'UTF-8') +
            '\n' +
            UriEncode('X-Amz-Algorithm', true) +
            '=' +
            UriEncode(AMZ_ALGORITHM, true) +
            '&' +
            UriEncode('X-Amz-Credential', true) +
            '=' +
            UriEncode(xAmzCredentialStr, true) +
            '&' +
            UriEncode('X-Amz-Date', true) +
            '=' +
            UriEncode(xAmzDate, true) +
            '&' +
            UriEncode('X-Amz-Expires', true) +
            '=' +
            UriEncode(EXPIRES, true) +
            '&' +
            UriEncode('X-Amz-SignedHeaders', true) +
            '=' +
            UriEncode(xAmzSignedHeaders, true) +
            '\n' +
            'host:' +
            host +
            '\n\n' +
            'host\n' +
            'UNSIGNED-PAYLOAD';
        String stringToSign =
            AMZ_ALGORITHM +
            '\n' +
            xAmzDate +
            '\n' +
            req +
            '\n' +
            EncodingUtil.convertToHex(Crypto.generateDigest('SHA-256', Blob.valueOf(canonicalRequest)));
        Blob dateKey = Crypto.generateMac(
            'hmacSHA256',
            Blob.valueOf(dateOnly),
            Blob.valueOf('AWS4' + SECRET_KEY));
        Blob dateRegionKey = Crypto.generateMac(
            'hmacSHA256',
            Blob.valueOf(REGION),
            dateKey);
        Blob dateRegionServiceKey = Crypto.generateMac(
            'hmacSHA256',
            Blob.valueOf('s3'),
            dateRegionKey);
        Blob signingKey = Crypto.generateMac(
            'hmacSHA256',
            Blob.valueOf('aws4_request'),
            dateRegionServiceKey);
        Blob signature = Crypto.generateMac(
            'hmacSHA256',
            Blob.valueOf(stringToSign),
            signingKey);
        String signatureStr = EncodingUtil.convertToHex(signature);
        return 'https://' +
               host +
               '/' +
               s3key +
               '?X-Amz-Algorithm=' +
               AMZ_ALGORITHM +
               '&X-Amz-Credential=' +
               EncodingUtil.urlEncode(xAmzCredentialStr, 'UTF-8') +
               '&X-Amz-Date=' +
               xAmzDate +
               '&X-Amz-Expires=' +
               String.valueOf(EXPIRES) +
               '&X-Amz-Signature=' +
               signatureStr +
               '&X-Amz-SignedHeaders=host';
    }

    /**
     * UriEncode変換
     * @param {String} input
     * @param {Boolean} encodeSlash
     * @return 変換後のUriEncode
     */
    private static String UriEncode(String input, Boolean encodeSlash) {
        String result = '';
        for (Integer i = 0; i < input.length(); i++) {
            String ch = input.substring(i, i + 1);
            if (
                (ch >= 'A' &&
                 ch <= 'Z') ||
                (ch >= 'a' &&
                 ch <= 'z') ||
                (ch >= '0' &&
                 ch <= '9') ||
                ch == '_' ||
                ch == '-' ||
                ch == '~' ||
                ch == '.'
                ) {
                result += ch;
            } else if (ch == '/') {
                result += encodeSlash ? '%2F' : ch;
            } else {
                String hexValue = EncodingUtil.convertToHex(Blob.valueOf(ch))
                                  .toUpperCase();
                if (hexValue.length() == 2) {
                    result += '%' + hexValue;
                } else if (hexValue.length() == 4) {
                    result +=
                        '%' +
                        hexValue.substring(0, 2) +
                        '%' +
                        hexValue.substring(2);
                }
            }
        }
        return result;
    }
}

参考