S3 CORS 헤더 관련 이슈 해결방법 (html2canvas, lottie)

김세환·2021년 3월 1일
25

AWS S3

AWS S3는 Simple Storage Service를 줄여 S3라고 부른다.
이 S3는 여러 용도로 쓰이는데 대표적으로 아래 세가지 용도로 주로 사용된다.

  1. 정적 리소스 저장용 (웹 하드 느낌?)
  2. 정적 웹 페이지 및 콘텐츠 호스팅
  3. 정적 리소스 서버로 활용

이번에 해결한 이슈는 3번 케이스와 같이 사용하다가 발생한 이슈이다.

CORS 란?

우선 CORS 라는 개념을 알고 가야 한다.

CORS란 Cross Origin Resource Sharing 을 의미한다.
우리말로 번역해보면 교차 출처 리소스 공유라고 번역 할 수 있다.

CORS는 웹 브라우저에서 보안상의 이유로 도입되었는데. 현재 사용자가 접속한 웹 애플리케이션이 다른 출처의 리소스를 불러올 때, Access-Control-Allow-Origin 헤더를 보내주지 않으면 브라우저가 그 리소스를 거부하는 보안 정책(?) 이라고 할 수 있다.

풀어서 쉽게 설명하면 다음과 같다.

현재 유저는 https://aaaaaa.com를 브라우저를 통해 접속했다.
https://aaaaaa.com은 별도의 API 및 리소스 서버를 두어서, 웹 애플리케이션에 필요한 이미지나 텍스트들을 https://bbbbbb.com 에서 불러온다고 해보자.

즉 현재 유저가 보고있는 웹 애플리케이션의 도메인과, 실제 리소스를 불러와서 사용할 서버의 도메인이 다르기때문에 웹 브라우저는 Access-Control-Allow-Origin 헤더가 리소스 서버의 응답에 적절히 들어있지 않다면 사용하기를 거부한다.

개발하다 한번쯤은 겪는 CORS 에러...

이 에러를 해결하기 위한 방법은 매우 간단한데, 응답을 보내주는 서버측에서 Access-Control-Allow-Origin 헤더에 적절한 값을 담아서 보내주면 된다.

이런식으로 서버측에서 access-control-allow-origin 헤더에 적절한 응답을 넣어주면 된다.

보통은 개발의 편의성을 위해 access-control-allow-origin : * 을 넣어주는 편인데, 어떤 Origin에서 요청하든 허용하겠다는 의미이다. (localhost:3000 이든,, IwillHackYou.com 같은 위험해 보이는 도메인이든..)

S3 CORS 활성화

S3를 AWS 웹 콘솔로 배포 할 경우와, serverless 프레임워크를 이용할 경우 두가지 케이스에 대해서 설명한다.

AWS 웹 콘솔에서 S3 CORS 활성화

우선 CORS를 활성화 하고자 하는 S3 버킷을 AWS 웹 콘솔을 통해 들어간다.

위 사진은 실제 이번 DND 4기 9조 사이드 프로젝트를 하면서 사용한 실제 S3 Static Resource 서버용 버킷이다.

위 사진에서 권한 부분을 클릭하여 들어간다.

밑으로 쭉 내리다 보면 CORS(Cross-origin 리소스 공유) 부분이 보인다. 이 부분을 편집을 눌러 다음과 같이 수정한다.

[
    {
        "AllowedHeaders": [
            "*"
        ],
        "AllowedMethods": [
            "GET",
            "HEAD"
        ],
        "AllowedOrigins": [
            "*"
        ],
        "ExposeHeaders": [
            "x-amz-server-side-encryption",
            "x-amz-request-id",
            "x-amz-id-2"
        ],
        "MaxAgeSeconds": 3000
    }
]

위 설정은 모든 출처에 대해서 (*) 리소스 공유를 허용한다는 내용의 설정이다.

Serverless.yml 에서의 설정

service: dnd-4th-9-seeat-image

