この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
よく訓練されたアップル信者、都元です。CloudFrontでコンテンツ保護を行う場合、サーバサイドで署名付きURLを発行する必要があります。この仕組みについて詳しくは弊社佐々木のエントリーCloudFront+S3で署名付きURLでプライベートコンテンツを配信するを御覧ください。
さて、上記のエントリでは主にPerlのスクリプトを用いて、作業用のローカルマシン上で署名付きURLを生成する手順をご紹介しています。また、参照先としてご紹介したドキュメントでは、下記のように各プログラミング言語上で署名付きURLを生成する方法について説明があります。
このうちJavaの解説ではJetS3t *1というAPIラッパーライブラリを用いていますが、現在の標準APIラッパーはAWS SDK for Javaです。従って、署名を生成するためだけに、わざわざJetS3tを採用する理由は特に無い *2と思います。
また、このドキュメントのコードはBouncy Castleという、これも古くからあるJavaの暗号実装に依存しています。暗号の実装は、J2SE 5.0(要するにJava5)以降であれば Java SE Security として標準提供されていますので、本稿ではそちらを使いたいと思います。
サンプルコード
というわけで、もっとシンプルに署名付きURLを生成するコードをご紹介します。このコードは「引数として下記のような情報を受け取り、標準出力に署名付きURLを出力する」という、シンプルなコマンドラインツールとして記述しました。
一応Java7で動作確認しています。出来れば外部のライブラリに依存せずに実現したかったのですが、1箇所だけGuavaを使わせてもらいました。Java8であれば、Guavaを使わずに標準APIだけで書けます。
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.SecureRandom;
import java.security.Signature;
import java.security.spec.PKCS8EncodedKeySpec;
import com.google.common.io.BaseEncoding;
public class SignedURLGenerator {
private static final long DEFAULT_DURATION_SEC = 60 * 5; // 5 minutes
private static final String POLICY_FORMAT =
"{\"Statement\":[{\"Resource\":\"%s\",\"Condition\":{\"DateLessThan\":{\"AWS:EpochTime\":%d}}}]}";
private static final String URL_FORMAT = "%s%sExpires=%d&Signature=%s&Key-Pair-Id=%s";
public static void main(String[] args) throws Exception {
if (args.length != 3 && args.length != 4) {
System.out.printf("usage: java %s <baseUrl> <keyPairId> <keyPath> [durationSeconds]%n",
SignedURLGenerator.class.getName());
System.out.println(" baseUrl ... Resource URL to sign. ex. http://xxx.cloudfront.net/path/to/resource");
System.out.println(" keyPairId ... CloudFront key pair ID. ex. APKAZZZZZZZZZZZZZZZZ");
System.out.println(" keyPath ... Path to private key. ex. /path/to/privateKey.der");
System.out.println(" durationSeconds ... Expiring duration in seconds. (optional, default 300) ex. 60");
System.exit(1);
}
String resourceUrl = args[0];
String keyPairId = args[1];
Path privateKeyFile = Paths.get(args[2]);
long durationSeconds = args.length == 4 ? Long.parseLong(args[3]) : DEFAULT_DURATION_SEC;
byte[] derPrivateKey = Files.readAllBytes(privateKeyFile);
long expires = (System.currentTimeMillis() / 1000) + durationSeconds;
String signedUrl = generateSignedURL(resourceUrl, keyPairId, derPrivateKey, expires);
System.out.println(signedUrl);
}
private static String generateSignedURL(String resourceUrl, String keyPairId, byte[] derPrivateKey, long expires) {
String policy = String.format(POLICY_FORMAT, resourceUrl, expires);
try {
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PKCS8EncodedKeySpec privSpec = new PKCS8EncodedKeySpec(derPrivateKey);
PrivateKey privateKey = keyFactory.generatePrivate(privSpec);
// Sign data
Signature signature = Signature.getInstance("SHA1withRSA");
signature.initSign(privateKey, new SecureRandom());
signature.update(policy.getBytes("UTF-8"));
byte[] signatureBytes = signature.sign();
String signatureString = BaseEncoding.base64().encode(signatureBytes);
// Convert the given data to be safe for use in signed URLs for a private distribution by
// using specialized Base64 encoding.
String urlSafeSignature = signatureString
.replace('+', '-')
.replace('=', '_')
.replace('/', '~');
return String.format(URL_FORMAT,
resourceUrl,
resourceUrl.contains("?") ? "&" : "?",
expires,
urlSafeSignature,
keyPairId);
} catch (Exception e) {
throw new Error(e);
}
}
}
コード詳解
非常にシンプルなので一気にご紹介します。
- 1〜9行目
- 非常にシンプルなimportです。Java標準クラスと、Guavaにしか依存していないことが分かります。
- 13〜18行目
- 各種定数の定義です。デフォルト値と書式文字列ですね。
- 21〜42行目
- mainメソッドです。ヘルプ表示や引数の解析等、本稿としてはあまり本質的ではない部分かと思います。
- 44行目
- 本稿のメインgenerateSignedURLメソッドです。
- 45行目
- まず、このURLが指し示すリソースに対してどのようなアクセス制限が必要なのか、それを表現するJSON文字列を生成します。定数書式に従って、ですね。ここを拡張すれば、IP縛り等のアクセス制限も実装可能です。
- 48〜57行目
- Java SE SecurityのAPIを利用して、電子署名を行っている部分です。署名はバイト列としてsignatureBytesに得られます。
- 58行目
- Guavaを使った箇所。署名バイト列をBase64エンコードしています。Java8には標準実装がありますね。それ以前であればこのコードのようにGuavaを使うか、またはcommons-codec等をご利用ください。
- 60〜65行目
- Base64エンコードした文字列をそのままURLに指定してしまうと、URLとして+, =, /が特殊解釈されてしまうため、これらをそれぞれ-, _, ~に置換します。この置換はドキュメントにも明記されています。
- 67〜72行目
- ここまでで得られた署名等の情報を元に、URLを組み立てて返します。
まとめ
というわけで、JetS3tに依存しない CloudFront 署名付きURL生成方法をご紹介しました。