[AWS] S3 PreSignedUrl / SignedUrl

한결·2024년 3월 14일

AWS

목록 보기
1/1

PreSignedUrl / SignedUrl를 이용하면
S3를 퍼블릭으로 해놓지 않아도 사용자가 리소스 업로드/get 가능
현업에서는 S3를 퍼블릭으로 열어두는 경우가 없기 때문에 이를 사용한다.
또한 만료시간이 정해져 있어(default : 3600s, 최대 7일)

API 예시는 AWS Lambda + Node.js를 사용함
Client 예시는 Next.js 사용

전제
AWS Lambda > 구성 > 권한에 S3 Access 권한이 있어야한다.

S3 Presigned Url

Presigned Url 이용하면 AWS 보안 자격 증명이나 권한이 없어도 업로드 가능

사용

AWS Lambda & Serverless & Node.js & TypeScript
를 사용해서 Presigned Url 을 발급하는 API를 만들었다.

Next.js를 S3에 정적배포하여 클라이언트를 구성했다.

사용 의존성

@aws-sdk/s3-presigned-post
@aws-sdk/client-s3

Lambda코드

export async function handler(event : any) : Promise<APIGatewayProxyResultV2> {
    preprocessLambdaEvent(event);
  	// 0
    const client = new S3Client({
        region : process.env.REGION
    });

    let Key = null;
    let result = null;
    let response = null;

    try {
        if (event.httpMethod === 'GET') {
            switch (event.resource) {
                case '/s3/presigned-url':
                    // 1
                    const directory = event.queryStringParameters.directory;
                    const fileName = event.queryStringParameters.fileName;
                    const lastDotIndex = fileName.lastIndexOf('.');
                    const extension = lastDotIndex >= 0 ? fileName.substring(lastDotIndex) : '';
                
                    Key = `${directory}${(Math.random() + 1).toString(36).substring(2)}${extension}`;
    				
                	// 2
                    let { url, fields } = await createPresignedPost(client, {
                        Bucket: process.env.S3_BUCKET_NAME,
                        Key,
                        Expires: 3600,
                    });
                    result = {url, fields};
                    break;
            }
        }

        response = messageUtil.success(result);;
    } catch (error) {
        console.log(error);
        response = messageUtil.error(error);
    }

    return response;
}  

0 : @aws-sdk/client-s3 의 S3Client를 이용해 새로운 사용자를 생성한다. region은 내가 업로드하려는 버킷의 region

1 : Key는 디렉토리 + 랜덤값 + 확장자(.png, .jpg 등) 로 생성
파일명을 그대로 사용하면 같은 디렉토리에 같은 이름의 파일이 업로드될 때 에러가 발생하므로 랜덤값을 줬다.
(Key로 나중에 Signed Url을 요청하여 Get해오기 때문에 DB에 저장했다. key는 게시글 생성할 때 같이 저장되므로 해당 로직은 다른 파일에 있음)

2 : @aws-sdk/s3-presigned-postcreatePresignedPost() 함수로 Presigned Url 생성
버킷명, Key 값, 만료 시간 을 지정


해당 람다 함수(API)로 Presigned Url을 얻을 수 있다.
생김새는 다음과 같다.

