本記事では、業務中調べるのに苦労した AWS の S3 のアップロード用署名付き URL の PUT と POST のできることの違いについてまとめました。
それぞれでできることが異なりますが、これをまとめている記事を見つけられなかったので備忘録がてら書いていきます。
S3 の署名付き URL とは
署名付き URL(Presigned URL)は、AWS の認証情報を持たないユーザーに対して、S3 バケットへの一時的なアクセス権を与える仕組みです。
URL や フォームデータ自体に認証情報が含まれているので、これらを知っていれば誰でもアクセスできるようになります。
署名付き URL には GET、PUT、POST の 3 つの HTTP メソッドがあります。
| メソッド | 主な用途 | AWS CLI で URL 発行可能か |
|---|---|---|
| GET | S3 に保存されたファイルのダウンロード | ○ |
| PUT | 1 つのファイルを直接 S3 にアップロード | × |
| POST | フォームベースでのファイルアップロード | × |
アップロード用署名付き URL を発行するための最小限コード
今回は TypeScript の SDK で紹介します。
S3のバケットポリシーは何も設定していないものとします。
PUT 最小限コード
長いURLが返ります。
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
const s3Client = new S3Client({ region: "ap-northeast-1" });
const command = new PutObjectCommand({
Bucket: "tora-ultra-bucket",
Key: "test/sample.txt",
});
async function main() {
const url = await getSignedUrl(s3Client, command);
console.log(url);
}
main();
https://tora-ultra-bucket.s3.ap-northeast-1.amazonaws.com/test/sample.txt?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20251216%2Fap-northeast-1%2Fs3%2Faws4_request&X-Amz-Date=20251216T083421Z&X-Amz-Expires=3600&X-Amz-Security-Token=IQoJb3JpZ2luX2VjEJj...(省略)&X-Amz-Signature=dbe2741e98a06ac74b65cd56d79a5470...&X-Amz-SignedHeaders=host&x-amz-checksum-crc32=AAAAAA%3D%3D&x-amz-sdk-checksum-algorithm=CRC32&x-id=PutObject
$ curl -X PUT --upload-file ./sample.txt "https://tora-ultra-bucket.s3.ap-northeast-1.amazonaws.com/test/sample.txt?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20251216%2Fap-northeast-1%2Fs3%2Faws4_request&X-Amz-Date=20251216T083421Z&X-Amz-Expires=3600&X-Amz-Security-Token=IQoJb3JpZ2luX2VjEJj...(省略)&X-Amz-Signature=dbe2741e98a06ac74b65cd56d79a5470...&X-Amz-SignedHeaders=host&x-amz-checksum-crc32=AAAAAA%3D%3D&x-amz-sdk-checksum-algorithm=CRC32&x-id=PutObject"
POST 最小限コード
URLとフォームデータが返ります。
import { S3Client } from "@aws-sdk/client-s3";
import { createPresignedPost } from "@aws-sdk/s3-presigned-post";
const s3Client = new S3Client({ region: "ap-northeast-1" });
async function main() {
const { url, fields } = await createPresignedPost(s3Client, {
Bucket: "tora-ultra-bucket",
Key: "test/sample.txt",
});
console.log("URL:", url);
console.log("Fields:", fields);
}
main();
URL: https://tora-ultra-bucket.s3.ap-northeast-1.amazonaws.com/
Fields: {
'bucket': 'tora-ultra-bucket',
'X-Amz-Algorithm': 'AWS4-HMAC-SHA256',
'X-Amz-Credential': 'AKIAIOSFODNN7EXAMPLE/20251216/ap-northeast-1/s3/aws4_request',
'X-Amz-Date': '20251216T084047Z',
'X-Amz-Security-Token': 'IQoJb3JpZ2luX2VjEJj...(省略)',
'key': 'test/sample.txt',
'Policy': 'eyJleHBpcmF0aW9uIjoiMjAyNS0...(省略)',
'X-Amz-Signature': '9c9bd3ef627b8df717dbcce67cad3783...'
}
$ curl -X POST "https://tora-ultra-bucket.s3.ap-northeast-1.amazonaws.com/" \
-F "bucket=tora-ultra-bucket" \
-F "X-Amz-Algorithm=AWS4-HMAC-SHA256" \
-F "X-Amz-Credential=AKIAIOSFODNN7EXAMPLE/20251216/ap-northeast-1/s3/aws4_request" \
-F "X-Amz-Date=20251216T084047Z" \
-F "X-Amz-Security-Token=IQoJb3JpZ2luX2VjEJj..." \
-F "key=test/sample.txt" \
-F "Policy=eyJleHBpcmF0aW9uIjoiMjAyNS0..." \
-F "X-Amz-Signature=9c9bd3ef627b8df717dbcce67cad3783..." \
-F "file=@./sample.txt"
PUT と POST のどちらも Content-Type を指定しないとメタデータは Content-Type: binary/octet-stream となります。
PUT と POST のできることの違い早見表
URLを使う側ができること
先述の最小限コードで発行したURLを使ってファイルをアップロードしようとしたときにできることです。
| 項目 | PUT | POST |
|---|---|---|
| Content-Type の指定 | ○ | × |
| ファイル上書き | ○ | ○ |
| 再利用 | ○ | ○ |
| 最大アップロードサイズ | 5GB | 明記なし |
- PUT で Content-Type を指定するには
-H "Content-Type: ..."をつけるとできます - POST で
-F "Content-Type=..."をつけると AccessDenied エラーになります - マルチパートアップロード機能を使用すると 48.8TiB までアップロードできます
署名付き URL を発行する側が制限できること
URLを発行する側が制限をかけたいとなると、PUTとPOSTでできることが異なります。
| 項目 | PUT | POST |
|---|---|---|
| Content-Type の制限 | ○ | ○ |
| ファイルサイズ制限 | × | ○ |
| 有効期限 | ○ | ○ |
| 上書き禁止 | ○ | × |
| キーをアップロード者に自由に設定させる | × | ○ |
制限できることを詳しく
ここからコードとともに説明していきます。
Content-Type の制限
PUT ⭕️
できます
PutObjectCommand の PutObjectCommandInput の ContentType と getSignedUrl 関数 の第3引数に指定します。
const command = new PutObjectCommand({
Bucket: "tora-ultra-bucket",
Key: "test/sample.txt",
ContentType: "text/plain", // 追加
});
async function main() {
const url = await getSignedUrl(s3Client, command, {
signableHeaders: new Set(["content-type"]), // 追加
});
console.log(
`curl -X PUT -H "Content-Type: text/plain" --upload-file ./sample.txt "${url}"`
);
}
もし Content-Type ヘッダーが無かったり、値が違ったりすると SignatureDoesNotMatch エラーが返ってきます。
また、URL を発行するときに signableHeaders を指定していないと、Content-Type が指定していないリクエストも受け付けてしまい、メタデータは Content-Type: binary/octet-stream となります。
POST ⭕️
できます。
Fields(と Conditions)に指定します。
const { url, fields } = await createPresignedPost(s3Client, {
Bucket: "tora-ultra-bucket",
Key: "test/sample.txt",
Fields: {
"Content-Type": "text/plain", // 追加
},
Conditions: [["eq", "$Content-Type", "text/plain"]], // 追加 なくてもよさそう
});
FieldsとConditionsについては以下の通りです
| 項目 | 説明 |
|---|---|
| Fields | 送信するフォームフィールドの初期値 |
| Conditions | リクエストを検証するためのポリシー条件 |
Fields に Content-Type が存在すると Conditions を書かずとも自動でポリシー設定されていたため、Conditions は書かなくてもいいかもしれません。
反対に Conditions だけを書いた場合、発行される URL のフォームデータには Content-Type が存在せず、そのまま使うとエラーになるというとんでも URL が生まれます。この時、リクエストに Content-Type を含めてあげると通ります。
ファイルサイズ制限
PUT ❌
先に書いた通り、最大ファイルサイズは 5GB に制限されており変更できません。
ただし、マルチパートアップロード機能を使用すると 48.8TiB までアップロードできます。(本記事では紹介しません)
POST ⭕️
できます。
Conditions に content-length-range を設定します。
const { url, fields } = await createPresignedPost(s3Client, {
Bucket: "tora-ultra-bucket",
Key: "test/sample.txt",
Conditions: [
["content-length-range", 0, 1024 * 1024], // 追加 この場合は最大1MB
],
});
設定できる範囲は明記されていませんでした。
有効期限
PUT ⭕️
できます
getSignedUrl 関数 の第3引数に指定します。
const url = await getSignedUrl(s3Client, command, {
expiresIn: 60, // 追加 60秒
});
そもそも設定していないとデフォルトは 15 分になります。
PutObjectCommand の PutObjectCommandInput に指定できる Expires は別物なので注意です。
POST ⭕️
できます
Expires に数値を設定します。
const { url, fields } = await createPresignedPost(s3Client, {
Bucket: "tora-ultra-bucket",
Key: "test/sample.txt",
Expires: 60, // 追加 60秒
});
そもそも設定していないときのデフォルトの有効期限が何分になるのかの記述は見つけられませんでした。
上書き禁止
PUT ⭕️
できます
PutObjectCommand の PutObjectCommandInput の IfNoneMatch(と getSignedUrl 関数 の第3引数)に指定します。
const command = new PutObjectCommand({
Bucket: "tora-ultra-bucket",
Key: "test/sample.txt",
IfNoneMatch: "*", // 追加
});
async function main() {
const url = await getSignedUrl(s3Client, command, {
signableHeaders: new Set(["if-none-match"]), // 追加 なくてもよさそう
});
console.log(
`curl -X PUT -H "If-None-Match: *" --upload-file ./sample.txt "${url}"`
);
}
すでに同じ Key で存在している場合は 412 PreconditionFailed エラーが返ります。
signableHeaders を指定しなくても If-None-Match ヘッダーがないリクエストはエラーになったため、signableHeaders は書かなくてもいいかもしれません。
POST ❌
AI に聞くと「できるよ!」って元気に返してくることがありますができません。
キーをアップロード者に自由に設定させる
PUT ❌
固定のキーでしか発行できません。
POST ⭕️
できます
キーに ${filename} を設定することでできます。
この例の場合は uploads/ 以降をアップロードする側に自由に決めてもらえます。
const { url, fields } = await createPresignedPost(s3Client, {
Bucket: "tora-ultra-bucket",
Key: "uploads/${filename}", // 編集
Conditions: [["starts-with", "$key", "uploads/"]], // 追加 なくてもよさそう
});
Conditions を書かずとも自動でポリシー設定されていたので書かなくてもよさそうです。
以下のようなとんでも URL を発行できてしまうので設定の仕方には注意してください。
const { url, fields } = await createPresignedPost(s3Client, {
Bucket: "tora-ultra-bucket",
Key: "${filename}",
});
おわりに
ここで紹介した以外のことでも POST の方が多くのことができますが、PUT でないとできないこともいくつかあります。
AI は嘘つきがちなので実現可能か調査して POST を使うか PUT を使うか選択しましょう!
参考サイト