provider:
  name: aws
  stage: ${opt:stage, self:custom.defaultStage}
  stackName: ${self:service}-${self:provider.stage}
  imageBucket: ${self:service}-${self:provider.stage}-image-bucket

custom:
  custom.defaultStage: dev

resources:
  Description: "seeat image bucket stack"
  Resources:
    imageBucket:
      Type: AWS::S3::Bucket
      Properties:
        BucketName: ${self:provider.imageBucket}
        AccessControl: Private
        CorsConfiguration:
          CorsRules:
            - AllowedOrigins:
                - '*'
              AllowedHeaders:
                - '*'
              AllowedMethods:
                - GET
                - HEAD
              MaxAge: 3000
    imageBucketAllowPublicPolicy:
      Type: AWS::S3::BucketPolicy
      Properties:
        Bucket: ${self:provider.imageBucket}
        PolicyDocument:
          Version: "2012-10-17"
          Statement:
            - Effect: Allow
              Action:
                - "s3:DeleteObject"
                - "s3:GetObject"
                - "s3:ListBucket"
                - "s3:PutObject"
                #버킷에 객체를 삽입, 삭제, 리스트 가능한 권한 설정.
              Resource:
                - arn:aws:s3:::${self:provider.imageBucket}
                - arn:aws:s3:::${self:provider.imageBucket}/*
                #버킷내 모든 리소스를 누구나 접근 가능
              Principal: "*" #모든 사용자가 접근 가능하도록 정의
          

위에서 CorsConfiguration: 부분을 잘 확인하여 동일하게 넣어서 Serverless를 활용해 배포하면 된다.

Serverless 프레임워크에 대해서는 추후 자세히 포스팅 할 예정이니 이 부분이 이해가 가지 않는다면 넘어가도 좋습니다!

그래도 남은 이슈들..

위 과정을 따라서 CORS 정책을 정의했다고 해서 html2canvas나 혹은 lottie와 같은 것들을 바로 사용 가능한 상태는 아니다.

S3버킷은 access-control-allow-origin 헤더를 특정 상황에서만 반환하는데..

클라이언트의 리소스 요청(request)에 Origin 헤더가 있어야만 access-control-allow-origin 헤더를 내려보내준다!

위 사진은 Origin 헤더 없이 요청한 응답의 결과. access-control-allow-origin 헤더가 응답에 포함되어 있지 않다.

위 사진은 Origin 헤더를 추가해서 요청한 응답의 결과. access-control-allow-origin 헤더가 응답에 포함되어있다.

따라서 대부분의 경우는 이슈가 없겠지만, html2canvas 등의 패키지를 클라이언트측에서 s3에 저장된 resource를 재구성 할 경우 문제가 생긴다.

https://github.com/dnd-side-project/dnd-mentee-4th-9-repo/issues/182

위는 실제 사이드 프로젝트를 하면서 cors 에러가 해결되지 않아 처리했었던 이슈

S3에서 받아온 이미지를 canvas로 재구성 하면서 디스크에 캐싱된 이미지의 응답에 access-control-allow-origin이 없어서 발생한 에러 캡쳐 사진

이 부분을 해결하기 위해 많은 구글링을 하였는데, 구글aws는 서로 각각 본인들의 문제가 아니라고 주장한다고 하더라...

풀어서 설명하자면 받아온 이미지를 canvas로 재구성하면서 이미 한번 내려받은 이미지를 재사용하는데, 이 이미지를 받을 당시의 응답에 access-control-allow-origin이 없고, canvas로 재구성하면서 cors 정책을 위반한다고 브라우저는 판단하고 cors 에러를 내뱉는다.

이 이슈를 해결하기 위해서는 서버측에서 access-control-allow-origin 헤더를 내려주어야 하는데 문제는 클라이언트 측에서 이미지 리소스를 받을 때 Origin 헤더를 담아서 보내지 않는다.

따라서 우리는 origin 헤더를 어떻게든 보내서 리소스 응답에 access-control-allow-origin 헤더를 받아와야 한다.

Cloud Front를 활용한 이슈 해결

그래서 우리는 cloud front를 활용해서 이러한 이슈를 해결하기로 했다.

cloud front는 aws에서 제공하는 CDN서비스로, 사용자의 요청과 가장 가까운 Edge 컴퓨터가 응답을 해주는 서비스인데, 캐싱정책을 통해 원본 서버에 요청하지 않고 캐싱된 응답을 내려줘 서버의 부하를 낮추고, 응답 속도를 획기적으로 증가시키기도 하는 서비스이다.

cloud frontS3를 함께 사용하게 되면 요청이 흐르는 과정은 다음과 같이 바뀐다.

사용자의 요청 -> cloud front -> S3 서버

이와 더불어 cloud front에서 임의로 헤더를 설정해서 원본 서버로 내려줄 수 있는데, 그래서 나는 cloud front에서 강제로 모든 요청에 대해서 Origin 헤더가 있는 것 처럼 S3 서버를 속이기로 했다.

Cloud Front에 대한 자세한 설명은 추후에 포스팅할 예정

위 사진의 맨 아래쪽을 주의해서 보자. Origin Custom Headers라는 부분에 강제로 origin : "*"이라는 헤더를 S3에 보내도록 해놓았다.

따라서 Cloud front를 통해 S3에 요청을 하게 되면 access-control-allow-origin 헤더를 무조건 받을 수 있게 된다.

다만 여기서도 제약사항이 있었는데, 헤더의 이름을 Origin으로 설정하려고 하면 AWS에서 해당 헤더이름은 사용 불가능하다고 막는다. 따라서 소문자로 origin으로 설정하여 보내고 있다.

cloud front의 도메인(xxxx.cloudfront.net)으로 클라이언트에서 Origin 헤더 없이 요청해보고, access-control-allow-origin이 받아지는지 테스트 해보았다.

결과는 성공!

따라서 이제 클라이언트 측에서 Origin 헤더를 굳이 담아서 보내지 않더라도, S3를 Cloud front를 이용해 속여서(?) access-control-allow-origin 헤더를 받아 낼 수 있었다.

우리는 사이드 프로젝트에서 Domain을 구입했기 때문에, cloud front 도메인을 https://resources.seeat-plant.com 과 연결하여 좀 더 이쁜 도메인으로 사용중이다.

resources.seeat-plant.com으로 오는 요청을 cloud front 도메인으로 리다이렉션 하도록 Route53 설정을 변경하였다.

이로서 어디서 어떻게 요청하든, cloud front가 임의로 origin헤더를 만들어서 S3에 넘겨주기 때문에 응답에는 무조건 access-control-allow-origin이 생기게 된다.

이로서 S3에 저장된 리소스를 활용해 html2canvas 혹은 lottie를 재구성해서 사용해도 더이상 브라우저는 CORS에러를 내뱉지 않을것이다.

마치며

사이드 프로젝트를 통해 정말 많은 이슈와 부딪혔고 이런 이슈들을 해결하기 위해 정말 많은 삽질을 했다. 하지만 이런 과정을 통해 정말 많이 배우고 성장했다.

너무 좋은 사이드프로젝트 팀원들과 함께했기 때문에 더 재밌게, 더 많이 성장한 느낌?

우리가 완성한 사이드 프로젝트는 https://www.seeat-plant.com에서 확인 할 수 있다.

profile
DevOps 엔지니어로 핀테크 회사에서 일하고있습니다. 아직 많이 부족합니다.

3개의 댓글

comment-user-thumbnail
2021년 12월 5일

잘 읽었습니다! :)

답글 달기
comment-user-thumbnail
2022년 2월 16일

안녕하세요. 저도 회사에서 세환님과 똑같은 문제를 겪었는데, 세환님 글이 많은 도움이 되었습니다. 클라우드 프론트가 아닌 다른 html2canvas(라이브러리 코드 수정) + jspdf(옛날 버전 사용)으로 문제를 해결했습니다. 가뭄의 단비와 같은 글 작성해주셔서 감사합니다!

1개의 답글