울면서개발하기
VARENA 개발일지

마을 통합 로그인 붙이기 — 입장권 확인, 서명형 세션 표, iframe 허용 제한

· 커밋 9개

같은 도메인의 그래픽 채팅 마을 VUTOPIA 에서 게임으로 넘어올 때, 마을 계정으로 바로 들어오게 하는 작업을 했다. 마을이 발급한 입장권을 VARENA 서버가 확인하고, 확인되면 이름 뒤에 인증 표시를 붙인다. 오후에는 마을 안 패널에 게임을 띄울 수 있게 iframe 허용 범위를 정했고, 마지막으로 게임 세션 표를 서버가 기억하지 않아도 되는 서명형으로 바꿨다.

한 일

마을 입장권으로 들어오기

  • 마을에서 게임으로 넘어올 때 주소에 입장권이 붙어 온다. 화면은 이걸 읽자마자 주소에서 지운다(방 코드·게임 방식 같은 다른 값은 그대로 둔다)
  • 입장 화면의 이름칸은 마을 닉네임으로 잠기고 “마을 계정 ✔” 표시가 나온다
  • 서버가 입장권을 확인하면 이름 뒤에 “ ✔” 를 붙인다. 일반 닉네임에 체크 표시 비슷한 글자를 넣어 흉내 내지 못하게, 일반 이름에서는 그런 글자를 지운다
  • 인증이 안 되면 다시 시도하지 않고 일반 닉네임으로 이어 가며 짧은 안내만 띄운다
  • 입장권과 세션 표는 로그에 남기지 않는다. 페이지에는 no-referrer 를 걸어 주소가 다른 곳으로 새지 않게 했다

게임 현황 API

  • 지금 접속자 수와 방 수를 돌려주는 작은 공개 API 를 만들었다. 캐시하지 않고, IP 별로 호출 횟수를 제한한다

마을 패널에서만 iframe 허용

  • 모든 HTTP 응답에 Content-Security-Policy: frame-ancestors 를 붙인다. 기본은 자기 자신만이고, 설정한 출처(마을 패널)만 추가로 허용한다
  • 설정값의 출처 모양이 틀리면 서버를 시작하지 않는다. 잘못 쓴 설정이 조용히 “아무 데서나 허용”이나 “아무 데서도 안 됨”이 되는 것을 막으려는 것이다

세션 표를 서명형으로

처음에는 입장권을 확인한 뒤 서버 메모리에 세션 표(12시간)를 두었다. 그런데 이러면 배포로 서버를 다시 띄울 때마다 표가 사라지고 마을 인증이 풀린다. 그래서 세션 표 자체에 내용과 서명을 담는 방식으로 바꿨다.

기술 메모

입장권 확인은 서버만 한다

화면도 입장권의 내용(이름)을 읽기는 하지만 입장 화면에 보여 주는 용도일 뿐 믿지 않는다. 실제 확인은 서버가 마을과 함께 정한 규격의 순서대로 한다. 대상(aud)이 이 게임인지, 서명이 맞는지, 만료되지 않았는지, 이미 한 번 쓴 입장권(jti)이 아닌지를 본다. base64url 도 정규형인지 확인한다. 같은 바이트를 다른 글자로 쓸 수 있으면 “한 번만 쓰기” 검사를 빠져나갈 여지가 생기기 때문이다.

서명 비교는 Node 의 crypto.timingSafeEqual 로 한다. 이 함수는 길이가 다르면 예외를 던지므로 길이를 먼저 비교한다.

import { createHmac, timingSafeEqual } from 'node:crypto';

function sign(secret: string, data: string): string {
  return createHmac('sha256', secret).update(data).digest('base64url');
}

function sameText(a: string, b: string): boolean {
  const x = Buffer.from(a);
  const y = Buffer.from(b);
  return x.length === y.length && timingSafeEqual(x, y);
}

리뷰 뒤에는 서명을 바이트가 아니라 base64url 글자 그대로 비교하도록 맞췄다. 마을·게임 등 이 규격을 쓰는 저장소들이 같은 문구로 같은 비교를 하게 하기 위해서다.

메모리 세션 표 → 서명형 세션 표

서명형 세션 표는 접두어.내용.서명 모양이다. 내용에는 대상·사용자·이름·만료 시각이 들어 있고, 서명은 접두어와 내용을 함께 HMAC-SHA256 으로 묶은 값이다. 서버는 표를 저장하지 않고 들어올 때마다 서명과 만료를 다시 확인한다. 같은 비밀을 쓰는 한 서버를 다시 띄워도 표가 그대로 통한다.

  • 접두어에 버전을 넣어, 옛 메모리 방식 표나 다른 버전 표는 섞여 들어오지 않게 거절한다(거절되면 일반 입장 + 안내)
  • 입장권의 “한 번만 쓰기” 기억은 그대로 둔다. 세션 표가 서명형이 됐다고 입장권 재사용 검사까지 없앨 수는 없다
  • 테스트에 “서버를 새로 만든 뒤에도 표가 통하는지”와 만료·변조·대상·버전 섞기 거절을 넣었다

화면 쪽은 받은 세션 표를 sessionStorage 에 둔다. 탭 안에서만 살아 있어서 새로고침·재접속·다음 판에는 다시 쓰고, 탭을 닫으면 사라진다. 저장이 막힌 브라우저라면 다시 들어올 때 일반 닉네임이 된다.