파일과 같은 데이터 업로드는 웹/앱에서 가장 많이 다루는 기능 중 하나이다. 사진, 동영상, 문서와 같은 미디어 파일을 업로드하는 프로세스는 주로 아래와 같다.
1. 사용자가 파일을 어플리케이션 서버에 업로드한다.
2. 어플리케이션 서버는 처리를 위해 업로드를 임시 공간에 저장한다.
3. 파일을 데이터베이스, 파일 서버 또는 영구 저장을 위한 개체 저장소로 전송한다.
보통 파일 업로드는 권한 설정 때문에 서버를 경유해야 한다. 영상 파일 같이 컨텐츠의 용량이 높은 경우, 서버에 넘겨준 후 서버에서 스토리지에 저장하는 이중 작업은 비효율적일 때가 있다.
네트워크 I/O 및 서버 CPU 사용량이 커지며 속도 지연을 일으킬 수도 있다는 것.👀
파일 크기가 크지 않더라도 서버에서 Multipart file을 받아 S3 버킷에 업로드하면 서버쪽에서 파일을 갖고 있어야 하는 자체로 리소스 낭비가 발생할 수 있다.
그렇다면 클라이언트에서 바로 업로드하면 되지 왜 굳이 서버를 거쳐서 업로드하는 걸까?
보안 때문이다.
정해둔 규칙 안에서 데이터가 관리되어야 하기 때문에 권한을 가진 사용자만 S3에 접근해야 한다. 이를 서버가 수행해주며 일종의 보안 절차 작업을 거치게 되는 것.👀
위와 같은 단점을 개선하여 서버단의 리소스를 사용하지 않고 클라이언트가 S3에 직접 파일을 업로드할 수 있는 + 보안 절차까지 보장되는 Presigned URL 방식을 알아보자.
직역하면 "미리 서명된 URL"을 의미한다. 서버에서 권한을 검증하여 나온 Presigned URL.
즉, 해당 URL은 이미 S3에 지급할 수 있는 권한을 가진 상태를 의미하기에, 서버에 거쳐 권한 검증을 할 필요가 없다.
1. Amazon API Gateway 를 호출하여, getSignedURL Lambda 함수 엔드포인트를 호출한다. 이를 통해 Signed URL을 얻을 수 있다.
2. 애플리케이션에서 S3 버킷으로 파일을 직접 업로드한다.
한 줄로 정리하자면,
필요에 따라 객체 소유자가 보안 자격 증명을 사용하여 일정 기간 동안 객체 접근을 허가하는 Pre-Signed URL을 만들어 다른 사용자와 객체를 공유할 수 있다는 것.
https://example-bucket.s3.amazonaws.com/images/2d098b12-5cd7-4f00-835b-a6c998a13617%EB%B8%94%EB%A1%9C%EA%B7%B8%20%EC%8D%B8%EB%84%A4%EC%9D%BC.png
?x-amz-acl=public-read
&X-Amz-Algorithm=AWS4-HMAC-SHA256
&X-Amz-Date=20230903T144326Z
&X-Amz-SignedHeaders=host
&X-Amz-Expires=120
&X-Amz-Credential=<ACCESS_KEY>/20230903/ap-northeast-2/s3/aws4_request
&X-Amz-Signature=<SIGNATURE_VALUE>
presigned url을 활용하여 업로드하고 리소스에 대한 GET 요청 시 access denied가 발생한다.
그러므로 presigned url 생성할 때 public read 권한을 설정해주어야 한다.
서명 버전과 알고리즘을 식별하고, 서명을 계산하는데 사용. 서명 버전 4를 위해서 “AWS4-HMAC-SHA256” 로 설정
날짜는 ISO 8601 형식
서명을 계산하기 위해 사용되어지는 헤더 목록. HTTP host 헤더가 요구
미리 선언된 URL이 유효한 시간 주기. 초단위. 정수 값. 최소 1에서 최대 604800 (7일)
AWS Identity and Access Management (IAM) 인스턴스 프로파일 : 최대 6시간 유효
AWS Security Token Service (STS): 최대 36시간 유효
IAM User: 최대 7일 유효(AWS v4 증명을 사용할경우)
액세스 키 ID와 범위 정보(요청 날짜, 사용하는 리전, 서비스 명). 리전 명은 리전 및 엔드포인트에서 확인 가능
요청을 인증하기 위한 서명
Presigned URL의 유효 기간이 지난 S3에 엑세스하면?
<?xml version="1.0" encoding="UTF-8"?>
<Error>
<Code>AccessDenied</Code>
<Message>Request has expired</Message>
<X-Amz-Expires>120</X-Amz-Expires>
<Expires>2023-09-03T14:45:26Z</Expires>
<ServerTime>2023-09-03T16:16:52Z</ServerTime>
<RequestId>Z3634PQ64CW2Y0EE</RequestId>
<HostId>giWKwpSWmbv62iwAeD4pCbjOsIOyMv4NC3eMTZUrNmgaiiNcEPg7utHibWwQyVWF6C75wisnbf4=</HostId>
</Error>
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'
S3 IAM 인증 키를 작성해준다.
# s3
cloud:
aws:
s3:
bucket: [bucket name]
stack.auto: false
region.static: ap-northeast-2
credentials:
accessKey: [access key]
secretKey: [secret key]
Presigned URL을 발급받기 위하여 S3 접근을 해야하므로 인증 정보를 설정한다.
@Configuration
public class S3Config {
@Value("${cloud.aws.credentials.access-key}")
private String accessKey;
@Value("${cloud.aws.credentials.secret-key}")
private String secretKey;
@Value("${cloud.aws.region.static}")
private String region;
@Bean
@Primary
public BasicAWSCredentials awsCredentialsProvider(){
BasicAWSCredentials basicAWSCredentials = new BasicAWSCredentials(accessKey, secretKey);
return basicAWSCredentials;
}
@Bean
public AmazonS3 amazonS3() {
AmazonS3 s3Builder = AmazonS3ClientBuilder.standard()
.withRegion(region)
.withCredentials(new AWSStaticCredentialsProvider(awsCredentialsProvider()))
.build();
return s3Builder;
}
}
Presigned URL을 발급받는 로직이다. 각 메서드마다 주석으로 설명을 달아놓았다.
@Service
@RequiredArgsConstructor
public class FileService {
@Value("${cloud.aws.s3.bucket}")
private String bucket;
private final AmazonS3 amazonS3;
/**
* presigned url 발급
* @param prefix 버킷 디렉토리 이름
* @param fileName 클라이언트가 전달한 파일명 파라미터
* @return presigned url
*/
public String getPreSignedUrl(String prefix, String fileName) {
if(ValidatorUtil.isNotEmpty(prefix)) {
fileName = createPath(prefix, fileName);
}
GeneratePresignedUrlRequest generatePresignedUrlRequest = getGeneratePreSignedUrlRequest(bucket, fileName);
URL url = amazonS3.generatePresignedUrl(generatePresignedUrlRequest);
return url.toString();
}
/**
* 파일 업로드용(PUT) presigned url 생성
* @param bucket 버킷 이름
* @param fileName S3 업로드용 파일 이름
* @return presigned url
*/
private GeneratePresignedUrlRequest getGeneratePreSignedUrlRequest(String bucket, String fileName) {
GeneratePresignedUrlRequest generatePresignedUrlRequest =
new GeneratePresignedUrlRequest(bucket, fileName)
.withMethod(HttpMethod.PUT)
.withExpiration(getPreSignedUrlExpiration());
generatePresignedUrlRequest.addRequestParameter(
Headers.S3_CANNED_ACL,
CannedAccessControlList.PublicRead.toString());
return generatePresignedUrlRequest;
}
/**
* presigned url 유효 기간 설정
* @return 유효기간
*/
private Date getPreSignedUrlExpiration() {
Date expiration = new Date();
long expTimeMillis = expiration.getTime();
expTimeMillis += 1000 * 60 * 2;
expiration.setTime(expTimeMillis);
return expiration;
}
/**
* 파일 고유 ID를 생성
* @return 36자리의 UUID
*/
private String createFileId() {
return UUID.randomUUID().toString();
}
/**
* 파일의 전체 경로를 생성
* @param prefix 디렉토리 경로
* @return 파일의 전체 경로
*/
private String createPath(String prefix, String fileName) {
String fileId = createFileId();
return String.format("%s/%s", prefix, fileId + fileName);
}
}
{
"code": "200",
"message": "요청 성공",
"data": "https://example-bucket.s3.amazonaws.com/images/2d098b12-5cd7-4f00-835b-a6c998a13617%EB%B8%94%EB%A1%9C%EA%B7%B8%20%EC%8D%B8%EB%84%A4%EC%9D%BC.png?x-amz-acl=public-read&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20230903T144326Z&X-Amz-SignedHeaders=host&X-Amz-Expires=120&X-Amz-Credential=<ACCESS_KEY>/20230903/ap-northeast-2/s3/aws4_request&X-Amz-Signature=<SIGNATURE_VALUE>"
}
참고
업로드 한파일을 Presigned URL를 통해 클라이언트가 볼수 있나요?