ecsimsw

CloudFront signed url, 인증된 사용자에게만 자원을 응답한다 본문

CloudFront signed url, 인증된 사용자에게만 자원을 응답한다

JinHwan Kim 2024. 4. 10. 16:10

문제 사항

PICUP은 사용자 사진, 동영상을 저장하고 읽는 스토리지다. 기존 PICUP의 파일 읽기 구조는 아래와 같다. 사용자가 WAS에 자원을 요청하면 WAS 에선 토큰을 확인해 사용자의 자원 액세스 권한을 확인하고 파일 시스템으로 실제 파일을 읽어와 반환한다. 중간 WAS 는 오롯이 권한 확인의 용도일 뿐이었다.

 

 

 

사용자가 파일을 요청할 때마다 WAS 는 같은 파일이라도 매번 Disk I/O 를 사용해 파일을 로드할 것이다. 메모리 캐시를 사용하자니 크기가 큰 동영상이나 이미지 파일을 올리는 데는 적합하지 않아, 근본적인 해결 방법은 안된다고 생각했다. 

 

html, js, css 나 FE의 에셋들과 같은 다른 정적 자원처럼, CDN이나 웹 서버의 캐싱을 사용해 파일 요청에 대한 WAS의 부하를 분산하고, 파일을 직접 파일을 읽어 반환하는 경우를 피하고 싶었다. 그러면서도 공유 URL 이 아닌, 사용자가 해당 자원에 대한 권한이 있는지 확인할 수 있는 방법을 고민했고, 아래와 같은 구조를 만들 수 있었다.

 

 

이 글에선 CloudFront 에서 요청의 권한을 확인할 수 있는 두 가지 방법을 소개하고, PICUP에서 선택한 방법과 처리 흐름을 설명하려 한다.

 

CloudFront function 을 사용한 토큰 인증

CDN 을 처음 공부할 때 AWS 정적 자원과 함께 Lambda function 도 올릴 수 있다고 봤었다. 이걸 이용하면 요청을 선처리 할 수 있지 않을까라는 방향으로 공부했고, CloudFront function 을 사용해서 JWT 토큰을 확인하는 방법을 찾을 수 있었다. 공식 문서 에서도 잘 설명되어 쉽게 따라 할 수 있었다. 

 

https://dev.to/haintkit/cloudfront-with-jwt-authentication-46dh

 

JWT 토큰을 읽어 토큰 유효성과 payload 를 가져올 수 있다. 토큰 페이로드에서 유저 정보를 확인하고, 요청 자원 url에 포함된 사용자 정보와 일치하는지 확인하여 1. 인증된 사용자가, 2. 본인의 자원에 접근하려 하는지를 검증할 수 있었다.

 

Function 을 정의하기만 하면 요청을 쉽고 빠르게 선처리할 수 있지만, JWT 토큰을 쿠키에 저장하는 경우 Domain 정보나 HttpOnly 옵션에 따라 쿠키가 제대로 넘어가는지, Function 에서 그 값을 가져올 수 있는지 같은 제약이 있다. 무엇보다, 이런식의 권한 확인이 가능했던 이유는 요청하는 URL 안에 자원의 사용자 정보가 명확한 상황뿐이다.

 

예를 들어 요청하는 자원의 경로가 "https://d1mx51dsfeok14i.cloudfront.net/users/243/my-image.jpg" 이런 식이고 User 정보 243 이 JWT 페이로드에 들어있다면 이 방식이 가능하다. 그렇지 않고 URL 에 권한을 확인할 수 있는 사용자 정보가 없거나 여러 사용자가 동시에 권한을 갖고 있는 자원은 권한 확인이 불가능할 것이다.

 

쉽고 빠르지만, 한계가 명확하다. 

 

CloudFront signed url

Signed url 을 사용하면 자원에 접근할 수 있는 권한을 확인할 수 있다. CDN 에 Public key 를 등록해 두고, 인증된 사용자가 요청하는 URL을 Private 키로 암호화해 두는 방식이다. 암호화된 Url 으로 자원을 요청했을 때 CloudFront 는 Public 키로 요청 URL이 올바르게 인증된 자원인지 확인한다. URL 암호화에 권한이 포함된 자원 정보, IP 범위, 권한 유효 시작 시간, 권한 유효 종료 시간를 지정할 수 있다. 

 

PICUP 을 예시로, FE에서 사진 파일 정보를 BE 에서 요청하면, BE는 파일 주소를 암호화하여 Signed Url 을 반환한다. FE 에서 이 Signed Url 에 요청하게 되면 CDN 을 통해 원하는 파일을 얻을 수 있게 된다. 암호화 과정에서 요청 IP를 제한하면 해당 URL 은 외부인이 사용할 수 없고, 유효 기간을 제한하면 유효 시간 전후의 요청에선 자원을 정상 응답하지 않는다.

 

