로그인 없이 시작하고 기록은 안 잃게 — 웹게임 계정을 세 층으로 붙인 기록
설치도 로그인도 없이 바로 시작하는 게임에서 기록을 어떻게 지키나. 표식 · 복구 코드 · 구글 연결 세 층을 붙였고, 복구 코드를 사람이 옮길 수 있는 꼴로 만들며 고른 것들(헷갈리는 글자 빼기 · 검사 글자 · 게임을 알려 주는 머리글자)과 닉네임 검사를 서버가 다시 하는 이유까지 적었습니다.
웹게임의 유일한 강점은 설치도 로그인도 없다는 것입니다. 주소를 누르면 그 자리에서 시작합니다. 그런데 그 강점을 그대로 두면 값을 하나 치릅니다 — 기록이 그 브라우저에 묶입니다. 기기를 바꾸거나 브라우저 저장이 지워지면 거기까지 올린 것이 전부 사라집니다 (아이폰에서는 이게 일주일 만에 일어나기도 합니다).
제일 쉬운 답은 첫 화면에서 가입을 받는 것입니다. 그런데 그러면 방금 말한 강점을 버리는 것이고, 들어온 사람의 대부분은 가입 화면에서 돌아갑니다. 그래서 순서를 뒤집었습니다. 먼저 놀게 하고, 지킬 만한 기록이 생긴 뒤에 지킬 방법을 줍니다. 그걸 세 층으로 붙였습니다.
아래는 제일 늦게 만든 디펜스 킹덤 기준입니다 — 앞의 게임들에서 쓰던 틀(복구 코드 · 구글 연결)을 거기서 다시 짰습니다.
① 표식 — 계정의 실체
로그인이 없으니 「누구인가」를 정하는 것이 필요합니다. 그 자리에 표식 하나를 둡니다.
- 서버가 만든 128비트 난수를 소문자 16진수 32자로 적은 문자열입니다. 아이디도 비밀번호도 아니고, 이것 하나가 계정 자체입니다. 이름을 정하는 순간 서버가 하나 주고, 브라우저가 그걸 들고 있습니다.
- DB에는 해시만 둡니다. 데이터베이스가 새어 나가도 표식 자체는 안 샙니다. 비밀번호를 그대로 저장 안 하는 것과 같은 이유입니다.
- 주소(URL)에 싣지 않습니다. 요청 본문으로만 보냅니다. 주소에 실으면 브라우저 기록, 공유 링크, 화면 캡처, 서버 접근 기록으로 줄줄 새어 나갑니다. 「어차피 우리 서버에만 가는 값」이라는 생각으로 주소에 넣기 쉬운 자리인데, 주소는 그런 성격의 자리가 아닙니다.
여기까지는 아직 그 브라우저 안의 계정입니다. 옮길 방법이 없으면 ①만으로는 아무것도 안 바뀝니다.
② 복구 코드 — 사람이 손으로 옮길 수 있는 꼴
표식을 사람이 받아 적을 수 있는 꼴로 보여 주는 층입니다. 게임마다 한 줄짜리 코드가 나오고, 다른 기기에 그 코드를 넣으면 같은 계정으로 들어옵니다.
받아 적게 할 거라면 고를 것이 꽤 됩니다. 넷을 골랐습니다.
- 헷갈리는 글자를 뺐습니다 — Crockford Base32 를 쓰면
I·L·O·U가 없습니다. 1과 I, 0과 O를 두고 「이게 숫자인가 글자인가」를 고민할 자리가 사라집니다. - 검사 글자를 하나 붙였습니다 — CRC-8 한 바이트입니다. 한 글자를 잘못 적으면 서버까지 가기 전에 화면에서 잡힙니다. 「코드가 틀렸습니다」를 서버 왕복 없이, 그리고 「없는 계정입니다」와 구별해서 말할 수 있습니다.
- 네 글자씩 끊고, 대소문자와 줄표는 아무래도 되게 했습니다 — 사람은 긴 문자열을 네 글자씩 끊어 읽습니다. 소문자로 적어 오든 줄표를 빼먹든 통과시킵니다. 적는 사람을 탓할 자리가 아닙니다.
- 머리 세 글자로 어느 게임인지 말하게 했습니다 — 게임마다
MX1·DK1·PD1처럼 머리만 다릅니다. 게임 셋을 만들어 두면 사용자의 메모장에 비슷한 코드가 여럿 쌓입니다. 그때 코드 스스로가 어느 게임 것인지 말하는 것이 「이거 넣어 보고 안 되면 저거」보다 낫습니다.
그리고 하나를 일부러 안 품게 했습니다. 코드는 이름을 품지 않습니다. 이름은 서버에 있고 바꿀 수 있는 것이라서, 코드가 이름을 품으면 이름을 바꾼 뒤 옛 코드가 거짓말을 합니다. 코드는 표식만으로 계정을 찾고, 그래서 이름을 바꿔도 적어 둔 코드가 그대로 쓸 수 있습니다.
⚠⚠ 이 코드는 비밀번호입니다. 표식이 통째로 들어 있어서, 다른 사람에게 보이는 순간 계정을 내주는 셈이 됩니다. 그래서 화면에는 가려 두고 눌러야 보이게 했습니다. 랭킹에 오르는 게임에서 「내 코드 여기 있어요」가 스크린샷으로 돌아다니면 되돌릴 방법이 없습니다.
③ 구글 연결 — 적어 두지 않아도 되게
②까지 하면 기록을 옮길 수 있지만, 적어 두지 않은 사람은 여전히 잃습니다. 그래서 셋째 층을 붙였습니다.
브라우저가 서버에 보내는 것은 구글이 서명한 ID 토큰 한 장입니다. 여기서 중요한 것 하나는, 서명을 확인하기 전의 토큰 내용은 아무 뜻이 없다는 것입니다. 그냥 글자입니다. 그래서 서버가 넷을 봅니다.
- 구글 공개키로 서명을 확인한다(공개키 목록을 받아 RS256으로 검사). 이걸 안 하면 아무나 아무 번호를 적어 보내면 됩니다.
- 받는 쪽이 우리인지(
aud가 우리 클라이언트 ID). 남의 서비스용으로 발급된 멀쩡한 토큰이 우리 쪽에 그대로 통하면 안 됩니다. - 발급한 쪽이 구글인지(
iss). - 아직 안 지났는지(
exp).
이 방식의 좋은 점은 서버에 비밀이 없다는 것입니다. 클라이언트 시크릿을 쓰지 않고 공개키로만 보기 때문에, 설정에 들어가는 값은 원래 공개되는 클라이언트 ID 하나뿐입니다. 공개키는 자주 바뀌지 않으니 한 시간쯤 들고 있습니다.
⚠ 계정을 잇는 열쇠는 이메일이 아니라 번호(sub)입니다. 이메일은 바뀔 수 있고 돌려쓸 수도 있는 값입니다. 그리고 그 번호도 해시만 저장합니다 — ①과 같은 이유입니다.
곁딸린 문제 — 이름
로그인이 없으면 사람을 가리키는 것이 닉네임 하나입니다. 랭킹에 그대로 공개되는 값이라 규칙이 필요했고, 여기서 한 가지가 중요했습니다.
⚠⚠ 판단은 서버가 합니다. 화면에서 하는 검사는 안내일 뿐입니다. 개발자 도구로 API를 직접 부르면 화면은 아무것도 못 막습니다. 그래서 화면과 서버가 같은 함수를 쓰고, 서버는 이름을 저장하는 갈래마다 그 함수를 다시 부릅니다. 「화면에서 이미 걸렀으니 서버에서는 안 봐도 된다」는 생각이 틀리는 자리는 대개 이 모양입니다.
규칙 쪽에서 고른 것들:
- 완성형 한글 · 영문 · 숫자만 받습니다. 띄어쓰기도 기호도 안 됩니다. 낱자(
ㄱ,ㅏ)를 막은 것은 초성으로 적는 욕이 드나드는 틈이라서입니다. - 같은 이름은 한 계정만 씁니다. 겹치면 거기서 돌려보냅니다.
- 막는 낱말은 숫자를 글자로 되돌린 꼴로 견줍니다(
s3x→sex). 안 그러면 목록이 장식이 됩니다. - ⚠ 막는 낱말을 늘리면 이미 그 이름을 쓰는 사람의 「이름 바꾸기」가 막힙니다. 늘릴 때는 그걸 알고 늘리는 편이 낫습니다 — 어느 날 갑자기 바꾸기가 안 되는 사람이 생기고, 그 사람은 왜인지 모릅니다.
세 층을 한 줄로
| 층 | 무엇 | 없으면 |
|---|---|---|
| 표식 | 계정 그 자체(서버가 준 난수 · DB엔 해시만) | 누구인지 정할 수 없다 |
| 복구 코드 | 그 표식을 사람이 옮기는 꼴 | 기기를 바꾸면 끝이다 |
| 구글 연결 | 적어 두지 않아도 되는 길 | 안 적어 둔 사람은 잃는다 |
세 층이 같은 계정 하나를 가리킨다는 점이 요점입니다. 어느 쪽으로 들어와도 같은 기록이고, 순서도 자유롭습니다 — 이름만 정하고 놀다가, 나중에 코드를 적어 두고, 더 나중에 구글을 잇습니다. 처음에 가입을 요구하지 않으려고 시작한 설계인데, 결과적으로 지킬 만한 것이 생긴 뒤에 지키게 되어서 이게 더 맞는 순서였다고 생각합니다.