Spring Boot에서 S3 Presigned URL 생성 방법

개발하는 구황작물·2023년 12월 4일
2

회사 프로젝트

목록 보기
2/8

회사에서 프로젝트 진행 중 이미지를 저장하고 조회하기 위해 S3를 사용하기로 하였다.

그러나 보안상의 이유료 presigned-url을 활용하기로 하였다.

Presigned URL

S3의 Bucket Policy나 acl 같은 권한 설정과 관계없이 특정 유효기간에 S3에 put, get이 가능하게 하는 URL이다.

버킷을 public access로 열어놓으면 보안 상 문제가 발생할 수 있고

클라이언트 -> 서버 -> S3로 업로드 혹은
S3 -> 서버 -> 클라이언트로 다운로드를 진행하기 보단 presigned URL을 통해 클라이언트 <-> S3로 바로 업로드/다운로드를 하여 서버의 리소스를 절약할 수 있는 장점이 있다.

SpringBoot에서 presigned URL 생성하기

※ S3 버킷 생성 이전에 IAM 계정을 생성해야 합니다.
IAM 권한 설정 시 S3FullAccess을 추가해주어야 합니다.

  1. S3 bucket 생성

버킷 만들기에 들어가 줍니다.

S3 region, 이름 입력 후 버킷 만들기를 클릭합니다.

이후 생성한 버킷에 들어간 후 권한에 들어가서 버킷 정책과 CORS 정책을 설정해주어야 합니다.

  • Principal: 본인이 허락할 IAM의 ARN 입력, 여러명일 경우 ,로 구분하여 입력, 전체 허용시 * 입력
  • Action: 버킷에 허용할 Action 설정 (ex : putObject, GetObject...)
  • Amazon Resource Name(ARN): 본인 버킷의 ARN 입력 (ex : arn:aws:s3:::{본인 버킷 이름}/* )

이후 Add statement 클릭 후 Generate Policy 클릭하면

아래와 같이 정책이 나오는데 이를 복사하여

정책 화면에 붙여넣기 하면 됩니다.

CORS 정책

  1. Spring boot

application.yml

spring:
  cloud:
    aws:
      s3:
        credentials:
          accessKey: ${AWS_S3_ACCESSKEY}
          secretKey: ${AWS_S3_SECRETKEY}
        bucket: ${AWS_S3_BUCKET}
        bucket.url: ${AWS_S3_BUCKET_URL}
      region:
        static: ${AWS_REGION}
      stack:
        auto: false

이때 accessKey와 secretKey는 퍼블릭 레포에 올리면 안된다
만약 올렸다면 레포를 전체 지우고 다시 올려야 한다.(기록이 남기 때문에...)

accessKey와 secretKey를 application.yml에 올리지 않고도 사용하는 방법은 여러가지가 있으나 여기에선 환경변수를 설정하는 방법을 사용하겠다.

다른 좋은 방법에 대해서는 여기로

accessKey와 secretKey를 각각 AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEY 로 환경변수를 설정해주면 EnvironmentVariableCredentialsProvider 에 주입된다.

이를 S3Config에 활용하면 된다.

@Configuration
public class AWSS3Config {  

    @Bean
    public S3Client s3Client() {
        return S3Client.builder()
                .credentialsProvider(EnvironmentVariableCredentialsProvider.create())
                .region(Region.AP_NORTHEAST_2)
                .build();

	}
    
    @Bean
    public S3Presigner presigner() {
        return S3Presigner.builder()
                .credentialsProvider(EnvironmentVariableCredentialsProvider.create())
                .region(Region.AP_NORTHEAST_2)
                .build();
    }

}

AwsService

@RequiredArgsConstructor
@Component
public class AwsService {
    private final S3Client s3Client;
    private final S3Presigner presigner;

    public String getPresignUrl(String filename) {
        if(filename == null || filename.equals("")) {
            return null;
        }

        GetObjectRequest getObjectRequest = GetObjectRequest.builder()
                .bucket(bucketName)
                .key(filename)
                .build();

        GetObjectPresignRequest getObjectPresignRequest = GetObjectPresignRequest.builder()
                .signatureDuration(Duration.ofMinutes(5)) // presignedURL 5분간 접근 허용
                .getObjectRequest(getObjectRequest)
                .build();

        PresignedGetObjectRequest presignedGetObjectRequest = presigner
                .presignGetObject(getObjectPresignRequest);
                
        String url = presignedGetObjectRequest.url().toString();

        presigner.close(); // presigner를 닫고 획득한 모든 리소스를 해제
        return url;
    }

}

AController


@Slf4j
@RequiredArgsConstructor
@RequestMapping("/abc")
@RestController
public class AController {
    private final AwsService awsService;

    @GetMapping("/file/{filename}")
    public ResponseEntity<String> getFile(@PathVariable(value = "filename") String fileName) throws IOException {

        String url = awsService.getPresignUrl(fileName);

        return new ResponseEntity<>(url, HttpStatus.OK);
    }

}

결과

profile
어쩌다보니 개발하게 된 구황작물

0개의 댓글