데이터와 보안 ·
비밀번호 원문을 남기지 않고 로그인 상태를 이어 가는 법
먼저 답부터
비밀번호는 복호화할 원문으로 저장하지 않고 검증에 알맞은 느린 해시 함수와 사용자별 소금을 사용해 보호해야 합니다. 로그인에 성공하면 서버는 예측하기 어려운 세션 식별자로 이후 요청을 연결하며, 이 값이 탈취되면 계정을 대신할 수 있으므로 안전한 쿠키와 만료·로그아웃 처리가 필요합니다.
회원가입 기능을 만들 때 가장 위험한 지름길은 비밀번호를 일반 글처럼 데이터베이스에 저장하는 것입니다. 운영자가 읽을 수 있다면 데이터베이스를 훔친 공격자도 읽을 수 있습니다. 비밀번호는 나중에 보여 줄 자료가 아니라 입력한 값이 맞는지만 확인할 자료로 다뤄야 합니다.
현관 열쇠를 복사해 서랍에 보관하는 대신, 열쇠 모양을 직접 되돌릴 수 없는 검사 장치에 통과시킨 결과만 남긴다고 생각해 보세요. 로그인 때 새로 입력한 열쇠를 같은 방식으로 검사해 결과를 비교합니다. 이 역할을 비밀번호 해싱이 맡습니다.
해싱은 원문을 되찾기 위한 암호화가 아니다
hashing은 해싱이라는 뜻으로, 입력값을 일정한 형식의 결과로 바꾸되 그 결과에서 원문을 되찾는 용도로 설계하지 않은 처리입니다. encryption, 즉 암호화는 올바른 키가 있으면 내용을 다시 읽기 위한 기술이므로 비밀번호 검증 목적과 다릅니다.
회원가입 입력 → 비밀번호용 해시 함수 → 검증용 결과 저장
로그인 입력 → 같은 규칙으로 검증 → 저장 결과와 비교
직접 만든 글자 섞기나 단순 SHA-256만 사용하는 방식은 비밀번호 저장에 적합하지 않습니다. 공격자가 매우 빠르게 많은 후보를 시험할 수 있기 때문입니다. OWASP는 현재 강하고 느린 비밀번호 전용 방식으로 Argon2id, scrypt, 조건에 따른 bcrypt나 PBKDF2를 설명합니다. 실제 선택은 사용하는 프레임워크의 최신 보안 기능과 운영 환경을 따릅니다.
소금은 같은 비밀번호의 결과를 다르게 만든다
salt는 소금이라고 번역하며, 사용자마다 무작위 값을 비밀번호에 더해 해시하는 값입니다. 같은 비밀번호를 쓴 두 사람도 서로 다른 결과를 갖게 해 미리 계산한 표를 그대로 적용하기 어렵게 합니다. 현대 비밀번호 라이브러리는 소금 생성과 저장을 함께 처리하는 경우가 많습니다.
소금은 비밀번호처럼 숨겨야만 하는 값은 아니지만 사용자가 직접 고정 문구 하나를 넣어 대신하면 안 됩니다. pepper는 후추라는 별도 비밀값을 추가하는 방식이며 데이터베이스 밖의 비밀 저장소에서 관리해야 합니다. 후추를 잃거나 유출했을 때 교체 절차까지 필요하므로 초보자가 임의 구현하기보다 검증된 라이브러리를 사용합니다.
느리게 검증해야 대량 추측을 어렵게 한다
work factor는 작업 비용이라는 뜻으로 해시 한 번에 필요한 계산량을 조절하는 값입니다. 정상 사용자는 로그인할 때 한 번 검증하지만 공격자는 유출된 결과에 수많은 비밀번호 후보를 넣어 봅니다. 각 시도가 충분히 느리면 대량 추측 비용이 커집니다.
비용을 무조건 높이면 서버 로그인도 느려지고 장애가 날 수 있습니다. 실제 장비에서 사용자가 받아들일 수 있는 시간과 서버 부하를 측정해 정하고, 하드웨어가 바뀌면 검토합니다. 기존 사용자가 로그인할 때 새 설정으로 다시 계산하는 점진적 갱신 방식을 사용할 수 있습니다.
로그인 성공 뒤에는 세션이 상태를 이어 준다
HTTP 요청은 기본적으로 서로 독립적입니다. session은 세션이라는 뜻으로 여러 요청을 같은 사용자 활동과 연결해 로그인 상태를 유지하는 방법입니다. 서버는 예측하기 어려운 세션 식별자를 만들고 브라우저는 보통 쿠키로 그 값을 다음 요청에 보냅니다.
세션 식별자는 임시 출입증과 같습니다. 비밀번호가 없어도 유효한 출입증을 가진 사람이 계정을 대신할 수 있으므로 값 자체를 민감정보로 다룹니다. 화면 HTML, 주소, 분석 이벤트, 오류 로그에 세션 값을 남기지 않습니다.
쿠키 속성은 전송 범위를 줄인다
세션 쿠키에는 Secure, HttpOnly, SameSite 같은 속성을 검토합니다. Secure는 HTTPS 연결에서만 보내도록 돕고, HttpOnly는 브라우저 JavaScript가 값을 직접 읽지 못하게 하며, SameSite는 다른 사이트에서 시작된 요청에 쿠키를 보낼 범위를 조절합니다.
Set-Cookie: session_id=임의값; Secure; HttpOnly; SameSite=Lax
이 문장은 구조를 보여 주는 예시일 뿐 모든 서비스에 그대로 맞는 정답이 아닙니다. 외부 로그인, 결제 복귀, 여러 하위 도메인을 쓰는 서비스는 필요한 흐름과 위조 요청 방어를 함께 검토해야 합니다. 세션 식별자를 localStorage에 저장하면 JavaScript에서 읽을 수 있어 스크립트 삽입 사고 때 탈취 위험이 커집니다.
만료와 로그아웃과 재발급을 설계한다
세션은 영원히 유지하지 않습니다. 일정 시간 활동이 없을 때의 만료, 전체 유효 기간, 사용자가 로그아웃했을 때 서버에서 무효화하는 절차를 정합니다. 비밀번호 변경, 권한 상승, 계정 복구처럼 위험 수준이 바뀌는 사건 뒤에는 세션 식별자를 새로 발급하거나 다시 인증합니다.
- 로그인 성공 시 기존 식별자를 그대로 재사용하지 않습니다.
- 로그아웃 시 브라우저 쿠키뿐 아니라 서버 세션도 무효화합니다.
- 비밀번호 변경 뒤 다른 기기의 세션 처리 정책을 정합니다.
- 관리자 기능 전에는 최근 인증을 다시 요구할 수 있습니다.
- 탈취가 의심되면 모든 세션을 끊고 사용자에게 알릴 경로를 준비합니다.
비밀번호 재설정은 원문 전송이 아니다
“비밀번호 찾기” 기능이 기존 비밀번호를 이메일로 보내 준다면 원문을 읽을 수 있게 저장했다는 신호입니다. 기존 값을 알려 주는 대신 짧은 시간만 유효한 일회용 재설정 절차로 새 비밀번호를 정하게 해야 합니다. 계정 존재 여부를 과도하게 드러내지 않고, 링크 사용 뒤 토큰을 무효화합니다.
비밀번호나 재설정 토큰을 고객 지원 채팅으로 받지 않습니다. 관리자도 사용자의 원문 비밀번호를 볼 필요가 없어야 합니다. 보안 사고가 의심되면 로그를 지우기보다 접근을 제한하고 정해진 사고 대응 절차로 증거와 영향을 확인합니다.
오늘 바로 해볼 작은 실습
코드 없이 로그인 자료 흐름을 한 장에 그립니다.
- 회원가입 입력에서 비밀번호 원문이 머무르는 구간을 표시합니다.
- 비밀번호용 라이브러리가 해시 결과를 만드는 상자를 그립니다.
- 데이터베이스에는 원문 대신 검증용 결과만 적습니다.
- 로그인 성공 뒤 세션 식별자가 쿠키로 오가는 화살표를 그립니다.
- 로그아웃과 만료 시 서버 세션이 무효화되는 지점을 표시합니다.
- 로그, 분석 도구, 주소에 보내면 안 되는 값을 빨간색으로 적습니다.
실제 비밀번호, API 키, 세션 쿠키를 실습 자료나 AI 채팅에 넣지 마세요. 암호 알고리즘과 작업 비용을 직접 발명하거나 운영 데이터베이스의 저장 형식을 즉시 바꾸지 않습니다. 현재 프레임워크의 공식 보안 기능, 마이그레이션과 복구 계획, 기존 세션 영향까지 설명할 수 없으면 배포를 멈추고 보안 검토를 받습니다.
확인한 자료
- Password Storage Cheat Sheet — OWASP Cheat Sheet Series
- Session Management Cheat Sheet — OWASP Cheat Sheet Series
함께 읽기
- 환경변수와 API 키를 서랍과 금고처럼 구분하는 법
환경변수는 주소나 실행 모드처럼 프로그램 밖에서 바꿀 설정을 담는 서랍이고, API 키는 외부 서비스 사용 권한을 가진 비밀번호에 가깝습니다. 비밀키는 브라우저 코드나 Git 저장소에 넣지 말고 서버의 비밀 저장소에서만 읽도록 해야 합니다.
- 쿠키와 세션과 브라우저 저장소를 보관함 위치로 구분하기
쿠키는 브라우저가 보관하며 조건에 맞는 서버 요청에 함께 보낼 수 있는 작은 값이고, 세션은 보통 서버가 로그인 상태를 보관하고 쿠키에는 연결용 식별자만 두는 방식입니다. localStorage는 같은 출처의 브라우저에 남지만 요청에 자동 첨부되지 않으므로 화면 설정에는 쓸 수 있어도 비밀키 저장소로 보면 안 됩니다.
- SQL로 데이터를 만들고 찾고 고치고 지우는 첫 연습
SQL은 데이터베이스에 말을 거는 문법입니다. INSERT는 새 행을 넣고 SELECT는 조건에 맞는 행을 읽으며 UPDATE는 기존 값을 고치고 DELETE는 행을 지웁니다. 변경 명령은 WHERE 조건이 빠지면 범위가 커질 수 있으므로 같은 조건을 SELECT로 먼저 확인해야 합니다.