Signed url 생성 - AWS  SDK Java V2

CloudFront 에서 public key 등록 방법은 공식 문서 또는 간단한 캡처본 를 참고한다. 

 

Java 에서 Signed URL 을 만들기 위해선 aws-sdk-java-v2 가 필요하다. 이때 공식 문서에서 소개하는 " 'software.amazon.awssdk:aws-sdk-java:2.X.X'"가 아니라 아래처럼 필요한 모듈만 가져오길 추천한다. 앞선 aws-sdk-java:2.x.x 은 모든 AWS 리소스를 다루기 위한 의존성을 가져온다. 이 글에서 다루는 S3 와 Cloudfront signed url 을 위해선 아래 두 의존성이면 충분하다.

dependencies {
    implementation 'software.amazon.awssdk:s3:2.25.27'
    implementation 'software.amazon.awssdk:cloudfront:2.25.27'
}

 

아래 코드로 Signed url 을 만들 수 있다. 사전 준비로 필요한 요소는 CloudFrontKeyPairId 와 CloudFront 도메인 주소, public 키와 함께 생성된 private key의 로컬 경로이다. 예시에서 생성된 Url 은 7일 동안, /my-image.jpg 자원에 한하여 유효하다. 

 

var cloudFrontKeyPairId = 0;
var cloudFrontDomainName = "";
var privateKeyPath = "";
var resourcePath = "/my-image.jpg";

var sign = CannedSignerRequest.builder()
    .privateKey(Path.of(privateKeyPath))
    .resourceUrl(new URL("https", cloudFrontDomainName, resourcePath).toString())
    .keyPairId(cloudFrontKeyPairId)
    .expirationDate(Instant.now().plus(7, ChronoUnit.DAYS))
    .build();
var signedUrl = cloudFrontUtilities.getSignedUrlWithCannedPolicy(sign);
return signedUrl.url();

 

CloudFrontKeyPairId 는 CloudFront -> 퍼블릭 키에서 생성 또는 확인할 수 있다.

 

 

CustomSignerRequest 를 사용하면 접근 가능한 자원에 와일드카드를 사용하거나, IP 범위, 유효 시작 시간을 지정할 수 있다. ( 공식 문서 )

 

var sign = CustomSignerRequest.builder()
    .privateKey(Path.of(PRIVATE_KEY_PATH))
    .ipRange("0.0.0.0")
    .resourceUrl(new URL(CDN_PROTOCOL, CLOUD_FRONT_DOMAIN_NAME, "/users/1/*").toString())
    .keyPairId(publicKeyId)
    .activeDate(Instant.now())
    .expirationDate(Instant.now().plus(EXPIRATION_AFTER_DAYS, ChronoUnit.DAYS))
    .build();
var signedUrl = cloudFrontUtilities.getSignedUrlWithCustomPolicy(sign);
return signedUrl.url();

 

캐싱

매번 자원의 url 을 새로 암호화하면 암호화 시간도 문제겠지만, 매번 바뀌는 Url 에 Content-Cache 정책이 적용되지 않는다.

 

@Cacheable(value = SIGNED_URL, key = "{#remoteIp, #originUrl}")
public String sign(String remoteIp, String originUrl) {

 

 Picup 에서는 url 암호화에 사용자 remote ip 를 사용하여 외부인이 접근하지 못하도록 막는데, {ip:url}으로 암호화된 url 을 캐싱하여 암호화 시간을 제거하고 Content-Cache 를 사용할 수 있도록 하였다.

 

 

그래서 바뀐 구조

- 이미지, 동영상 파일 요청을 더이상 WAS에서 처리하지 않는다. WAS 부하를 낮추고, Disk I/O 를 줄인다.

- 파일 CDN URL에 유효 기간을 둘 수 있다.

- CDN 이 아닌, S3 버킷에 직접 요청을 막는다.

- 인증된 사용자 외, 다른 외부인이 파일 URL에 접속해도 자원을 얻을 수 없다.

 

 

암호화

- 암호화에는 RSA 를 사용한다.

- 암호화에 필요한 시간은 생각보다 크지 않다.

- 애플리케이션에서 처음 Signed url 을 만드는데만 시간이 걸리고 (16ms), 그 이후부터는 1ms, 0ms 가 소요된다. 

signed url duration : 16ms
signed url duration : 5ms
signed url duration : 6ms
signed url duration : 3ms
signed url duration : 2ms
signed url duration : 2ms
signed url duration : 2ms
signed url duration : 1ms
signed url duration : 2ms
signed url duration : 2ms
signed url duration : 1ms
signed url duration : 1ms
signed url duration : 0ms
signed url duration : 0ms
signed url duration : 0ms
signed url duration : 0ms
signed url duration : 0ms
signed url duration : 0ms
signed url duration : 1ms
signed url duration : 1ms
signed url duration : 1ms
Comments