S3の制限付きダウンロードURLにIPアドレス制限をかける

前回のソースでは有効期限のみの制限となるのでリンクを知っている人なら
誰でもダウンロードできてしまう問題がある

もう一段回セキュリティを強化する方法として時間の制限に加えてIPアドレスの制限をかける
IPアドレスの制限はIAMのポリシーで行うことができるのでダウンロードさせたいIPアドレス
ポリシーに設定したユーザを作成し、そのAWSアクセスキーでURLを生成すればよい

ポリシーは以下のようになる

{
    "Statement": [
        {
            "Sid": "【適当な半角英数字の文字列(記号不可)】",
            "Effect": "Allow",
            "Action": [
                "s3:GetObject"
            ],
            "Resource": [
                "arn:aws:s3:::【バケット名】/【バケット配下のパス】【ファイル名】"
            ],
            "Condition": {
                "IpAddress": {
                    "aws:SourceIp": [
                        "【IPアドレス】/【サブネットマスク】"
                    ]
                }
            }
        }
    ]
}

ダウンロードなのでActionは「s3:GetObject」のみ
IPアドレス固定の場合「xxx.xxx.xxx.xxx/32」でOK

AWSのJavaSDKでユーザを作成する場合はこんな感じ

// 適当なユーザ名を作る
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmssSSS");
String userName = "user" + sdf.format(Calendar.getInstance().getTime());
String policyName = "policy_" + userName;
String sid = "sid" + userName;
String ipAddress = "【IPアドレス】/【サブネットマスク】";

// ダウンロードしたいファイルを指定
String bucketName = "【バケット名】";
String key = "【バケット配下のパス】【ファイル名】";

// IAMの操作権限を持つアカウントでクライアントを生成
AWSCredentials creAdmin = new BasicAWSCredentials(AWS_ACCESS_KEY, AWS_SECRET_ACCESS_KEY);
AmazonIdentityManagementClient client = new AmazonIdentityManagementClient(creAdmin);

// ユーザ作成
CreateUserRequest createUserRequest = new CreateUserRequest(userName);
User user = client.createUser(createUserRequest).getUser();

// アクセスキー作成
AccessKey accessKey = client.createAccessKey(
		new CreateAccessKeyRequest().withUserName(userName))
		.getAccessKey();
System.out.println(accessKey.getAccessKeyId());		// <- AWSアクセスキー
System.out.println(accessKey.getSecretAccessKey());	// <- AWSシークレットキー

// ポリシー設定
Resource s3Resource = new S3BucketResource(bucketName + "/" + key);
Condition condition = new IpAddressCondition(ipAddress);
Statement statement = new Statement(Effect.Allow).withId(sid)
		.withActions(S3Actions.GetObject).withResources(s3Resource)
		.withConditions(condition);
Policy policy = new Policy().withStatements(statement);
PutUserPolicyRequest putUserPolicyRequest = new PutUserPolicyRequest(
		userName, policyName, policy.toJson());
client.putUserPolicy(putUserPolicyRequest);

あとは作成したユーザのAWSアクセスキー/AWSシークレットキーでダウンロードURLを作成するだけ

ユーザ作成からAWSアクセスキーが使用可能になるまで少々時間がかかるらしく
5〜8秒くらい待つと大丈夫みたい(URLの生成はできるがURLをGETした際に403エラーとなる)

IP制限/時間制限付きダウンロードURLを返すRESTサービス

ダウンロードURLを生成するRESTサービスをJerseyで作成するとこんな感じになると思います
リクエストからリモートIPアドレスを取得してIAMの権限にセットしているので同じクライアント(IPアドレス)
からしかダウンロードできないよう制限をかけることができる

