AWS CloudFront - SignedURL 관련 CORS 에러

 

에러 (error)

Access to XMLHttpRequest at 'URL A' from origin 'URL B' hash been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource

 

[Browser Console Log]

 

[참고]

XMLHttpRequest ? Web Browser가 Web Server로 페이지 전환 없이 데이터를 요청하기 위한 방법을 제공하는 Web API이다.

Access-Control-Allow-Origin ? Web Server의 Http Response Header에 포함된 필드 중 하나로, 접근 허용할 Origin들이 포함된다.

CORS ? Cross-Origin Resource Sharing의 약자로, 서버에서 자신의 Resource를 다른 Origin들과 공유할지 여부를 설정할 때 사용된다.

 

 

환경 (environment)

OS : macOS

Browser : Chrome

Server : Go

Client : ReactJS

 

 

상황 (Situation)

개발 중인 Server에서 CloudFront Signed URL을 생성한 후, 개발 중인 Client에서 해당 URL을 받아서 접근하려고 했다. 이때, Signed URL 에 포함되는 정책 내용으로는 특정 시간 동안만 제한하는 기능을 사용하도록 하였다. 아래는 실제로 사용된 코드의 일부분이고, 코드를 따라 최종으로 생성된 Signed URL은 Client가 Signed URL이 만들어진 생성시점부터 1시간 동안 접근이 가능하게 된다. 하지만, Client에서는 Server로부터 받은 해당 Signed URL을 통해 접근을 시도했지만, 수십번을 다시 시도해도 위처럼 CORS 에러가 발생하였다. 

// go language

import (
	cf "github.com/aws/aws-sdk-go/service/cloudfront/sign"
)

now := time.Now()
policy := &cf.Policy{
   Statements: []cf.Statement{
      {
         Resource: rawURL,
         Condition: cf.Condition{
            DateGreaterThan: cf.NewAWSEpochTime(now),
            DateLessThan: cf.NewAWSEpochTime(now.Add(1 * time.Hour)),
         },
      },
   },
}

signer := cf.NewURLSigner("Key ID", "Private Key")
signedURL, err := signer.SignWithPolicy(rawURL, policy)

 

 

원인  (Cause)

Server의 시간과 CloudFront의 시간의 기준이 동일하지 않았다는 것이 주요 원인이다. 만약, 개발 중인 Server와 CloudFront가 동일한 시스템 내에서 실행하고 있다면 시간이 완전히 일치할 것이다. 하지만, 지금 개발 환경을 보면 Server는 로컬에서 개발되고 있고, CloudFront는 AWS 위에서 운영되고 있다. 다시 말해서 서로 다른 시스템 위에서 실행되고 있기 때문에, 두 시스템 간의 시간도 조금은 차이가 있을 수 있는 것이다. 실제로 테스트한 결과 약 1초 이내의 차이가 나는 것을 확인할 수 있었고, 이 때문에 에러가 발생하고 있는 것이었다.

 

정리하면, 현재 Server보다 CloudFront가 조금 더 빠른 시간으로 세팅되어 실행되는 상황이었고, 이 상황에서는 Server 가 생성한 Signed URL의 접근 가능한 시작시간(=DateGreaterThan)이 CloudFront에서는 아직 지나가지 않은 시간일 수 있는 상황이 발생할 수 있다는 것이다. 예를 들어, Server 시스템의 시간이 CloudFront 시스템의 시간보다 3초가 빠르다면 아래와 같은 상황이 발생할 수 있다는 것이다.

 

Server 시스템의 현재 시간 CloudFront 시스템의 현재 시간
2021-01-14 14:00:15 <-- Server는 이때부터 ~ 2021-01-04 14:00:12 <-- 이때 접근하면 에러 발생 
2021-01-14 14:00:16
2021-01-04 14:00:13 <-- 이때.. (이때 접근해서 에러 발생)
2021-01-14 14:00:17 2021-01-04 14:00:14 <-- 이때 접근하면 에러 발생 
... 2021-01-14 14:00:15 <-- Client는 이때부터 ~
2021-01-14 15:00:15 <-- 이때까지 유효한 Signed URL 을 생성 2021-01-14 14:00:16
2021-01-14 15:00:16 2021-01-14 14:00:17
2021-01-14 15:00:17  ...
2021-01-14 15:00:18  2021-01-14 15:00:15 <-- 이때까지 CloudFront로 접근이 가능

 

위 표에서 나타난 것처럼 Server의 시간이 14:00:15 일때, CloudFront의 시간은 14:00:12 일 수 있다는 것이다. 그리고 해당 Signed URL을 사용해서 재빠르게 CloudFront로 접근을 시도한다면 유효시간이 아니기 때문에, 아래 그림처럼 실패나는 상황이 만들어지게 되는 것이다.

 

 

[ error 발생해서 실패한 응답을 받는 상황 ]

 

 

추가로, 현재 에러는 정확히 따지자면 CORS 관련 에러가 아닌, CloudFront에서 Policy 내용을 검증하다가 발생한 에러이다. 현재, CloudFront 에서는 HTTP Status Code 403 과 Response 로 Access Denied 라는 XML 데이터를 반환하고 있었지만, Chrome Browser Console 에서는 이러한 메시지가 나오지 않고 위 처럼 CORS 에러를 자동으로 내뱉고 있을 뿐이다. 그래서 실제로, 현재 문제를 해결하기 위해 CORS 부분만을 의심했었고, 이 때문에 CloudFront 설정, S3 설정, Client 요청코드에서 의심되는 부분들만을 검토하면서 원인을 파악하는데 시간을 많이 버렸었다. 참고로, CloudFront 에서 실패가 발생해서 403 코드와 Acces Denied 가 반환되는 내용은 Chrome Browser로는 확인이 되지 않기 때문에, 로컬에 별도의 proxy program(ex, mitmproxy)를 세팅한 후, 이를 통해 교환되는 메시지를 보고 확인을 했다.

 

 

해결 (Solution)

현재 상황을 해결하기 위한 방법이 아래처럼 몇가지 있을 수 있다. 

  • 시스템 시간을 직접 조정

    • Server 시스템 시간을 CloudFront 시스템 시간보다 앞땅기는 것

    • CloudFront 시스템 시간을 Server 시스템 시간보다 뒤로 미루는 것

  • Signed URL 의 정책시간을 조정

    • DateGreaterThan 시간을 두 시스템 간의 차이만큼 앞땅기는 것

    • DateGreaterThan 시간을 사용하지 않는 것

사실, 시스템이 서로 독립적이라면 시간의 차이는 (밀리초, 마이크로초 단위로) 조금이라도 분명 존재할 수 밖에 없다. 그리고 현재 개발 중인 프로그램은 Server에서 Signed URL로 접근하는 시점이 Signed URL을 생성한 직후부터 1시간으로 제한할 것이기 때문에, 이전의 시간까지 CloudFront에서 반드시 검증할 필요는 없다. 따라서, 위에 소개한 방법들 중에 4번째 방법처럼 접근 가능을 위한 시작 시간을 사용하지 않도록 DateGreaterThan 항목을 제거함으로서 해결하였다.

 

 

[ error 없이, 성공적인 응답을 받는 상황 ]

 

 

 

참고 (Reference)

AWS CloudFront 서명된 URL 사용하기 Link