mason
mason-log.

GRG Games 랭킹 SDK.

mason

masonfe-hyunsu

🧵 1. 개요.

GRG Games는 브라우저에서 바로 도는 HTML5 미니게임(설치 없이 웹에서 짧게 즐기는 작은 게임)을 한곳에 모아 둔 플랫폼이다.

목록에서 게임을 선택하면, 브라우저에서 바로 플레이되고, 점수 있는 게임은 기록이 랭킹에 남는다.

그 개인 랭킹을 등록하려고, 게임 제작자가 쓰기 편하게 JS SDK를 제공할 수 있게 준비했다.

GRG Games 서비스 대표 이미지

게임 HTML에는 스크립트랑 종료 훅만 남기고, 인증이랑 검증은 플랫폼이 맡는다. 연동은 개발자 가이드에 정리해 뒀다.

이 포스팅에서는 그 랭킹 SDK랑 보안 경계를 정리해 본다.

한 줄로 요약하면, 점수는 게임이 아니라 플랫폼이 연 플레이 세션(한 판을 서버가 식별하는 기록)으로 받는다.


🎯 2. 간편한 SDK 구성.

제작자가 개인 랭킹을 붙일 때, 게임이 끝나면 점수만 넘기면 끝나게 짜고 싶었고, 그래서 구성은 스크립트 한 줄이랑 종료 훅 하나다.

  • 스크립트 한 줄: https://grggames.com/grg-games-sdk.js

  • 종료 훅 하나: GRGScore.onGameEnd(score)

  • 점수 타입: Number만. 문자열, Infinity, NaN, null은 막는다

  • 허용 범위: -999,999 ~ 999,999,999,999

골프 타수나 클리어 시간처럼 낮은 숫자가 더 좋은 게임도 있다. 그런 경우는 최종 score에 * -1을 붙여 음수로 넘기라고 했다.

랭킹 정렬은 항상 “큰 값이 위”로 두고, 규칙 차이는 부호로 흡수하는 쪽이 단순했다.

<script src="https://grggames.com/grg-games-sdk.js"></script>
// 게임 종료 시 Number 타입 score 제출
GRGScore.onGameEnd(score);

게임 로직은 게임 쪽에, 스코어 파이프라인(점수 검증, 인증, 전송, 랭킹 반영)은 플랫폼 쪽에 둔다.

게임 HTML은 iframe으로 띄우고, 점수는 부모 페이지(게임을 감싼 호스트)가 플레이 세션으로 넘긴다.

게임 → SDK → 부모 페이지 → 서버 세션

🛡️ 3. 클라이언트 조작과 보안.

점수는 브라우저에서 올라오니 클라이언트 조작을 많이 고민했고, 서버가 그 값만 믿고 가면 안 됐다.

그래서 게임에 넘기던 토큰(클라이언트가 들고 다니는 인증값)을 iframe에 주지 않기로 했고, 브라우저에 들어간 비밀값은 탈취된다고 봤다.

그래서 경계를 이렇게 나눴다.

  • 게임: GRGScore.onGameEnd(score)로 점수만 보내고, 토큰은 읽거나 전달하지 않는다.

  • 부모 페이지(게임을 iframe으로 감싼 호스트): 로그인 유저 기준으로 서버에 플레이 세션(한 판을 서버가 식별하는 기록)을 연다. 세션 ID는 부모와 서버에만 두고, 게임 iframe URL, localStorage, DOM에는 넣지 않는다.

  • 서버: POST /api/scores만 점수를 받는다. player_scores 직접 write는 막았다.

클라이언트 SDK만으로 치트를 끝낼 수는 없다.

이상한 타입, 말도 안 되는 값, 세션 없는 제출부터 걸러 둔다. 다만 점수를 클라이언트가 정해서 보내는 구조라, 로그인한 사람이 세션만 연 뒤 가짜 점수를 넣는 것까지는 막지 못한다.


⏱️ 4. Rate Limit과 Cross-Origin.

같은 세션에서 점수를 연타하거나, 게임이 아닌 곳에서 점수를 직접 쓰는 것도 막아야 했다.

예전엔 로그인만 되면 DB에 upsert(있으면 갱신, 없으면 삽입)가 가능했다.

그래서 Rate Limiting(짧은 시간에 같은 요청을 너무 자주 못 보내게 막는 제한)이랑 출처 검사를 같이 걸었다.

  • 속도: 1초당 1회

  • 일일 상한: 100회

  • 초과 시: 거절한다

  • 제한은 SDK만이 아니라 서버 play_sessions에서도 본다

  • 세션 시작 후 약 2초 안에는 제출이 안 된다 (min_play_ms, 게임별로 더 늘릴 수 있음)

  • 게임별 max_score가 있으면 그 상한을 넘긴 점수는 거절한다

Cross-Origin 보안(허용된 출처에서 온 요청인지 보는 경계)도 같이 묶는다.

  • 부모가 받는 건 게임이 호스팅된 도메인에서 온 postMessage뿐이다

  • 부모 페이지 origin에서 DevTools로 postMessage를 보내는 위조는 받지 않는다

  • 실제 DB 기록은 서버 API + 본인 세션이 있을 때만 된다

세션, 검증, 속도 제한, 출처 검사 레이어

📐 5. 이 구성의 한계.

SDK는 붙이기 쉽게 짰다. 조작이 그냥 된다는 뜻은 아니고, 막는 것과 못 막는 것이 갈린다.

개발자 가이드도 SDK를 넣고 onGameEnd만 호출하면 끝나게 짧게 잡아 뒀다.

막는 것과 못 막는 것을 나눠 보면 이렇다.

  • 점수는 게임이 보낸 값을 받는다. 서버가 플레이를 다시 보고 가짜를 걸러 내진 않아서, 로그인한 사람이 세션을 연 뒤 점수를 부풀리는 것까지는 막지 못한다.

  • 인증값을 게임에 넣으면 빼갈 수 있어서, 세션은 부모 페이지와 서버에만 둔다. 게임에서 쓰는 API는 그대로라 이미 붙인 게임은 고칠 필요가 없다.

  • 점수가 진짜인지까지 보려면, 같은 조작이면 결과가 같은 게임만 골라 서버에서 다시 돌려 봐야 한다. HTML5 iframe에 올린 게임을 전부 그렇게는 못 한다.

  • 그래도 점수를 부풀리면 티가 나는 지점은 상위 1~7등 보상이랑 홈의 Top Player 목록이다. 게임마다 점수 상한은 이미 있고, 비정상적으로 높은 점수는 나중에 보류하는 쪽이 다음이다.

  • 요청을 너무 자주 보내는 건 Rate Limit으로 줄인다. 다만 재도전을 많이 하는 장르면 하루 상한을 다시 봐야 한다.

이상한 값, 세션 없는 제출, 게임 밖에서 DB를 직접 쓰는 건 막는다. 이번 서버 세션 업데이트로 막은 것도 게임에서 훔친 값으로 DB에 쓰는 길이다. 로그인한 사람이 세션을 연 뒤 점수를 부풀리는 것까지는 이번에도 못 막는다. HTML5 미니게임 랭킹에서는 그 선을 일단 받아들였다.


🔗 6. 참고 자료.

이전 글
i18n으로 글로벌 언어 처리.
다음 글
HTTP 캐시.

댓글

불러오는 중...
목록으로