@GET
@Produces( { MediaType.APPLICATION_JSON })
@Path("download/{bucket}/{key}/")
public Response generateDownloadUrl(@Context HttpServletRequest request,
		@PathParam("bucket") String bucket, @PathParam("key") String key) {

	// 適当なユーザ名を作る
	SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmssSSS");
	String userName = "user" + sdf.format(Calendar.getInstance().getTime());
	String policyName = "policy_" + userName;
	String sid = "sid" + userName;
	String ipAddress = request.getRemoteAddr() + "/32";

	// IAMの操作権限を持つアカウントでクライアントを生成
	AWSCredentials creAdmin = new BasicAWSCredentials(AWS_ACCESS_KEY, AWS_SECRET_ACCESS_KEY);
	AmazonIdentityManagementClient clientIAM = new AmazonIdentityManagementClient(creAdmin);

	// ユーザ作成
	CreateUserRequest createUserRequest = new CreateUserRequest(userName);
	User user = clientIAM.createUser(createUserRequest).getUser();

	// アクセスキー作成
	AccessKey accessKey = clientIAM.createAccessKey(
			new CreateAccessKeyRequest().withUserName(userName))
			.getAccessKey();
	System.out.println(accessKey.getAccessKeyId());
	System.out.println(accessKey.getSecretAccessKey());

	// ポリシー設定
	Resource s3Resource = new S3BucketResource(bucket + "/" + key);
	Condition condition = new IpAddressCondition(ipAddress);
	Statement statement = new Statement(Effect.Allow).withId(sid)
			.withActions(S3Actions.GetObject).withResources(s3Resource)
			.withConditions(condition);
	Policy policy = new Policy().withStatements(statement);
	System.out.println(policy.toJson());
	PutUserPolicyRequest putUserPolicyRequest = new PutUserPolicyRequest(
			user.getUserName(), policyName, policy.toJson());
	clientIAM.putUserPolicy(putUserPolicyRequest);

	// すぐにはユーザが反映されないらしいのでちょっと待つ
	TimeUnit.SECONDS.sleep(5);

	// 作成したアカウントでクライアントを生成
	AWSCredentials cre = new BasicAWSCredentials(
			accessKey.getAccessKeyId(), accessKey.getSecretAccessKey());
	AmazonS3Client client = new AmazonS3Client(cre);

	// 有効期限(5分)
	Calendar cal = Calendar.getInstance();
	cal.add(Calendar.MINUTE, 5);
	Date limit = cal.getTime();

	// URLを生成
	URL url = client.generatePresignedUrl(new GeneratePresignedUrlRequest(
			bucket, key).withExpiration(limit));

	// 生成したURLを返す
	Response.ResponseBuilder responseBuilder = Response.ok();
	responseBuilder.entity(url.toString());
	return responseBuilder.build();
}

実際に使おうと思ったらユーザの有効チェックやファイルの存在チェックを入れてもいいと思います。
あとユーザ情報が残るので有効期限が切れたら削除してあげるような仕組みを作っておいた方が良いかも

使い終わったユーザを削除する

こんな感じのコードを生成処理と同じところに入れておけば良いと思います

ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1);
FutureTask<String> task = new FutureTask<String>(new DeleteUserTask(
		creAdmin, userName, policyName, accessKey.getAccessKeyId()));
// 有効期限に合わせる
executor.schedule(task, 5, TimeUnit.MINUTES);
executor.shutdown();
class DeleteUserTask implements Callable<String> {

	private AWSCredentials cre;
	private String userName;
	private String policyName;
	private String userAccessKeyId;

	public DeleteUserTask(AWSCredentials cre, String userName,
			String policyName, String userAccessKeyId) {
		this.cre = cre;
		this.userName = userName;
		this.policyName = policyName;
		this.userAccessKeyId = userAccessKeyId;
	}

	public String call() throws Exception {
		// IAMの操作権限を持つアカウントでクライアントを生成
		AmazonIdentityManagementClient client = new AmazonIdentityManagementClient(cre);
		// ポリシー削除(ユーザ削除の前に行わないとエラー)
		client.deleteUserPolicy(new DeleteUserPolicyRequest(userName, policyName));
		// アクセスキー削除(ユーザ削除の前に行わないとエラー)
		client.deleteAccessKey(new DeleteAccessKeyRequest(userAccessKeyId).withUserName(userName));
		// ユーザ削除
		client.deleteUser(new DeleteUserRequest(userName));
		return "ok";
	}
}

2012/09/15追記

毎回IAMユーザを作成するのは面倒だし反映されるまで毎回待たされるのも問題
Amazon IAMはユーザを作っても作りっぱなしにしても料金は掛からないので、
使い終わったユーザが残ってても気にしないのであれば消さずに再利用するのが良さそう
AWS Identity and Access Management (IAM) Preview Beta | アマゾン ウェブ サービス(AWS 日本語)