로그인 없이 내 채팅을 구분하는 방법
회원가입을 받지 않으면서도 내가 만든 분석은 나만 이어서 쓸 수 있게 하고 싶었어요. 브라우저에 무작위 키 하나를 두는 방식과 그 한계를 정리했어요.
WhoIsLoafing에는 로그인이 없어요. 레포 링크만 넣으면 바로 쓸 수 있게 하는 것이 처음부터의 목표였어요.
그런데 분석 결과를 저장하고 링크로 공유하는 기능을 넣으려니 문제가 생겼어요. 저장된 채팅에는 주인이 있어야 해요. 주소만 알면 아무나 남의 채팅을 이어서 쓰거나 공유해 버릴 수 있으면 안 되니까요.
계정 없이 "이 채팅은 이 사람 것"을 어떻게 알 수 있을까요?
브라우저마다 무작위 키 하나
방법은 단순해요. 브라우저가 처음 왔을 때 아주 긴 무작위 값을 하나 만들어서 저장해 둬요.
const OWNER_KEY_STORAGE = 'whoisloafing:owner'
function ownerKey(): string {
try {
let key = localStorage.getItem(OWNER_KEY_STORAGE)
if (!key) {
key = `${crypto.randomUUID()}${crypto.randomUUID()}`
localStorage.setItem(OWNER_KEY_STORAGE, key)
}
return key
} catch {
return ''
}
}
그리고 서버에 요청을 보낼 때마다 이 값을 헤더에 실어 보내요.
headers: { 'X-Owner-Key': key }
채팅을 처음 저장할 때 서버는 이 키를 채팅과 함께 기록해요. 나중에 같은 채팅에 요청이 오면 키가 같은지 비교해요. 같으면 주인이고, 다르면 주인이 아니에요.
이 키는 로그인 정보가 아니에요. 이름도 이메일도 담겨 있지 않고, 누구인지 알려 주지 않아요. "이 채팅을 만든 바로 그 브라우저"라는 것만 증명해요.
서버에는 해시만 저장해요
키를 그대로 저장하지는 않아요. 저장하는 것은 키의 SHA-256 해시예요.
export const hashOwnerKey = (key: string) =>
createHash('sha256').update(key).digest('hex')
비교할 때는 들어온 키를 같은 방식으로 해시해서 저장된 값과 맞춰 봐요.
const owner = !!ownerKey && hashOwnerKey(ownerKey) === chat.ownerKeyHash
이렇게 하는 이유는 데이터베이스가 유출되는 경우를 생각해서예요. 키가 그대로 들어 있으면 유출된 값을 헤더에 넣기만 해도 주인 행세를 할 수 있어요. 해시만 들어 있으면 원래 키를 알아낼 수 없어요.
비밀번호는 사람이 정하니 짧고 뻔해서 해시를 느리게 만드는 장치가 필요해요. 이 키는 무작위 UUID 두 개를 이어 붙인 값이라 추측으로 맞힐 수 없어서, 빠른 해시 한 번으로 충분해요.
채팅 주소도 브라우저가 만들어요
채팅의 주소는 /c/ 뒤에 UUID가 붙는 형태예요. 이 UUID도 서버가 아니라 브라우저가 만들어요.
const id = crypto.randomUUID()
window.history.pushState(null, '', `/c/${id}`)
링크를 내는 순간 주소가 바로 바뀌어요. 서버의 답을 기다리지 않아도 되니 화면 전환이 빨라요. 서버는 받은 값이 UUID 모양인지 확인한 다음에만 저장해요.
UUID는 추측할 수 없을 만큼 길어서, 주소를 모르는 사람이 남의 채팅을 우연히 찾아낼 일은 없어요.
볼 수 있는 사람과 쓸 수 있는 사람
주인 키와 공유 여부를 합치면 권한이 이렇게 정리돼요.
| 상태 | 주인 | 다른 사람 |
|---|---|---|
| 공유하기 전 | 보고 이어서 쓸 수 있어요 | 열 수 없어요 |
| 공유한 뒤 | 볼 수만 있어요 | 볼 수만 있어요 |
공유한 뒤에는 주인도 더 이어서 쓸 수 없게 한 것이 눈에 띌 거예요. 이유는 공유 링크를 받은 사람의 입장에 있어요. 어제 받은 링크를 오늘 열었는데 내용이 달라져 있으면 그 링크를 믿을 수 없어요. 공유는 그 시점의 결과를 고정하는 일로 정했어요.
그래서 공유는 되돌릴 수 없고, 누르기 전에 확인 창으로 한 번 더 물어봐요.
화면에서 막는 것으로는 부족해요
읽기 전용 화면에서는 버튼을 숨겨요. 하지만 버튼을 숨기는 것은 편의일 뿐 보안이 아니에요. 요청을 직접 만들어 보내면 화면은 아무것도 막지 못해요.
그래서 서버가 요청마다 다시 확인해요.
if (existing?.sharedAt) {
emit({ type: 'error', text: '공유한 채팅에서는 더 이상 분석을 이어갈 수 없어요.' })
} else if (existing && existing.ownerKeyHash !== hashOwnerKey(ownerKey!)) {
emit({ type: 'error', text: '이 채팅은 다른 브라우저에서 만든 채팅이라 이어서 쓸 수 없어요.' })
}
조회할 때도 구분해요. 없는 채팅은 404, 있지만 볼 권한이 없는 채팅은 403을 돌려줘요.
이 방식의 한계
편한 만큼 포기한 것도 분명해요.
- 다른 기기에서 이어서 쓸 수 없어요. 키가 브라우저 안에만 있어서, 노트북에서 만든 채팅을 휴대폰에서는 이어서 쓸 수 없어요. 공유하면 볼 수는 있어요.
- 브라우저 데이터를 지우면 주인이 아니게 돼요. 키가 사라지면 되찾을 방법이 없어요. 공유하지 않은 채팅은 다시 열 수 없게 돼요.
- 시크릿 창에서는 창을 닫으면 끝이에요.
- 같은 컴퓨터를 쓰는 다른 사람과 구분되지 않아요. 키는 사람이 아니라 브라우저를 가리켜요.
이 서비스에서는 받아들일 만한 한계라고 판단했어요. 분석은 언제든 다시 돌릴 수 있고, 잃어버려도 큰일이 나는 자료가 아니기 때문이에요. 결제 내역이나 개인 문서처럼 잃으면 안 되는 자료였다면 이 방식을 쓰면 안 돼요.
언제 쓸 만한가요
로그인을 붙이면 문제는 깔끔하게 풀리지만, 처음 온 사람이 서비스를 써 보기 전에 가입부터 해야 해요. 아래 조건이 맞는다면 무작위 키 방식이 좋은 출발점이에요.
- 처음 온 사람이 바로 써 볼 수 있어야 해요.
- 저장하는 자료가 잃어버려도 다시 만들 수 있는 것이에요.
- 여러 기기에서 이어서 쓰는 일이 드물어요.
나중에 로그인을 붙이더라도 이 키를 계정에 연결하면 그동안 만든 채팅을 넘겨받을 수 있어요. 처음부터 무거운 인증을 넣기보다, 가벼운 방식으로 시작하고 필요해질 때 올리는 편이 작은 팀에게는 맞았어요.