본문 바로가기
포트폴리오/개인 자작

[2인 프로젝트] 방탈출 소개 사이트 (feat 예약 시스템, 서명 쿠키 방식)

by jamong1014 2025. 2. 11.
반응형

 

프로젝트 주제 : 온라인 방탈출을 소개해주는 사이트 제작

도메인: https://foolblack.net

개발 기간 : 2주

본인 파트 : 인프라 구축 및 웹 프론트/백엔드

사용 기술 : AWS Serverless(API Gateway(Rest API), Lambda, Dynamodb(Stream, TTL), CloudWatch(EventBridge), S3, Cloudfront(뷰어 액세스 제한), Route53), EC2, VPC(NAT, IGW), ALB, ASG, SES

 

인프라 구조


예약 시스템이 들어간 방탈출 소개 사이트를 만든 이유?

사실 예약 시스템은 필요 없다.

온라인 방탈출을 하는데 예약이라는 개념이 들어가는 순간 언제 어디서든 플레이할 수 있는 온라인이라는 매리트가 없어지기 때문에 예약 시스템은 사실 불필요한 기능이다.

 

단순 여러 가지 솔루션을 다뤄보고 싶기에 인프라를 통해 예약 시스템을 개발해보고 싶었다.

또한 서버리스로 개발한 것이 아닌 EC2 서버(Spring 개발)를 올려 개발을 한 것이기에 비용적인 부분에 있어서 인프라 구축만 하고 배포는 하지 않았다.

 

포트폴리오용으로 제작한 것이기 때문에 블로그 포스팅용으로 제작한 것.

https://foolblack.net 사이트는 단순 서버리스로 제작된 예약시스템이 존재하지 않은 상태이다.


예약 시스템 작동 원리

 

예약 탭을 통해 예약을 할 수 있다.

일주일 간격으로 테마 예약이 열리며, 하루 기준 예약은 총 30명까지 제한, 자신이 원하는 예약 날짜를 선택하면 몇 명이 예약되어 있는지 확인할 수 있다.

 

하루 예약으로 30명으로 제한한 이유는 게임의 테마들, 볼륨이 많이 커질 시 게임의 트래픽 또한 많이 몰릴 것이기에 그거에 대한 사전 방지인 셈이다.

(사실 이것도 SQS 등으로 처리할 수 있고, 람다 함수의 동시성 처리 또한 1000개까지 가능하다.)

 

그냥 나름대로의 시나리오를 짜서 솔루션을 구축한 거라고 보면 된다.

 

 

 

정보를 입력하고 예약을 하게 되면, 랜덤 예약 ID가 발급되고 이걸 통해서 예약 조회 또한 가능하다.

여기서 예약된 정보가 DynamoDB에 저장되고, Stream을 통해 람다 함수가 작동하여 SES 솔루션을 통해 본인이 입력한 이메일로 예약 확인 메일이 전송된다.

 

 

예약 조회가 이루어지면 오른쪽 사진처럼 예약 내역을 볼 수 있다.

여기서 '접속' 버튼이 있고, 예약한 날짜가 되면 CloudWatch(EventBridge)을 통해 람다 함수가 작동이 되어 버튼 활성화 데이터가 들어가고, 접속 버튼을 누를 수 있는 상태로 변한다.

 

여기서 의문점이 들 수 있다.

접속 버튼이고 뭐고 그냥 방탈출 할 수 있는 사이트로 들어가면 되는 거 아니냐?

저렇게 예약해서 접속하는 게 무슨 의미가 있냐?

라고 할 수 있다.

 

그래서 인증된 사용자만 들어갈 수 있게끔 개발한 솔루션이 서명 쿠키 방식이다.

그렇게 하기 위해선 뷰어 액세스 제한 기능을 활성화시켜야 하는데 이거는 CloudFront 서명된 URL 또는 서명된 쿠키를 사용하여 콘텐츠 접근을 제한할 때 사용하는 설정이다.

뷰어 액세스 제한 기능 활성화

 

from datetime import datetime, timedelta
import rsa
import base64
import json

# RSA 서명 함수 (SHA-1 사용)
def rsa_signer(message):
    with open('private_key.pem', 'rb') as key_file:
        private_key = rsa.PrivateKey.load_pkcs1(key_file.read())
    signature = rsa.sign(message.encode('utf-8'), private_key, 'SHA-1')  # SHA-1 사용
    return base64.b64encode(signature).decode('utf-8')

