백엔드 개발을 하다 보면 CRUD 다음으로 마주하는 산이 바로 '회원 로그인' 기능이다. 로그인을 만든다는 건 곧 시스템의 보안을 다뤄야 한다는 뜻이다. 이때 다음 문장을 꼭 기억하자.
"보안은 잘하는 것이 아니라, 보안 사고가 터질 수밖에 없는 구조 자체를 만들지 않는 것이다."
처음 로그인 기능을 구현할 때 흔히 "비밀번호를 안전하게 암호화해야지"라고 생각하기 쉽다. 하지만 이는 정답이 아니다. 정답은 '단방향 해싱(Hashing)'이다. 암호화는 복호화(원본으로 되돌림)가 가능하다는 것을 전제로 한다. 만약 DB가 털리면서 암호화 키까지 유출된다면, 모든 유저의 비밀번호 원본이 고스란히 노출되는 치명적인 사고로 이어진다. 반면 해싱은 입력값을 넣으면 항상 같은 결과가 나오지만, 원래 값으로 되돌리는 것(복호화)은 불가능하다. 즉, 비밀번호는 '확인'만 가능해야 하며 시스템 관리자조차 다시는 원본을 꺼낼 수 없어야 한다.
💡 로그인 인증 흐름
로그인 시에는 이 단방향 원리를 이용한다.
1. 사용자가 회원가입(이메일 + 비밀번호)을 한다.
2. DB에는 비밀번호 원본이 아닌, 해싱된 '해시값'이 저장된다.
3. 사용자가 다음에 로그인할 때 입력한 비밀번호를 똑같이 해싱하여, DB에 저장된 해시값과 비교한다.
4. 두 해시값이 일치하면 인증을 통과시킨다.
단방향이라고 해서 SHA-256 같은 일반적인 해시 함수를 로그인에 써서는 안 된다. 일반 해시 함수는 처리 속도가 너무 빠르다. 해커가 무차별 대입(Brute Force) 공격으로 비밀번호를 순식간에 알아낼 위험이 크고, 미리 계산해 둔 해시값 표(레인보우 테이블)를 이용해 역추적할 수도 있다.
따라서 안전한 패스워드 해시 알고리즘은 다음 3가지 조건을 반드시 갖춰야 한다.
이 모든 조건을 만족하는 알고리즘으로 Argon2id, bcrypt, scrypt 등이 있다. 그중에서도 현재 백엔드 실무 표준이자 가장 강력히 권장되는 알고리즘은 Argon2id다. 이걸 사용하자.
알고리즘 선택은 끝났다. 하지만 실제 로그인 로직을 짤 때 다음 3가지 디테일을 놓치면 시스템에 빈틈이 생긴다. 꼭 기억하자.
로그인을 시도할 때 DB에 해당 이메일이 없으면 바로 에러를 반환하고, 이메일이 존재하면 패스워드 검증을 하느라 시간이 조금 더 걸린다고 가정해 보자. 해커는 이 미세한 '응답 시간의 차이'를 측정해 특정 이메일이 시스템에 가입되어 있는지 알아낼 수 있다.
이를 막으려면 DB에 이메일이 존재하지 않더라도 임의의 더미(Dummy) 해시 검증 로직을 실행하도록 만들어야 한다. 성공하든 실패하든 응답 시간을 동일하게 맞춰서 해커의 눈을 속이는 것이다.
에러 메시지를 친절하게 "존재하지 않는 계정입니다"와 "비밀번호가 틀렸습니다"로 나누어주면, 해커는 이를 이용해 유효한 이메일 목록만 싹 수집한 뒤 집중 공격을 퍼부을 수 있다.
시스템 내부 정보가 노출되지 않도록, 계정이 없든 비밀번호가 틀렸든 응답 메시지는 무조건 "이메일 또는 비밀번호가 올바르지 않습니다"로 뭉뚱그려 통일해야 한다.
시간이 흘러 컴퓨팅 파워가 발전하면 보안 정책(해싱 반복 횟수, 메모리 사용량 등)을 더 강력하게 업그레이드해야 한다. 이때 기존 유저들의 구버전 해시값은 어떻게 할까?
유저가 이전 방식으로 해싱된 비밀번호로 정상 로그인을 성공했을 때, 백그라운드에서 새로운 보안 정책이 적용된 해시값으로 DB를 조용히 업데이트해 주면 된다. 이를 재해싱이라 부르며, 유저가 모르는 사이에 시스템 보안을 알아서 최신으로 유지하는 마이그레이션 기법이다.