본문 바로가기
Spring Boot

[Spring Boot] aws s3 presignedUrl로 업로드 하는 법(One Time Token, OTT)

by 디토20 2022. 7. 29.
반응형

 

 

 

[Spring Boot] aws s3 presignedUrl로 업로드 하는 법

 

회사에서 이미지 업로드시 서버의 I/O 부하를 줄이기 위해 클라이언트에서 바로 aws의 S3에 업로드를 할 수 있도록 presignedUrl을 내리라는 미션을 받았다.

 

"만든 Url이 정상적으로 작동하는지는 프론트에서 확인을 해줘야하나요?"

라고 팀장님께 물으니, 프론트 서버도 직접 만들어 api 테스트 하고 머지하라는 팀장님의 말씀에.. 만만한 vuekotlin을 사용해서 aws s3 presignedUrl을 받아 이미지를 업로드 하고, 이미지를 받아오는 것까지 구현해보았다.

 

처음에 one time token = OTT 라는 키워드를 주셔서 검색을 했는데 잘 안나왔다. 그래서 이리저리 보니 서명된 url이라는 키워드가 있어서 타고타고 presignedUrl을 발견했다.(실제로는 one time token은 아니고 토큰 만료시간을 짧게 주는 느낌)

 

presignedUrl을 스프링부트(코틀린, 자바)에서 만들어서 프론트에 내려서 이미지를 업로드하는 코드가 거의 전무하고, 설명도 제대로 된게 없어서 나름 삽질을 했던 터라, 도움이 되었으면 하는 의미로 작성하는 포스팅.

 

 

1. S3 만들기

S3 - 버킷 만들기

버킷 이름을 지정

 

 

 

프론트에서 aws s3 버킷에 직접 접근으로 이미지를 받아올 예정이기 때문에, 퍼블릭 액세스를 열어준 후 생성한다.

 

 

 

생성된 s3 버킷을 클릭하고 권한을 눌러서 두가지 설정을 해준다.

 

 

1.1 Get, Put 오픈

버킷 정책에 위의 코드를 적어준다. 해당 코드를 적지 않으면 이후에 403 (Forbidden)를 만나게 된다.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": "*",
            "Action": [
                "s3:Put*",
                "s3:Get*"
            ],
            "Resource": "arn:aws:s3:::s3-upload-test-ditto/*"
        }
    ]
}

 

 

1.2 CORS 방지

아래에 CORS 에러를 방지하기 위한 설정을 해준다.

[
    {
        "AllowedHeaders": [
            "*"
        ],
        "AllowedMethods": [
            "HEAD",
            "GET",
            "PUT",
            "POST"
        ],
        "AllowedOrigins": [
            "*"
        ],
        "ExposeHeaders": [
            "ETag"
        ]
    }
]

해당 설정을 하지 않으면 이미지 업로드 시 아래와 같은 에러 메세지가 뜬다.

from origin 'http://localhost:8080' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.

 

 

 

2. IAM 사용자 추가

IAM - 사용자 추가

 

 

사용자 이름 작성 후 액세스 키 - 프로그래밍 방식 액세스 클릭 후 다음

 

 

기존 정책 직접 연결에서 AmazonS3FullAccess 를 연결해준 뒤 사용자를 만들어 준다.

 

 

사용자가 만들어지면 액세스 키 ID비밀 액세스 키를 저장해 둔다.(실제 키는 외부에 노출되어서는 안됨)

 

 

 

 

3. Spring Boot 코드 짜기(kotlin)

@Configuration
class AmazonS3Config {

    private val accessKey: String? = "AKIAS6GKCXMUCZ3OVHJD"

    private val secretKey: String? = "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"

    private val clientRegion: String? = "ap-northeast-2"

    val bucketName: String? = "s3-upload-test-ditto"

    @Bean
    fun amazonS3(): AmazonS3 {
        val awsCredentials = BasicAWSCredentials(accessKey, secretKey)
        return AmazonS3ClientBuilder.standard()
            .withCredentials(AWSStaticCredentialsProvider(awsCredentials))
            .withRegion(clientRegion)
            .build()
    }
}

우선 amazonS3 config 클래스를 만들어준다.

실제 accessKey ~ bucketName 은 application.properties에서 관리하나, 포스팅 코드의 직관성을 위해 하드코딩으로 넣어두었다. 해당 정보들을 이용해 AmazonS3에 접근할 수 있는 크레덴셜을 만들어 빈으로 등록해둔다.

 

 

 

