처음에는 accessToken과 refreshToken을 응답 헤더로 내려주고, 클라이언트에서 JS로 직접 쿠키에 저장하는 방식으로 구현했습니다.
이 방식은 간단하지만 XSS에 취약했습니다.
이를 개선하기 위한 인증 구조 리팩토링을 진행하게 되었습니다.
기존 인증 구조 정리
전체 로그인 흐름
-
자체 로그인
- 서버에서 accessToken과 refreshToken을 응답 헤더에 담아 클라이언트로 전달
- 클라이언트는 accessToken과 refreshToken을 document.cookie를 통해 JS에서 쿠키로 직접 저장
-
OAuth2 로그인
- 로그인 성공 시 accessToken, refreshToken, email을 redirect URL의 쿼리 파라미터에 담아 전달
- 클라이언트는 쿼리에서 accessToken과 refreshToken을 파싱해서 document.cookie를 통해 JS에서 쿠키로 직접 저장
RefreshToken 저장 및 검증 (Rtr 방식)
- 서버는 refreshToken을 Redis에
"RT:{email}"
형식으로 저장
- AccessToken 만료 시:
- 클라이언트는 refreshToken과 email을 요청 헤더에 함께 전송
- 서버는 헤더 값과 Redis의 저장값과 일치 여부를 비교
- 유효 시 → accessToken + refreshToken 재발급 및 Redis 갱신
로그아웃 처리
- accessToken은 Redis 블랙리스트로 등록 (남은 만료 시간 기준)
- Redis에서 해당 email의 refreshToken 삭제
개선 필요성을 느낀 이유
- JS에서 접근 가능한 쿠키로 인해 XSS 공격에 매우 취약
- OAuth2 로그인과 자체 로그인 간 인증 흐름이 일관되지 않음
개선된 인증 구조 설계
구조 개요
-
자체 로그인 시:
- accessToken → 응답 바디(JSON)
- refreshToken → Secure + HttpOnly + SameSite=None 쿠키로 전달
-
OAuth2 로그인 시:
- refreshToken만 Secure + HttpOnly + SameSite=None 쿠키로 전달
- email을 redirect URL의 쿼리 파라미터에 담아 전달
- accessToken은 전달하지 않고, 리다이렉트된 페이지에서 재발급 요청을 통해 별도로 발급
클라이언트 저장 방식
- accessToken → Recoil에 저장 클라이언트 메모리에서만 유지 (브라우저 저장 없음)
- refreshToken → HttpOnly 쿠키에 저장 (JS 접근 불가)
- email → localStorage에 저장, refreshToken 재발급 시 Redis 키 식별자 역할
RefreshToken 저장 및 검증 (Rtr 방식 유지)
로그아웃 처리 방식
- refreshToken 쿠키 제거 (maxAge = 0)
- accessToken → Redis 블랙리스트 등록
- Redis에서 refreshToken 삭제
개선 효과
- XSS에 강한 구조로 보안성 향상
- 클라이언트 토큰 저장 방식 통일 (쿠키 + 메모리 기반)
- 자체 로그인 / OAuth2 흐름 구조 일관성 확보
구조 개선 전후 비교
항목 | 기존 구조 | 개선 구조 |
---|
토큰 전달 방식 | 헤더 / 쿼리 파라미터 | 응답 바디 + HttpOnly 쿠키 |
RefreshToken 저장 | Redis "RT:{email}" | 동일 |
RefreshToken 전달 방식 | 클라이언트가 헤더에 직접 포함 | 클라이언트가 쿠키에 저장된 값을 자동 전송 |
email 전달 방식 | localStorage 저장 후 헤더로 전달 | 동일 |
보안성 | JS 접근 가능 쿠키 → XSS 취약 | HttpOnly + Secure → XSS 방지 |
Silent Refresh | RefreshToken 헤더 + localStorage 기반 자동 처리 | 쿠키 + localStorage 기반 자동 처리 |
로그아웃 처리 | 블랙리스트 등록 + Redis 삭제 | 동일 + 쿠키 제거까지 포함 |
전체 코드 보기