JWT 토큰 인증 개념 및 회원 플랫폼 프로젝트 분석
MSA 환경에서 여러 서비스가 공통으로 사용하는 회원 관리 플랫폼 API 프로젝트를 분석하였습니다.
JWT 토큰 인증 분석
세션 대신 JWT 사용 이유
세션 인증 방식은 상태를 서버에 저장하므로 세션 저장소에 의존성이 발생하지만,
JWT는 인증 정보를 토큰에 담아 전달해서 각 서비스가 독립적으로 검증하는 방식도 가능합니다.
모바일 앱 등 다양한 클라이언트 환경에서도 인증이 용이하고 확장성이 좋습니다.
대규모 트래픽 환경에서는 서버에 인증 상태를 보관하는 세션 방식보다
인증 정보를 토큰 형태로 전달하는 JWT 방식이 서버 메모리 부담을 줄일 수 있습니다.
매 요청마다 토큰을 파싱하고 서명을 검증하는 CPU 연산 비용은 추가됩니다.
토큰 만료 시간 설정 근거
액세스 토큰 만료 시간 : 15분
액세스 토큰은 탈취 즉시 API 호출하여 악용될 수 있기 때문에 만료 시간이 짧을수록 안전합니다.
하지만 액세스 토큰 만료가 빈번해 자주 재발급하면 네트워크 트래픽이 증가하고 UX 저하가 발생합니다.
네트워크가 느린 환경에서도 사용자 경험을 해치지 않고 보안을 강화하도록 15분으로 설정했습니다.
15분은 OWASP JWT Sheet 권장 하한값이며, 탈취 시 피해를 최소화할 수 있습니다.
더 짧은 만료시간보다 토큰 재사용 감지가 보안 강화에 더 효과적입니다.
리프레시 토큰 만료 시간 : 7일
리프레시 토큰 만료 시간이 길면 재로그인을 최소화해서 사용자 경험이 향상되지만,
탈취 시 장기간 악용될 수 있다는 점을 고려해서 7일로 설정하였습니다.
Rotation 적용 시, 액티브 유저는 슬라이딩 세션 효과를 제공받아 7일 제한이 체감되지 않게 됩니다.
Refresh Token Rotation 적용
클라이언트가 리프레시 토큰을 이용해서 액세스 토큰을 재발급 요청하면,
액세스 토큰 및 리프레시 토큰을 모두 새로 발급합니다.
리프레시 토큰 재발급 시, 기존 리프레시 토큰을 무효화하고 새로 발급하는 방식을 적용합니다.
기존 토큰을 서버에서 관리할 경우, 탈취된 토큰의 재사용을 탐지하거나 차단할 수 있습니다.
토큰 재사용이 감지되면 레디스에서 해당 회원의 전체 세션을 무효화하고
사용자에게 안내해서 모든 기기 재로그인을 유도하는 것이 보안상 안전합니다.
JWT 토큰 저장 장소 선정
액세스 토큰 : 메모리
로컬스토리지에 저장하면 Javascript 삽입 XSS 공격으로 토큰이 탈취될 수 있으므로,
JavaScript 변수 등 클라이언트 메모리에 저장하는 것이 권장됩니다.
페이지를 새로고침하면 토큰이 휘발되므로 리프레시 토큰으로 받아오는 처리가 필요합니다.
리프레시 토큰 : HttpOnly 쿠키
HttpOnly 쿠키에 저장하면 Javascript로 접근할 수 없어 XSS 방어되고,
HTTP 요청 시마다 쿠키가 헤더에 자동 포함되어 전송되므로 인증 처리가 간편합니다.
토큰을 쿠키에 저장 시, 의도치 않은 요청을 보내는 CSRF 공격에 취약할 수 있으므로
다른 사이트에서 온 요청에는 쿠키를 보내지 않는 SameSite=Strict 설정 방어가 필요합니다.
JWT 인증 관리 레디스 도입 이유
서버가 토큰 상태를 알 수 없는 stateless 방식의 JWT 운영 통제 한계를 보완하기 위해서
레디스에 리프레시 토큰, 세션 목록, Rate Limit, 로그인 실패 횟수 등을 저장하고 관리했습니다.
레디스 장점
- 데이터를 디스크가 아니라 메모리에 저장하여 일반 DB보다 처리 속도가 훨씬 빠름
- 여러 클라이언트가 동시 실행해도 값이 꼬이지 않는 원자적 연산 (INCR key)를 제공
- 저장된 key마다 EXPIRE 명령으로 유효기간 (TTL) 설정하여 자동 만료 가능
JWT 로그아웃 구현
로그아웃
레디스에서 리프레쉬 토큰 삭제, 클라이언트에서 액세스 토큰 삭제하여 사용자 관점에서 로그아웃 처리합니다.
액세스 토큰에 저장된 만료시간 (EXP) 동안은 유효한 토큰이므로 주의가 필요합니다.
즉시 무효화가 필요하다면 블랙리스트나 토큰 버전 관리가 필요하지만,
매 요청마다 Redis 조회가 추가되어 성능 비용이 발생합니다.
현재 프로젝트는 API 서버 인증 필터에서 매 요청의 DB 유저 상태 (SUSPENDED/LOCKED) 를 검증합니다.
계정 정지나 잠금 상태를 즉시 반영하기 위해 성능보다는 보안을 우선하는 설계입니다.
전체 로그아웃
레디스에서 유저 세션 목록 Set에 저장된 모든 jti (JWT ID) 조회 후 일괄 삭제합니다.
DB 설계 및 보안 분석
회원, 회원 민감정보 테이블 분리
- member : 자주 조회되는 회원 인증 상태, 사용자/관리자 역할 등 저장
- member_privacy : 이름, 전화, 생년월일 등을 별도로 암호화 저장하여 보안 강화
회원 테이블 분리 시 장점
회원 조회 시 불필요한 개인정보를 조회하지 않아 성능 및 보안상 이점이 있습니다.
탈퇴 시 민감정보 테이블만 삭제하고 member는 익명화하여 감사로그 추적 및 통계 유지 가능합니다.
이메일 암호화 안한 이유
이메일은 로그인 시 빠른 조회가 필요하고 중복 가입을 방지하기 위한 식별 정보입니다.
암호화하면 매번 다른 암호문이 생성되어 DB에서 UNIQUE 인덱스 사용이 어렵고,
해시하면 검색은 가능하지만 이메일 원본 복구가 불가하므로 평문 그대로 저장합니다.
개인정보 조회 API 응답 시에는 이메일 일부를 마스킹하고, 감사 로그를 기록하여 보안을 보완했습니다.
전화번호 컬럼 두개인 이유
- phone_encrypted : AES-256-GCM 양방향 암호화 (전화번호 원본 조회용)
- phone_hash : SHA-256 단방향 해시 + Salt (전화번호 중복 체크 등 정확 일치 비교 검색용)
비밀번호 저장 시 BCrpt Cost Factor 10 단방향 해시 이유
bcrypt는 의도적으로 느리게 만들어서 모든 경우의 수를 시도하는 브루트포스 공격을 어렵게 하는 해시입니다.
자주 쓰는 비밀번호 리스트로 공격하는 사전 공격의 시도 속도도 늦출 수 있습니다.
해시 연산 반복 횟수를 의미하는 Cost Factor를 10으로 설정하여
8보다 보안성을 확보하면서도 12보다 로그인 성능에 영향을 최소화했습니다.
bcrypt는 내부적으로 랜덤 Salt를 생성하여 해시에 포함하기 때문에
동일한 비밀번호라도 서로 다른 해시 값이 생성됩니다.
감사 로그 테이블, 로그인 히스토리 테이블 FK 없는 이유
탈퇴 후 개인정보 파기 시에도 각 로그는 일정 기간 동안 보존이 필요합니다.