학습 목표
인증(Authentication) 과 인가(Authorization) 를 구분할 수 있다.
세션·쿠키 기반 로그인 흐름을 설명할 수 있다.
비밀번호를 평문 저장하지 않는 이유와 솔트·해싱 원칙을 설명할 수 있다.
쿠키 속성(HttpOnly, Secure, SameSite)이 세션 탈취 방어에 어떻게 쓰이는지 설명할 수 있다.
문제 상황
- DB 유출 후
users.password컬럼이 그대로 노출됐다- 평문이면 즉시 계정 탈취, 다른 사이트에서도 재사용 공격
- 로그인은 되는데 XSS 한 줄로
document.cookie가 털린다- 세션 ID가 JS에서 읽히면 인증 우회
- HTTP로 로그인 폼을 보냈다
- TLS 없이 credential이 네트워크에 노출
- "세션"과 "JWT"를 같은 말로 쓴다
- 저장 위치·검증 주체·로그아웃 방식이 다른데 설계가 꼬인다
TLS로 전송 구간은 봤다. 이번 편은 서버가 사용자를 어떻게 기억하고, 비밀번호를 어떻게 보관하는지다.
1. 인증이란
인증 — "너 누구야?"를 확인하는 과정.
| 단계 | 질문 | 예 |
|---|---|---|
| Identification | 누구라고 주장? | username, email |
| Authentication | 증명했나? | password, OTP, WebAuthn |
| Authorization | 뭘 할 수 있나? | RBAC, resource owner check |
- TLS는 전송 기밀성·서버 인증
- 앱 인증은 사용자 신원 확인 — 계층이 다르다
2. 세션 기반 인증
가장 흔한 웹 패턴. 로그인 성공 후 서버가 세션을 만들고, 클라이언트는 세션 ID만 들고 다닌다.

POST /login— username + password (HTTPS)- 서버 — credential 검증, 서버 측 세션 저장소에 세션 생성
- 응답 —
Set-Cookie: session_id=...; HttpOnly; Secure; SameSite=Lax - 이후 요청 — 쿠키의
session_id로 사용자 식별
| 구성 | 역할 |
|---|---|
| Session store | Redis, DB, in-memory — session_id → user_id, expiry |
| Session ID | 예측 불가능한 랜덤 값 (UUID, crypto random) |
| Cookie | 브라우저가 자동으로 붙이는 전달 수단 |
- 로그아웃 — 서버에서 세션 삭제 + 쿠키 만료
- 서버 상태 — 세션 수·무효화를 서버가 통제 (JWT와 대비, 다음 편)
3. 쿠키 보안 속성
세션 ID는 비밀번호와 같은 비밀이다. 쿠키 설정이 방어선이다.

| 속성 | 효과 |
|---|---|
| HttpOnly | JavaScript document.cookie 접근 차단 — XSS 완화 |
| Secure | HTTPS에서만 전송 |
| SameSite | Strict/Lax/None — CSRF 완화 (CSRF는 이후 편) |
| Path / Domain | 전송 범위 최소화 |
Set-Cookie: session_id=s%3A...; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=3600
SameSite=None은 Secure 필수- 서브도메인 공유(
Domain=.example.com)는 범위를 넓힌다 — 필요할 때만
4. 비밀번호 저장 — 절대 평문 금지
유출은 언제든 일어날 수 있다. 저장 형식이 마지막 방어선이다.

하면 안 되는 것
- 평문 저장
- 단순 MD5/SHA-256 한 번만 (레인보우 테이블·GPU 크랙)
- 모든 사용자 동일 솔트
해야 하는 것
| 요소 | 설명 |
|---|---|
| Salt | 사용자마다 랜덤 — 동일 비밀번호도 다른 해시 |
| Slow hash | bcrypt, scrypt, Argon2 — brute-force 비용 증가 |
| Work factor | CPU/메모리 비용 파라미터 — 주기적 상향 |
# 개념 예시 (Python passlib / bcrypt)
import bcrypt
salt = bcrypt.gensalt(rounds=12)
stored = bcrypt.hashpw(password.encode(), salt)
# 로그인 시 bcrypt.checkpw(password.encode(), stored)
- Pepper — 앱 전역 비밀을 추가 (HSM/Vault에 보관, DB와 분리)
- 비밀번호 정책만으로는 부족 — 저장 방식이 핵심
5. 인증 실패와 fail secure
보안 원칙과 연결한다.
| 상황 | 나쁜 동작 | 권장 |
|---|---|---|
| 잘못된 비밀번호 | "user not found" vs "wrong password" 구분 | 동일 메시지 — 계정 열거 방지 |
| 로그인 API 장애 | 인증 스킵 | 503 · fail closed |
| 세션 만료 | 무한 연장 | 재로그인 요구 |
| brute force | 무제한 시도 | rate limit, lockout, CAPTCHA |
HTTP/1.1 401 Unauthorized
{"error": "invalid credentials"}
- 성공/실패 로그는 보안 감사에 필요 (PII 마스킹은 이후 편)
6. 세션 vs 토큰 (미리보기)
| 세션 (쿠키) | 토큰 (JWT 등) | |
|---|---|---|
| 상태 | 서버 저장 | often stateless |
| 로그아웃 | 서버에서 즉시 무효화 | 만료·블록리스트 설계 필요 |
| 적합 | 전통 웹, BFF | SPA, 모바일 API |
| XSS | HttpOnly로 완화 | localStorage 저장 시 위험 |
다음 편부터 JWT·OAuth로 확장한다. 세션 기초 없이 토큰만 쓰면 설계가 얕아진다.
7. 정리
- 인증은 사용자 신원 확인 — TLS와 별개 계층
- 세션 ID는 서버가 발급·검증, 쿠키는 전달 수단
- HttpOnly, Secure, SameSite로 쿠키 보호
- 비밀번호는 솔트 + 느린 해시 — 평문·단순 해시 금지
- fail secure — 불확실하면 거부
다음에 다룰 것
- JWT
- 구조, 서명, 만료, 저장 위치 트레이드오프
해당 내용은 OWASP Authentication Cheat Sheet 및 OWASP Password Storage Cheat Sheet 를 기반으로 합니다.