@Service
class AmazonS3TestService(
    private val amazonS3Config: AmazonS3Config
) {
    fun getPreSignedUrl(fileName: String): Map<String, Serializable>? {
        val encodedFileName = "${fileName}_${LocalDateTime.now()}"
        val objectKey = "test/${encodedFileName}"

        val expiration = Date()
        var expTimeMillis: Long = expiration.time
        expTimeMillis += (3 * 60 * 1000).toLong() // 3분
        expiration.time = expTimeMillis // url 만료 시간 설정

        val generatePresignedUrlRequest: GeneratePresignedUrlRequest =
            GeneratePresignedUrlRequest(amazonS3Config.bucketName, objectKey)
                .withMethod(HttpMethod.PUT)
                .withExpiration(expiration)

        return mapOf(
            "preSignedUrl" to amazonS3Config.amazonS3().generatePresignedUrl(generatePresignedUrlRequest),
            "encodedFileName" to encodedFileName
        )
    }
}

service 클래스를 만들어 준다. 프론트에서 fileName을 받아야 하므로 fileName을 파라미터로 받아준다. 파일 이름은 중복될 수 있으므로 파일 이름 뒤에 현재 시간을 추가해준다.

 

preSignedUrl에서의 objectKey는 s3에 실제 저장될 경로다.(처음에 objectKey가 뭔지 몰라서 많이 헤맸다). 

위처럼 "test/fileName"으로 설정하면 s3 버킷에 test 폴더를 자동으로 만들고, 그 안에 파일을 저장하게 된다. 만든 preSignedUrl과 변경된 fileName을 프론트로 던져준다.

 

 

 

 

4. Front 코드 짜기(Vue)

<template>
  <div>
    <div>
      <input type="file" @change="uploadFile" ref="file">
      <img :src="uploadedUrl"/>
    </div>

  </div>
</template>

<script>
import axios from "axios";

export default {
  name: 'FileUploadByS3',
  data() {
    return {
      file: null,
      preSignedUrl: null,
      encodedFileName: null,
      uploadedUrl: null
    }
  },
  methods: {
    uploadFile() {
      this.file = this.$refs.file.files[0];
      axios.get("http://localhost:1443/s3/preSignedUrl", {params: {fileName: this.file.name}},)
      .then((res) => {
        this.preSignedUrl = res.data.preSignedUrl
        this.encodedFileName = res.data.encodedFileName
        this.uploadImageToS3(this.preSignedUrl, this.file)

      })
    },
    uploadImageToS3(preSignedUrl, file) {
      axios.put(preSignedUrl, file)
      .then(() => {
        this.uploadedUrl = "https://s3-upload-test-ditto.s3.ap-northeast-2.amazonaws.com/test/" + this.encodedFileName
      });
    },
  }
}
</script>

우선 브라우저에서 파일을 선택 하게 되면 uploadFile() 메소드에서 서버에 fileName을 던지며 preSignedUrl을 달라는 요청을 보내게 된다. 요청을 보내면 응답값으로 preSignedUrl과 파일 이름 뒤에 날짜가 추가된 FileName을 받게된다.

 

그럼 받은 preSingedUrl에 PUT 메소드로 file을 함께 넘겨주면 업로드가 완료되고, 업로드 된 이미지 경로는

폴더가 있다면 -> https://버킷네임.리전.amazonaws.com/폴더명/파일이름 

폴더가 없다면 -> https://버킷네임.리전.amazonaws.com/파일이름 

이 되어 바로 가져다가 사용할 수 있다.

 

 

 

 

5. 혹시나 아래와 같은 에러가 난다면?

Failed to load: resource: net::ERR_CERT_COMMON_NAME_INVALID

처음에 fileName을 가져와 그대로 업로드를 시켰더니 위와 같은 에러 메세지가 자꾸 출력되어 폭풍 구글링을 했는데 해도 답이 없어서 주변 개발자한테 물어보니, 파일 이름이 한글이라 그런것일 수도 있으니 인코딩을 해보라는 답을 받았다. base64로 인코딩해서 보냈더니 제대로 업로드가 되었다.

 

근데 이상한게, 자꾸 저 에러가 나서 base64로 인코딩해서 업로드 하고 프론트에서 파일 이름이 필요하면 base64로 디코딩해서 썼는데, 블로그 포스팅 하려고 인코딩 없이 새로 만들어 보니 또 업로드가 잘된다....

 

 

그래서 업무 코드도 인코딩을 빼봤는데 업로드가 또 잘 됨....

뭐지?!

 

(수정됨)

**해결**

버킷 이름에 .이 들어가면 Failed to load: resource: net::ERR_CERT_COMMON_NAME_INVALID 오류가 발생한다. 버킷 이름에 .이 들어갈 경우 postman은 잘 되는데 구글은 안된다던가, 구글은 되는데 네이버 웨일은 안된다던가 하는 문제가 발생할 수 있으니 버킷이름에 .은 지양하도록 하자

 

728x90
반응형

댓글