AWS CloudFront Signed Cookie 적용하기 (Spring)

김해찬·2025년 6월 4일
post-thumbnail

AWS CloudFront Signed Cookie란?

  • 사용 이유 : S3에 저장된 자원에 대해 접근을 제한해야 하기 위해
  • 장점 : CloudFront(CDN)를 이용하기 때문에 S3만 이용할 때 보다 더 최적화된 리소스 접근을 할 수 있다. Signed Cookie를 통해 사용자 접근에 대한 제어를 백엔드 프로그램에서 할 수 있다.
  • Signed Cookie를 사용하면 접근 경로를 *을 통해 표시할 수 있다. -> 다중 자원에 대한 권한을 처리할 수 있음. 반면에 SignedUrl은 자원 각각에 따라 접근권한을 처리해주어야한다. SNS 서비스 특성상 한 게시물에 여러개의 이미지가 포함되어 있으므로 postId에 종속적으로 S3 패키지 구조를 생성하여 특정 게시물에 속한 이미지에 권한을 부여하여 한번에 처리할 수 있다.

Signed? (서명된)

  • 서명된은 무슨 의미일까? 공개키 암호 방식에서 개인키를 이용하여 암호화한 방식을 의미한다.
    • 검증 가능한 공개키를 공유함으로써 개인키의 주인이 암호화(서명)한 파일임을 확인할 수 있다.
  • Signed Cookie / URL에서는 S3 자원에 대해 접근 권한이 명시된 정책의 진위성을 확인하기 위해 쓰인다.

정책(Polcy)

https://docs.aws.amazon.com/ko_kr/AmazonCloudFront/latest/DeveloperGuide/private-content-setting-signed-cookie-custom-policy.html#private-content-custom-policy-statement-signed-cookies-examples

{
    "Statement": [
        {
            "Resource": "https://*",
            "Condition": {
                "IpAddress": {
                    "AWS:SourceIp": "192.0.2.10/32"
                },
                "DateGreaterThan": {
                    "AWS:EpochTime": 1357034400
                },
                "DateLessThan": {
                    "AWS:EpochTime": 1357120800
                }
            }
        }
    ]
}
  • 위는 AWS 공식문서에 명시된 예제이다. Resource, Condition 등 접근 권한에 필요한 속성을 사용한다.
  • 시간은 EpochTime을 사용하여 TimeZone에 독립적인 장점이 있다.
  • 위 데이터를 Base64방식으로 Encoding하여 서명하면 Signed Policy를 구할 수 있다. 이를 공유하여 백엔드 서비스에서 주관하는 이미지 리소스 제어를 강제할 수 있다!

어떤 정책을 쓸까?

  • 정책은 Custom Policy와 Canned Policy가 있다. 쉽게 말해 사용자 설정 정책과 기본 정책인데 다중 리소스 명시를 위해 *를 사용하려면 Custom Policy를 사용해야한다.

키 발급을 받자!

  • openssl 라이브러리를 이용한 shell 프로그래밍으로 생성할 수도 있지만 Root 계정의 보안자격증명에서 CloudFront의 키를 발급받아 사용할 수도 있다.
  • 발급받은 키는 파일로 다운받아서 CloudFront에 등록하자(키관리> 키그룹, 퍼블릭 키)

Spring을 통해 구현해보자!

공식문서에 어느정도 나와있다. 그렇지만 Custom Policy를 사용하는 것은 설명이 친절하지 않으며 AWS Jdk 버전에 따라서 그 방식도 상이하다. 그래도 몇가지 포인트만 확인하면 쉽게 잘 구현할 수 있을 것이다.

private final CloudFrontUtilities cloudFrontUtilities = CloudFrontUtilities.create();

Instant now = Instant.now();
		Instant expireDate = now.plusSeconds(2 * 60 * 60); // 2시간 동안 유효

		CustomSignerRequest request;
		try {
		request = CustomSignerRequest.builder()
			.resourceUrl(resourcePathPattern) // https://~~~~~~~~~~.cloudfront.net/images/post/* 와 같은 패턴
			.resourceUrlPattern(resourcePathPattern) // 이부분은 사실 필요없다... 코드 내부를 까보면 resourceUrl로 대체된다.
			.privateKey(Paths.get(privateKeyPath))   // private key 경로이다. .pem파일도 가능하며 절대경로로 지정해야한다.(배포 환경에 따른 환경변수 처리 필수)
			.keyPairId(keyPairId) // key-pair-id 아니다.. 공개키의 ID이다. AWS 콘솔에서 확인할 수 있다.
			.expirationDate(expireDate)
			.activeDate(now.minusSeconds(5 * 60)) // 5분 전부터 유효
			.ipRange("0.0.0.0/0") // ip 제한하려면 이 부분을 조절하면 된다.
			.build();
		} catch (Exception e) {
			throw new RuntimeException("Failed to create CustomSignerRequest: " + e.getMessage(), e);
		}

CookiesForCustomPolicy cookiesForCustomPolicy = cloudFrontUtilities.getCookiesForCustomPolicy(request);

// 아래와 같은 방식으로 쿠키에 필요한 3가지 값을 확인할 수 있다.
// 쿠키에 바로 사용가능한 Key=Value 형식으로 작성되어있다.
cookiesForCustomPolicy.policyHeaderValue(); // 정책을 Base64로 인코딩한 값
cookiesForCustomPolicy.signatureHeaderValue(); // 서명값
cookiesForCustomPolicy.keyPairIdHeaderValue(); // 공개키 ID 값

여기에서 화나는 포인트가 있다..

  1. keyPairId는 키 그룹 ID가 아니라 공개키의 ID를 의미하는 것이다.
    -> 저는 그것을 확인하지 못해서 계속 403에러를 받았습니다..
  2. resourceUrlPattern에는 값을 안넣어도 된다.
    -> resourceUrl에 CloudFront의 루트 도메인 경로를 명시하고 resourcePathPattern에 path를 명시하면 된다고 생각했지만 resourcePathPattern은 signedCookie방식에만 쓰이고 실제로 pattern이 포함된 문자열을 resourceUrl에 넣어야 한다.

실습

  • 여기에서도 아쉬운 점이 있다. CloudFront와 Backend의 도메인이 달라서 쿠키를 발급해도 공유하지 못한다. Backend에서 발급한 쿠키를 CloudFront의 도메인으로 설정한 후 사용해야한다. 실제 서비스에서는 CloudFront에 도메인을 연결하여 사용하면 될 듯 하다.
profile
성실히 열심히 즐겁게

0개의 댓글