aws-sdk-java-v2に追加されたS3Presigner

## TL;DR

- AWS SDK for Java 2.0に、S3Presignerが追加された
- virtual-hosted styleにデフォルトで対応
- httpsにデフォルトで対応
- 有効期限の設定方法が変更
- 署名付URLの生成には、S3Presignerを使用しましょう

## はじめに
この記事は、JavaやScalaでAWSを利用するバックエンド、およびSRE業務に従事する人を対象にしています。

## S3の署名付URLとは

S3には、オブジェクトのダウンロードとアップロードのために発行するURLに署名をつけることによって、そのオブジェクトへのアクセスを制限する仕組みがあります。

## AWS SDK for Java 2.10.12以前
AWS SDK for Java 2.10.12以前では、AwsS3V4Signerを使用して、Presigned URLを生成していました。
以下、サンプルコードは、scalaで記述しています。

```scala:before.scala
import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider
import software.amazon.awssdk.auth.signer.AwsS3V4Signer
import software.amazon.awssdk.auth.signer.params.Aws4PresignerParams
import software.amazon.awssdk.http.{SdkHttpFullRequest, SdkHttpMethod}

val request = SdkHttpFullRequest
  .builder()
  .encodedPath("bucket/key")
  .host("s3.ap-northeast-1.amazonaws.com")
  .method(SdkHttpMethod.GET)
  .protocol("https")
  .build()

val params = Aws4PresignerParams
  .builder()
  .expirationTime(Instant.now().plusSeconds(300)
  .awsCredentials(DefaultCredentialsProvider.create().resolveCredentials())
  .signingName("s3")
  .signingRegion("ap-northeast-1")
  .build()

AwsS3V4Signer.create().presign(request, params).getUri

// 生成される署名付URLの形式
// https://s3.ap-northeast-1.amazonaws.com/bucket/key?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20200625T000000Z&X-Amz-SignedHeaders=host&X-Amz-Expires=300&X-Amz-Credential=XXX&X-Amz-Signature=YYY
```

## AWS SDK for Java 2.10.12以後
AWS SDK for Java 2.10.12から、S3Presignerが追加されました[^1]。引き続きAwsS3V4Signerは使用できますが、S3Presignerを使うことが推奨されています。この理由については後述しますが、一方でS3Presignerの内部実装には、依然、AwsS3V4Signerが使用されています。

```scala:after.scala
import software.amazon.awssdk.regions.Region
import software.amazon.awssdk.services.s3.model.GetObjectRequest
import software.amazon.awssdk.services.s3.presigner.S3Presigner
import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest

val presigner: S3Presigner = S3Presigner
    .builder()
    .region("ap-northeast-1")
    .build()

val request = GetObjectRequest
  .builder()
  .bucket("bucket")
  .key("key")
  .build()

val presignRequest = GetObjectPresignRequest
  .builder()
  .signatureDuration(Duration.ofSeconds(300))
  .getObjectRequest(request)
  .build()

presigner.presignGetObject(presignRequest).url.toURI

// 生成される署名付URLの形式
// https://bucket.s3.ap-northeast-1.amazonaws.com/key?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20200625T000000Z&X-Amz-SignedHeaders=host&X-Amz-Expires=300&X-Amz-Credential=XXX&X-Amz-Signature=YYY
```

## 何が変わった？
### 1. virtual-hosted style
生成されるURLを前後で比較すると、バケット名の位置が異なることがわかります。

```scala
"https://s3.ap-northeast-1.amazonaws.com/bucket/key" // 前: path style
"https://bucket.s3.ap-northeast-1.amazonaws.com/key" // 後: virtual-hosted style
```
両者の違いについては、以下を参照ください。

1. [パス形式のリクエスト](https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/dev/VirtualHosting.html#path-style-access)
1. [仮想ホスティング形式のリクエスト](https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/dev/VirtualHosting.html#path-style-access)

```scala
val request = SdkHttpFullRequest
  .builder()
  .encodedPath("key")
  .host("bucket.s3.ap-northeast-1.amazonaws.com")
  .method(SdkHttpMethod.GET)
  .protocol("https")
  .build()
```
なお、AwsS3V4SignerでもSdkHttpFullRequestを上記のようにすることで、virtual-hosted styleの署名付URLが生成できますが、S3Presignerは実装方法を意識する必要がありません。

### 2. protocolの指定方法
AwsS3V4Signerでは、`.protocol("https")`の部分にある通り、プロトコルの指定ができました。しかし、 S3Presignerにはプロトコルを指定するメソッドがありません。これについては、内部的にhttpsがデフォルトで指定されていることに起因します。使用するシーンは想像しにくいですが、以下のようにendpointを上書きすることで、httpを選択することも可能です。

```scala
val presigner: S3Presigner = S3Presigner
    .builder()
    .region("ap-northeast-1")
    .endpointOverride(new URI("http://s3.ap-northeast-1.amazonaws.com"))
    .build()
```

### 3. 有効期限の指定方法
AwsS3V4Signerでは、有効期限として設定したい未来の日時を指定する方法でした。しかし、S3Presignerでは有効期間をDurationとして指定する方法が採用されています。

```scala
// 前
val params = Aws4PresignerParams
  .builder()
  .expirationTime(Instant.now().plusSeconds(300)

// 後
val presignRequest = GetObjectPresignRequest
  .builder()
  .signatureDuration(Duration.ofSeconds(300))

```

## まとめ
[Amazon S3 path-style 廃止予定 – それから先の話 –](https://aws.amazon.com/jp/blogs/news/amazon-s3-path-deprecation-plan-the-rest-of-the-story/)

AWSは2020年9月30日以降で作成されるバケットについて、path styleをサポートしないことを発表しています。このため、S3Presignerへの変更は早期に対応しておく必要があると言えます。

[^1]: 追加までの経緯は、[こちら](https://github.com/aws/aws-sdk-java-v2/issues/203)