presignedPost
를 이용하여 s3에 파일 업로드하는 방법을 좀더 자세히 알아보기.
사실 예전에 개인 프로젝트를 진행하면서 s3의 presignedPost를 사용해 봤던 적이 있는데 막상 지금 다시 보니 왜 이렇게 썻는지 기억이 나질 않았다.
그래서 presignedPost의 사용법을 메모하는겸 정리하여 작성한다. 서명된 URL(presigned)을 사용하는 이유는 다들 알거라 생각하고 생략한다.
해당 포스트의 대부분의 내용은 이 포스트를 참조하였다.
참조 포스트에도 나와있는 내용대로 s3.getSignedUrl
의 putObject
로도 파일업로드가 가능하지만 해당 포스트가 쓰여진 2018년 기준 getSignedUrl에서는 업로드할 파일의 크기를 제한할수있는 방법이 없다. 그래서 대신 조건 설정이 가능한 s3.createPresignedPost
를 사용한다고 설명한다.
서명된 url을 생성할때 파일 크기 제한을 걸어야하는 이유는 이 url을 통해 실제 파일을 업로드하는 작업은 백엔드 서비스가 아닌 클라이언트에서
하기 때문이다. 즉, 악의적인 사용자가 서명된 url을 가지고 크기가 큰 파일을 업로드할수있기 때문에 url 자체에서 크기제한을 걸어둬야한다.
그 외에도 업로드할 파일의 확장자나 파일명등을 지정할수있다.
(2023년 기준 getSignedUrl 에서도 파일 크기 제한이 가능한지는 자료를 찾지못했다...)
aws-sdk가 v3로 넘어오면서 패키지가 모듈형식으로 바뀌었다. 필요한 패키지 두개를 설치한다.
(aws-sdk v3가 궁금하다면 aws-sdk v3 공식 문서를 확인해보자.)
npm i @aws-sdk/s3-presigned-post @aws-sdk/client-s3
import { createPresignedPost } from '@aws-sdk/s3-presigned-post';
import { S3Client } from '@aws-sdk/client-s3';
const client = new S3Client({ //따로 credentials를 설정하지 않으면 자동으로 /.aws/credentials를 가져온다
region: 'ap-northeast-2'
});
const url = createPresignedPost(client, {
Bucket: {버킷 이름},
Conditions: [정책],
Key: {업로드될 파일 이름},
Expires: {링크의 유효시간(초)}
});
Conditions 옵션으로 업로드할때 조건을 설정할수있다. 배열 형태로 파라미터를 넘겨주는데 몇가지 옵션은 정말 특이한 타입으로 되어있어서 헷갈릴수있다. 공식문서에서 설명하는 내용은 이 Conditions에는 두가지 조건으로 정확한 일치(exact matching)
와 시작부분 부터 일치(starts-with matching)
조건을 지원한다고 한다. 그리고 content-length-range
라는 별도의 조건 타입도 존재한다.
먼저 createPresignedPost() 메소드의 Conditions의 타입을 보자.
//@aws-sdk/s3-presigned-post 패키지에서 정의된 createPresignedPost() 메소드
import { S3Client } from "@aws-sdk/client-s3";
import { Conditions as PolicyEntry } from "./types";
type Fields = Record<string, string>;
export interface PresignedPostOptions {
Bucket: string;
Key: string;
Conditions?: PolicyEntry[];
Fields?: Fields;
Expires?: number;
}
export interface PresignedPost {
url: string;
fields: Fields;
}
/**
* Builds the url and the form fields used for a presigned s3 post.
*/
export declare const createPresignedPost: (client: S3Client, { Bucket, Key, Conditions, Fields, Expires }: PresignedPostOptions) => Promise<PresignedPost>;
export {};
코드상에서 PolicyEntry[]
라는 타입으로 정의 되어있다. 그리고 저 PolicyEntry
는 이런 타입으로 정의 되어있다.
type EqualCondition = ["eq", string, string] | Record<string, string>;
type StartsWithCondition = ["starts-with", string, string];
type ContentLengthRangeCondition = ["content-length-range", number, number];
export type Conditions = EqualCondition | StartsWithCondition | ContentLengthRangeCondition;
export {};
Conditions 파라미터에 넘겨주는 값의 타입은 ["eq", string, string] | Record<string,string>
일수도있고 ["starts-with", string, string]
일수도 있고 ["content-length-range", number, number]
일수도 있다고 한다.
그리고 공식문서의 조건일치 항목을 보자.
조건의 타입을 설명하고있다. 첫번째로 정확한 일치(exact match)
조건인데 위에서 정의된 EqualCondition
이라는 타입 대로 ['eq',string,string]
또는 {key:value}
(Record 타입) 타입을 지원한다.
그리고 starts with
조건 타입이 있다. 이것도 StartsWithCondition
이라는 타입 처럼 ["starts-with", string, string]
형식으로 쓸수있다.
마지막으로 content-length-range
라는 조건을 이런방식으로 사용할수 있다. (byte 단위)
["content-length-range", 1048576, 10485760]
또 다시 공식문서로 돌아가서 적용할수있는 조건 목록을 보면 어떤 조건타입을 지원하는지 볼수있다.
예를들어 위 이미지에 나온대로 Content-Type 은 정확한 일치와 starts-with 일치를 지원한다. 아래와 같은 방법으로 사용할수있다. 어떤 방법을 사용하든 결과는 똑같다.
//정확한 일치 사용시(exact match)
const contentTypePolicy=['eq','$Content-Type','image/jpeg']
//or
const contentTypePolicy={'Content-Type':'image/jpeg'}
//starts-with 일치 사용시
const contentTypePolicy=['starts-with','$Content-type','image/jpeg']
createPresginedPost로 파일을 업로드할때 조건을 설정해보자.
const url = await createPresignedPost(client, {
Bucket: {버킷 이름},
Conditions: [
['content-length-range',0,10485760],
{'Content-Type':'image/'} // 'image/'로 시작하는 모든 타입을 허용한다.
],
Key: {업로드될 파일 이름},
Expires: {링크의 유효시간(초)}
});
createPresignedPost() 메소드를 실행하고 나면 이런 결과값이 나온다.
{
url: 'https://s3.ap-northeast-2.amazonaws.com/{버킷이름}',
fields: {
bucket: {버킷이름},
'X-Amz-Algorithm': {사용한 서명 알고리즘},
'X-Amz-Credential': '*************/20230713/ap-northeast-2/s3/aws4_request',
'X-Amz-Date': {만료시간},
key: {업로드될 파일명},
Policy: {base64로 인코딩된 정책 문자열},
'X-Amz-Signature': '4d680d3ec36fe3f11ab8cdc686095bfea3af0e9ff5382263d8d87a133e7cced6'
}
}
이 값들로 프론트엔드에서 formData로 post 요청을 보낸다. fields에 있는 모든값들을 formData에 추가해준다.
//프론트엔드에서 s3로 업로드할때
const test=(e)=>{
const form=new FormData()
form.append('bucket','')
form.append('X-Amz-Algorithm','')
form.append('X-Amz-Credential','')
form.append('X-Amz-Date','')
form.append('key','test.jpg')
form.append('Policy','')
form.append('X-Amz-Signature','')
form.append('Content-Type','image/png') //fields 에는 없지만 content-type 도 입력해줘야한다.
form.append('file',e.files[0])
axios.post('https://s3.ap-northeast-2.amazonaws.com/버킷이름',form)
}