Cloudfront에 signed cookie로 access 하기
Cloudfront에서 특정 유저만 url에 접근하도록 하고 싶은 경우 사용할 수 있는 방법은 크게 3가지가 있다.
만약 프론트엔드가 url에 접근하려 한다면 signed url과 pre-signed url을 이용하는 방법이 있는데 pre-signed url을 이용할 경우 cognito를 이용해야 한다는 번거로움이 있다. 또한, S3의 특정 디렉토리에 있는 여러 개의 파일을 가져와야 하는데 이를 위해서는 signed cookie가 필요하다. 우리는 여러 개의 파일에 접근해야 하기 때문에 백엔드에서 signed cookie를 받아와 이를 프론트엔드에 전달해 주는 작업을 하려고 한다. 과정은 다음과 같다.
다음과 같은 방법으로 public key와 private key를 다운 받아 key pair를 생성할 수 있다(aws 공식 문서 참고).
private_key.pem
파일에 저장한다. openssl genrsa -out private_key.pem 2048
private_key.pem
파일에서 public_key.pem
파일로 공개키를 추출한다. openssl rsa -pubout -in private_key.pem -out public_key.pem
CloudFront > Public key > 퍼블릭 키 생성
에서 public key를 등록한다.cat public_key.pem
으로 나온 내용을 복붙한다.boto3 패키지에서 signed cookie 생성은 지원이 안된다.... 그렇기 때문에 직접 코드를 짜야 한다. 그러나 여러 사람들이 짜 놓은 코드들이 있었는데 이를 가져와서 조금의 변형을 거쳐 코드를 완성했다. 참고한 소스 코드 링크는 여기 있다.
수정한 코드
get_signed_cookies
함수만 호출하면 된다.CloudFront-Policy
, CloudFront-Signature
, CloudFront-Key-Pair-Id
class SignedCookieGenerator:
"""
클라이언트에서 cloudfront로부터 원하는 이미지를 가져오기 위해 필요한 signed cookie를 생성한다.
"""
@staticmethod
def _replace_unsupported_chars(some_str: str) -> str:
"""Replace unsupported chars: '+=/' with '-_~'"""
return some_str.replace("+", "-").replace("=", "_").replace("/", "~")
@staticmethod
def _in_an_hour() -> int:
"""Returns a UTC POSIX timestamp for one hour in the future"""
return int(time.time()) + (60 * 60)
@staticmethod
def rsa_signer(message: bytes, key: bytes) -> bytes:
"""
Updated version
Based on https://boto3.readthedocs.io/en/latest/reference/services/cloudfront.html#examples
"""
private_key = serialization.load_pem_private_key(
key, password=None, backend=default_backend()
)
signature = private_key.sign(message, padding.PKCS1v15(), hashes.SHA1())
return signature
def generate_policy_cookie(self, url: int) -> tuple:
"""Returns a tuple: (policy json, policy base64)"""
# expires in an hour
policy_dict = {
"Statement": [
{
"Resource": url,
"Condition": {
"DateLessThan": {"AWS:EpochTime": self._in_an_hour()}
},
}
]
}
# Using separators=(',', ':') removes seperator whitespace
policy_json = json.dumps(policy_dict, separators=(",", ":"))
policy_64 = str(base64.b64encode(policy_json.encode("utf-8")), "utf-8")
policy_64 = self._replace_unsupported_chars(policy_64)
return policy_json, policy_64
def generate_signature(self, policy: json, private_key: bytes):
"""Creates a signature for the policy from the key, returning a string"""
sig_bytes = self.rsa_signer(policy.encode("utf-8"), private_key)
sig_64 = self._replace_unsupported_chars(
str(base64.b64encode(sig_bytes), "utf-8")
)
return sig_64
@staticmethod
def generate_cookies(policy: json, signature: str, cloudfront_id: str) -> dict:
"""Returns a dictionary for cookie values in the form 'COOKIE NAME': 'COOKIE VALUE'"""
return {
"CloudFront-Policy": policy,
"CloudFront-Signature": signature,
"CloudFront-Key-Pair-Id": cloudfront_id,
}
def generate_signed_cookies(
self, url: int, cloudfront_id: str, private_key: bytes
) -> dict:
policy_json, policy_64 = self.generate_policy_cookie(url)
signature = self.generate_signature(policy_json, private_key)
return self.generate_cookies(policy_64, signature, cloudfront_id)
def get_signed_cookies() -> dict:
"""
How to get pem keys
1. private_key with RSA
$ openssl genrsa -out private_key.pem 2048
2. public_key from private_key
$ openssl rsa -pubout -in private_key.pem -out public_key.pem
Signed Cookie consists of 3 keys
"""
CLOUDFRONT_URL = settings.CLOUDFRONT_URL
CLOUDFRONT_PUBLIC_KEY_ID = os.getenv("CLOUDFRONT_PUBLIC_KEY_ID")
signed_cookie_generator = SignedCookieGenerator()
path_to_pem_key = os.path.join(settings.BASE_DIR, "private_key.pem")
with open(path_to_pem_key, "rb") as f:
cookies = signed_cookie_generator.generate_signed_cookies(
url=CLOUDFRONT_URL,
cloudfront_id=CLOUDFRONT_PUBLIC_KEY_ID,
private_key=f.read(),
)
return cookies
프론트엔드에 signed cookie를 전달해 주기 전에 실제로 원하는 url에 접근하려 할 때 signed cookie를 이용할 수 있는 지를 테스트 해 보았다.
테스트 코드는 다음과 같다.
import requests
from utils.aws import get_signed_cookies # 위에서 생성한 get_signed cookies 함수 임포트(utils/aws.py 파일에 생성함)
def images_request_test() -> None:
"""
signed cookie를 이용해 cloudfront에 요청시 status code 200이 오는지 테스트 하기 위한 스크립트
문제 발생시를 대비해 임시로 사용할 예정
"""
cloudfront_url = CLOUDFRONT_URL
cookies = get_signed_cookies()
print("signed cookie 정보", cookies)
images = [
image_id_1, image_id_2, ...
]
try:
print("요청을 시작합니다.")
responses = []
for image in images:
key = f"test/{image}.png"
url = f"{cloudfront_url}/{key}"
response = requests.get(url, cookies=cookies)
print(response.status_code)
responses.append(response.status_code)
print(responses)
except Exception as e:
print(e)
print("끄읕")
문제가 있어서 검색하다 아티클이있어서 질문때문에 댓글 답니다.
혹시 아래의 문제에 대해서 해결하셨으면 정보 고유 부탁드릴수있을까해서 글 남깁니다.
프론트크라우드설정을 다했습니다.
import requests를 사용하구 쿠키를 적용하면 인식이 잘됩니다.
크롬에서 단독 url 에 쿠키를 생성하고 값을적용하면 또한 잘작동합니다.
장고같은 어플리케이션에서 쿠키를 적용하고
html 내부안에 img src='크라우드프론트url'/
적용한 경우 인식을 하지 않습니다. 403 값을 리턴합니다. cors 문제같기도 하고 잘모르겠습니다.
혹시 html 에서 적용하는 방법아실까요?
사인된 url 은 성공했습니다. ts 파일로 쪼개져있는 동영상 파일을 스트리밍하려고 하는데 단독파일보다는 쿠키가 적합한거 같아서 적용해보려고합니다.