aws s3의 createPresignedPost 사용

2023.07.14

s3 logo


무엇을 할 것인가?

presignedPost를 이용하여 s3에 파일 업로드하는 방법을 좀더 자세히 알아보기.


사실 예전에 개인 프로젝트를 진행하면서 s3의 presignedPost를 사용해 봤던 적이 있는데 막상 지금 다시 보니 왜 이렇게 썻는지 기억이 나질 않았다.
그래서 presignedPost의 사용법을 메모하는겸 정리하여 작성한다. 서명된 URL(presigned)을 사용하는 이유는 다들 알거라 생각하고 생략한다.
해당 포스트의 대부분의 내용은 이 포스트를 참조하였다.


getSignedUrl 이 아닌 createPresignedPost를 쓰는 이유

참조 포스트에도 나와있는 내용대로 s3.getSignedUrlputObject 로도 파일업로드가 가능하지만 해당 포스트가 쓰여진 2018년 기준 getSignedUrl에서는 업로드할 파일의 크기를 제한할수있는 방법이 없다. 그래서 대신 조건 설정이 가능한 s3.createPresignedPost를 사용한다고 설명한다.
서명된 url을 생성할때 파일 크기 제한을 걸어야하는 이유는 이 url을 통해 실제 파일을 업로드하는 작업은 백엔드 서비스가 아닌 클라이언트에서하기 때문이다. 즉, 악의적인 사용자가 서명된 url을 가지고 크기가 큰 파일을 업로드할수있기 때문에 url 자체에서 크기제한을 걸어둬야한다.
그 외에도 업로드할 파일의 확장자나 파일명등을 지정할수있다.
(2023년 기준 getSignedUrl 에서도 파일 크기 제한이 가능한지는 자료를 찾지못했다...)


createPresignedPost 메소드 (aws-sdk v3)

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 옵션으로 업로드할때 조건을 설정할수있다. 배열 형태로 파라미터를 넘겨주는데 몇가지 옵션은 정말 특이한 타입으로 되어있어서 헷갈릴수있다. 공식문서에서 설명하는 내용은 이 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] 일수도 있다고 한다.
그리고 공식문서의 조건일치 항목을 보자.


condition matching


조건의 타입을 설명하고있다. 첫번째로 정확한 일치(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]

또 다시 공식문서로 돌아가서 적용할수있는 조건 목록을 보면 어떤 조건타입을 지원하는지 볼수있다.


condition policy


예를들어 위 이미지에 나온대로 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']

Conditions 설정 예제

createPresginedPost로 파일을 업로드할때 조건을 설정해보자.

  1. 업로드 파일의 크기는 0~10MiB
  2. 업로드 파일의 타입은 image/ 타입만 가능
const url = await createPresignedPost(client, {
      Bucket: {버킷 이름},
      Conditions: [
          ['content-length-range',0,10485760],
          {'Content-Type':'image/'}    // 'image/'로 시작하는 모든 타입을 허용한다.
      ],
      Key: {업로드될 파일 이름},
      Expires: {링크의 유효시간()}
    });

생성된 url로 프론트엔드에서 사용하기

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)
}

Do you want something exciting?

© 2022. YSH All rights reserved.