{
    "url": "https://${bucket_name}.s3.ap-northeast-2.amazonaws.com/",
    "fields": {
        "bucket": ${bucket_name},
    	"X-Amz-Algorithm": "AWS4-HMAC-SHA256",
    	"X-Amz-Credential": "ASIA2GHGWWN2O5NWQYW4/20240314/ap-northeast-2/s3/aws4_request",
    	"X-Amz-Date": "20240314T022938Z",
    	"X-Amz-Security-Token": "IQoJb3JpZ2luX2VjEGIaDmFwLW5vcnRoZWFzdC0yIkcwRQIhAJkSWABVZ37Tf0THVWZXIaumi21aCz+gRJi4E5iZmtAHAiAdzG1R1raC/gNgqP0Tmpzs9+X7K3/zKNJXbIJcpXVZ7CqaAwhsEAQaDDcwMDU2MzUwMTk0MCIM2tHLD2Rs2SdNg74IKvcC0mi1nBKtLGqAjrWpVoDz1dUo1LhmcYboSbcg3OdkPUZqkCnUggI5jiJn69MkWAiFrJO0v+PghPvSfNn/Ut7tdHMQhQAbUfYfOBOC033VkBDAGtNWQiFod35eOWrYz64PoLyaF7tJs8YtXcjuaEd/ULWYnG8MxYc+C6TZxPc+qUPwXavSt4j7gd4KDKZiOJUNev025lH5D4d5+wvWXCGMY+R/e72IADeZHyzgmVLXji43PcfbikOcbCgK10qOga8G2r0tGc5LSSL91K3fTT5Hpl9eauj++BN1RQ39L8QqiNdGzHK3LFRV4NAuiiX+T2cQJm+ZQZpk3W/phdARRJ7VlKT76xenVCFGuHbfyxls34nwnZofoqMvN1dHqvyBK8Hkp2rSTaIkRAWL5FXzZswhj/vcLglsIKfx54ijenVTYK/ySoesT3hewQCdID5Nqip6xmH36bxXlIkyVrYjFdFokcQtRIjdmGvUovB7J2ezmlipHyR6RktJMJDBya8GOp0B7dgpJu4srIWzhultS1oIj7z52/+3uTce3fcNCcWXkqWCNs0tXhsyjufv2tSxMeMfcfpQQ8HacHwmK7UpsotvCj9dXBeyrmr+8P1dp9v3oIuNuIPvpoP+Aw6RSyxzwaKhDvEiJ//DesEGHZxVVyrhvtCdcaZLJWp5EFlSZDy1cR75PC2AndW40gAdUFQUt/VkrZECpGiGTyq5uF9PZA==",
    	"key": "images/notice/l7efgaexqa",
    	"Policy": "eyJleHBpcmF0aW9uIjoiMjAyNC0wMy0xNFQwMzoyOTozOFoiLCJjb25kaXRpb25zIjpbeyJidWNrZXQiOiJlb21obmd5ZW9sIn0seyJYLUFtei1BbGdvcml0aG0iOiJBV1M0LUhNQUMtU0hBMjU2In0seyJYLUFtei1DcmVkZW50aWFsIjoiQVNJQTJHSEdXV04yTzVOV1FZVzQvMjAyNDAzMTQvYXAtbm9ydGhlYXN0LTIvczMvYXdzNF9yZXF1ZXN0In0seyJYLUFtei1EYXRlIjoiMjAyNDAzMTRUMDIyOTM4WiJ9LHsiWC1BbXotU2VjdXJpdHktVG9rZW4iOiJJUW9KYjNKcFoybHVYMlZqRUdJYURtRndMVzV2Y25Sb1pXRnpkQzB5SWtjd1JRSWhBSmtTV0FCVlozN1RmMFRIVldaWElhdW1pMjFhQ3orZ1JKaTRFNWlabXRBSEFpQWR6RzFSMXJhQy9nTmdxUDBUbXB6czkrWDdLMy96S05KWGJJSmNwWFZaN0NxYUF3aHNFQVFhRERjd01EVTJNelV3TVRrME1DSU0ydEhMRDJSczJTZE5nNzRJS3ZjQzBtaTFuQkt0TEdxQWpyV3BWb0R6MWRVbzFMaG1jWWJvU2JjZzNPZGtQVVpxa0NuVWdnSTVqaUpuNjlNa1dBaUZySk8wditQZ2hQdlNmTm4vVXQ3dGRITVFoUUFiVWZZZk9CT0MwMzNWa0JEQUd0TldRaUZvZDM1ZU9Xcll6NjRQb0x5YUY3dEpzOFl0WGNqdWFFZC9VTFdZbkc4TXhZYytDNlRaeFBjK3FVUHdYYXZTdDRqN2dkNEtES1ppT0pVTmV2MDI1bEg1RDRkNSt3dldYQ0dNWStSL2U3MklBRGVaSHl6Z21WTFhqaTQzUGNmYmlrT2NiQ2dLMTBxT2dhOEcycjB0R2M1TFNTTDkxSzNmVFQ1SHBsOWVhdWorK0JOMVJRMzlMOFFxaU5kR3pISzNMRlJWNE5BdWlpWCtUMmNRSm0rWlFacGszVy9waGRBUlJKN1ZsS1Q3NnhlblZDRkd1SGJmeXhsczM0bnduWm9mb3FNdk4xZEhxdnlCSzhIa3AyclNUYUlrUkFXTDVGWHpac3doai92Y0xnbHNJS2Z4NTRpamVuVlRZSy95U29lc1QzaGV3UUNkSUQ1TnFpcDZ4bUgzNmJ4WGxJa3lWcllqRmRGb2tjUXRSSWpkbUd2VW92QjdKMmV6bWxpcEh5UjZSa3RKTUpEQnlhOEdPcDBCN2RncEp1NHNySVd6aHVsdFMxb0lqN3o1Mi8rM3VUY2UzZmNOQ2NXWGtxV0NOczB0WGhzeWp1ZnYydFN4TWVNZmNmcFFROEhhY0h3bUs3VXBzb3R2Q2o5ZFhCZXlybXIrOFAxZHA5djNvSXVOdUlQdnBvUCtBdzZSU3l4endhS2hEdkVpSi8vRGVzRUdIWnhWVnlyaHZ0Q2RjYVpMSldwNUVGbFNaRHkxY1I3NVBDMkFuZFc0MGdBZFVGUVV0L1ZrclpFQ3BHaUdUeXE1dUY5UFpBPT0ifSx7ImtleSI6ImltYWdlcy9ub3RpY2UvbDdlZmdhZXhxYSJ9XX0=",
    	"X-Amz-Signature": "52610a97ace742a31c7a09e2f79e2a55ea59bd106705a3a06755d11b60095850"
}

이제 받은 Presigned Url로 클라이언트에서 S3로 리소스를 저장한다.

Client 코드

	async function getPreSignedPostInfo(fileName : string) {
        const params = {
            directory : "images/notice/",
            fileName
        }

        try {
            let res = await axios.get(NEXT_PUBLIC_API_URL + '/s3/presigned-url', {params});
            setPresignedPostInfo(res.data.result);
        } catch (error) {
            console.log(error);
        }
    }

위의 람다함수로 요청을 보내 Presigned Url을 받아와서 상태에 저장해준다.

	async function postImage(file : Blob) {
        if (file) {
            try {                
                const url = presignedPostInfo.url;
                const fields = presignedPostInfo.fields;
                const formData = new FormData()
                Object.entries({ ...fields, file }).forEach(([key, value]) => {
                    if (typeof value === 'string' || value instanceof Blob) {
                        formData.append(key, value);
                    } 
                })
                await axios.post(url, formData);
            } catch (error) {
                console.log(error);
            }
        }
    }

이제 받아온 Presigned Url을 위의 postImage 메서드에서 사용해 파일을 S3에 업로드한다.

Presigned Url의 field와 file을 body에 넣고
Presigned Url의 url로 요청을 보내면 된다.

여기서 발생했던 ts에러는 file의 타입관련 에러이다.
file을 Blob 타입으로 넣어줘야만한다.

const blob = new Blob([file], { type: 'image/*' });

성공하면 204로 온다

Signed Url

S3 버킷이 퍼블릭이 아니라면 일반적인 객체URL로는 리소스를 가져올 수 없다.

따라서 일반 사용자가 Presigned Url을 이용해 리소스를 S3에 업데이트했듯이
Signed Url을 이용해 리소스를 Get할 수 있다.

이것도 전제는 Lambda 함수에 S3 Access 권한이 있어야한다.

사용

AWS Lambda & Serverless & Node.js & TypeScript
를 사용해서 Signed Url 을 발급하는 API를 만들었다.

Next.js를 S3에 정적배포하여 클라이언트를 구성했다.

사용의존성

@aws-sdk/s3-presigned-post
@aws-sdk/client-s3

예시

import { APIGatewayProxyResultV2 } from "aws-lambda";
import { preprocessLambdaEvent } from "../util/aws-util";
import { GetObjectCommand, GetObjectRequest, S3Client } from "@aws-sdk/client-s3";
import { createPresignedPost } from "@aws-sdk/s3-presigned-post";
import messageUtil from "../util/message-util";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";

export async function handler(event : any) : Promise<APIGatewayProxyResultV2> {
    preprocessLambdaEvent(event);
    const client = new S3Client({
        region : process.env.REGION
    });

    let Key = null;
    let result = null;
    let response = null;

    try {
        if (event.httpMethod === 'GET') {
            switch (event.resource) {
                case '/s3/presigned-url':
                    
                    const directory = event.queryStringParameters.directory;
                    const fileName = event.queryStringParameters.fileName;
                    const lastDotIndex = fileName.lastIndexOf('.');
                    const extension = lastDotIndex >= 0 ? fileName.substring(lastDotIndex) : '';
                
                    Key = `${directory}${(Math.random() + 1).toString(36).substring(2)}${extension}`;
    
                    let { url, fields } = await createPresignedPost(client, {
                        Bucket: process.env.S3_BUCKET_NAME,
                        Key,
                        Expires: 3600,
                    });
                    result = {url, fields};
                    break;
            
                // Get Signed Url
                case '/s3/signed-url':
        			
                    const input: GetObjectRequest = {
                        Bucket: process.env.S3_BUCKET_NAME,
                        Key: event.queryStringParameters.key
                    };
                    const command = new GetObjectCommand(input);
    
                    result = await getSignedUrl(client, command,{
                        expiresIn: 3600, //Seconds before the presigned post expires. 3600 by default.
                    });
                    break;
            }
        }

        response = messageUtil.success(result);;
    } catch (error) {
        console.log(error);
        response = messageUtil.error(error);
    }

    return response;
}  

@aws-sdk/s3-presigned-post의 getSignedUrl()을 사용하여 생성한다.

createPresignedPost() 파라미터 구성은 다르지만 내용은 똑같다.

버킷명, 해당 버킷에서 가져올 파일의 Key값, 만료시간을 넣어주면된다.

"https://${버킷명}.s3.ap-northeast-2.amazonaws.com/images/notice/cv4tgap042.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=ASIA2GHGWWN2LYNNHXGY%2F20240314%2Fap-northeast-2%2Fs3%2Faws4_request&X-Amz-Date=20240314T041738Z&X-Amz-Expires=3600&X-Amz-Security-Token=IQoJb3JpZ2luX2VjEGQaDmFwLW5vcnRoZWFzdC0yIkcwRQIhAJCB%2B%2FM1vwfQQVN2wBcly1v9natHeZGs1fGyykFzBRfGAiAH55zwlFr03PsC8ML0VrzvzRNWxRvakj15j9ky%2Bn%2FpUiqaAwhtEAQaDDcwMDU2MzUwMTk0MCIMB0fKob8XNEvAeiukKvcC8jaU2anoR%2BNTioq1pufY64bx7SaxUesK5jwBIMTQEpu7dIKTKMMd3hdW%2Bec69M8G8oEZDvvIPU7vDVy54%2BEa9obTv5n16%2Bq6q51Cj6GswU9UFJL0bY5jo87eblQfR%2FL4fmx40xFDNystr1l3fnsFkG5vxC7f5Klqroempx7aDICdnZwvi5LLTTyq9VUTGDpiF4ShM7BXAN3E2uRp2c8eAYFY1KYl%2FfbKwyLLikLKyE7J%2BsQ3UGEKj8gPN04tAQ6JDe1%2Bk7Z6oQSyN%2F2%2F3rfZKnLshfCjZw4JS2bHBIiuBmbe0LXRlZAIBu22syo5PtSA8y6nApmCHJRsx%2Fr4KhWRNMi2ZdZaUxafwwbJA8V%2F6LkhUYwMd4DDEH3rzXhtIvs7fMGkP6B8ejOQH74YxFK%2FkF1jiUaPlDdjyamTPx9wJDuHSKYZqJQZ2UnHw1ML8c7Og3ZJclPOIRUbOoUWDIeZYp5VpeAtAKwPnokUqLPIAf%2FIdH8fo3EyMOL0ya8GOp0BUrX7Wgb8LcmBL%2BrG0IkLutSlgjRr%2B7P6P%2FlmuHGuL5TVVc2En9l5Zd5dlGpPofF5AI9pmEarYByO1gVCOwW0yVenvCipyIXzAP9sVXIp9lgps1qawc1MmDiMVHvv3YcLxlet1UDi3f3HJ9Ln%2BYa9dnWlOKli68vkO8%2BQiEsgQzJ6athUcX99tTfH44lL2aYjJ4ILnXWUECygZNPxiw%3D%3D&X-Amz-Signature=fed0b311c26b0603813c9a065ba74900a3562857fcde66d01ce01ad74334fc96&X-Amz-SignedHeaders=host&x-id=GetObject"

그러면 이렇게 긴걸준다
이게 바로 흔히 다운로드 url이라고 불리는 S3 리소스를 Get가능한 Url이다
이걸 그대로 img의 scr에 넣거나 다운로드 하도록 사용하면 된다.

0개의 댓글