# Lambda 핸들러 함수
def lambda_handler(event, context):
    # CORS 헤더 설정
    cors_headers = {
        "Access-Control-Allow-Origin": "*",  # 모든 도메인 허용 (필요 시 특정 도메인으로 제한 가능)
        "Access-Control-Allow-Methods": "POST, GET, OPTIONS",
        "Access-Control-Allow-Headers": "*",
    }

    # 클라이언트에서 보낸 요청 본문 확인
    try:
        body = json.loads(event['body'])
    except Exception as e:
        return {
            'statusCode': 400,
            'headers': cors_headers,
            'body': json.dumps('Bad Request: Invalid body')
        }
    
    # 키 검증 (sh8932jh7937이 아닌 경우 403 Forbidden 응답)
    key = body.get('key')
    if key != "sh8932jh7937":
        return {
            'statusCode': 403,
            'headers': cors_headers,
            'body': json.dumps('Forbidden: Invalid key')
        }

    # CloudFront 서명 쿠키 생성 정보
    key_pair_id = ''  # CloudFront 키 페어 ID
    cloudfront_domain = ''

    # 만료 시간 설정 (현재 시간 기준 1시간 후)
    expires = int((datetime.utcnow() + timedelta(hours=1)).timestamp())

    # 서명 정책 생성
    policy = {
        "Statement": [
            {
                "Resource": f"{cloudfront_domain}/*",
                "Condition": {
                    "DateLessThan": {"AWS:EpochTime": expires}
                }
            }
        ]
    }

    # 서명 정책을 JSON으로 직렬화하고 Base64 인코딩
    policy_json = json.dumps(policy)
    policy_base64 = base64.b64encode(policy_json.encode('utf-8')).decode('utf-8')

    # 서명 생성
    signature = rsa_signer(policy_json)

    # 서명된 쿠키 생성
    cookies = {
        "CloudFront-Policy": policy_base64,
        "CloudFront-Signature": signature,
        "CloudFront-Key-Pair-Id": key_pair_id
    }

    # 성공적으로 쿠키 생성 시 응답 반환
    return {
        "statusCode": 200,
        "headers": cors_headers,  # CORS 헤더 추가
        "body": json.dumps(cookies)
    }

 

서명된 쿠기 값을 생성할 수 있는 람다 코드이다.

클라이언트 측에서 API Gateway를 통해 서명된 쿠키값을 가져와 간단한 웹 스크립트를 통해 쿠키를 설정하여 접속할 수 있는 것이다.  

 

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Escape Room</title>
</head>
<body>
<script>
    // URL 파라미터에서 쿠키 값을 추출
    const urlParams = new URLSearchParams(window.location.search);
    const policy = urlParams.get("CloudFront-Policy");
    const signature = urlParams.get("CloudFront-Signature");
    const keyPairId = urlParams.get("CloudFront-Key-Pair-Id");

    // escape.foolblack.net 도메인에 쿠키 설정
    const domain = "foolblack.net";
    document.cookie = `CloudFront-Policy=${policy}; path=/; domain=${domain}; secure`;
    document.cookie = `CloudFront-Signature=${signature}; path=/; domain=${domain}; secure`;
    document.cookie = `CloudFront-Key-Pair-Id=${keyPairId}; path=/; domain=${domain}; secure`;

    // 설정이 완료되면 escape.foolblack.net 홈으로 이동
    window.location.href = "https://escape.foolblack.net/";
</script>
</body>
</html>

 

escape.foolblack.net (방탈출 게임 사이트)는 foolblack.net의 서브 도메인이기 때문에 foolblack.net의 쿠키를 설정하게 되면 서브 도메인 또한 설정이 가능하다.


 

이 포스팅은 기본적인 서버에 대한 인프라 이해도와 그 외 여러 가지 솔루션을 보여주기 위함으로 제작하였기 때문에 억지스러운 시나리오를 통해 제작한 감이 없지 않아 있다.

 

덕분에 사용해보지 않은 솔루션과 방식들을 이용해 봐서 공부가 많이 됐었다.

 

반응형