← 목록으로 돌아가기

웹 인증 세션 보안: HttpOnly Cookie, Refresh Token Rotation, CSRF 방어를 함께 설계하기

Security

로그인은 토큰을 저장하는 문제가 아니다

웹 애플리케이션에서 인증은 자주 단순화됩니다. access token을 어디에 저장할지, localStorage가 편한지, cookie가 안전한지 같은 질문으로 시작합니다. 하지만 실제 보안은 저장 위치 하나로 결정되지 않습니다. XSS, CSRF, 토큰 탈취, 세션 고정, refresh token 재사용, 로그아웃 전파까지 함께 설계해야 합니다.

인증 세션은 사용자 경험과 보안의 균형입니다. 너무 자주 로그인하게 만들면 사용자는 불편하고, 너무 오래 유지하면 탈취된 세션의 피해가 커집니다. access token은 짧게, refresh token은 더 엄격하게 관리하고, 의심스러운 재사용이 보이면 세션을 폐기해야 합니다. 이 흐름을 서버와 클라이언트가 함께 지켜야 합니다.

이 글에서는 브라우저 기반 웹 앱에서 HttpOnly Cookie, refresh token rotation, SameSite, CSRF token, 세션 무효화를 어떻게 조합할지 정리합니다. 단일 정답보다 위협 모델에 맞는 선택 기준을 다룹니다.


웹 인증 세션 보안 구조

1. HttpOnly Cookie는 XSS 피해를 줄인다

토큰을 localStorage에 저장하면 JavaScript가 쉽게 읽을 수 있습니다. XSS 취약점이 생겼을 때 공격자는 access token이나 refresh token을 그대로 가져갈 수 있습니다. HttpOnly Cookie는 JavaScript에서 읽을 수 없기 때문에 토큰 탈취 난이도를 높입니다. Secure 속성으로 HTTPS에서만 전송하고, SameSite로 cross-site 전송 조건을 제한할 수 있습니다.

하지만 cookie가 모든 문제를 해결하지는 않습니다. Cookie는 요청에 자동으로 붙기 때문에 CSRF 위험을 고려해야 합니다. SameSite=Lax는 많은 일반 요청을 막아주지만, 모든 시나리오를 대체하지는 않습니다. 민감한 상태 변경 요청에는 CSRF token이나 double submit cookie 같은 추가 방어가 필요할 수 있습니다.

또한 XSS가 있으면 HttpOnly여도 공격자가 사용자의 브라우저에서 요청을 수행할 수 있습니다. 토큰을 훔치기 어렵게 만들 뿐, XSS 방어를 생략해도 된다는 뜻은 아닙니다. CSP, 입력 검증, 출력 인코딩, dependency 관리가 함께 필요합니다.


2. Refresh Token Rotation은 재사용을 탐지한다

Access token은 짧게 유지하고, refresh token으로 새 access token을 발급하는 구조가 흔합니다. 이때 같은 refresh token을 계속 재사용하면 탈취 탐지가 어렵습니다. Refresh token rotation은 refresh할 때마다 기존 token을 폐기하고 새 token을 발급합니다. 이미 사용된 token이 다시 들어오면 탈취 가능성으로 보고 세션 전체를 무효화합니다.

이 방식에는 서버 저장소가 필요합니다. refresh token id, 사용자 id, 만료 시각, 회전 상태, 폐기 여부, 기기 정보 등을 저장해야 합니다. 단순 JWT만으로 완전히 stateless하게 처리하면 재사용 탐지와 강제 로그아웃이 어렵습니다. 보안이 중요한 서비스에서는 세션 상태를 서버가 갖는 편이 운영상 유리합니다.

동시 요청도 고려해야 합니다. 사용자가 여러 탭을 열어 동시에 refresh가 발생하면 한쪽 요청이 token 재사용처럼 보일 수 있습니다. 짧은 grace window를 두거나, 클라이언트에서 refresh 요청을 단일화하는 mutex를 적용할 수 있습니다. 보안과 사용자 경험 사이에서 정책을 정해야 합니다.


3. CSRF와 CORS를 혼동하지 않는다

CORS는 브라우저가 다른 origin의 응답을 읽을 수 있는지 제어합니다. CSRF는 사용자의 브라우저가 인증 cookie를 붙여 원치 않는 상태 변경 요청을 보내는 문제입니다. CORS를 막았다고 CSRF가 자동으로 해결되는 것은 아닙니다. 공격자는 응답을 읽지 못해도 상태 변경을 유도할 수 있습니다.

CSRF 방어는 상태 변경 요청에 예측 불가능한 token을 요구하거나, SameSite cookie 정책과 Origin/Referer 검증을 함께 적용하는 방식으로 구성합니다. API 서버는 POST, PUT, PATCH, DELETE 같은 요청에서 Origin 헤더가 허용된 도메인인지 확인할 수 있습니다. 다만 일부 환경에서 헤더가 없을 수 있으므로 정책을 명확히 정해야 합니다.

SPA에서는 access token을 메모리에 두고 refresh는 HttpOnly Cookie로 처리하는 혼합 전략도 사용합니다. 이 경우 access token 탈취 지속 시간을 줄일 수 있지만, 페이지 새로고침 후 silent refresh 흐름과 CSRF 방어를 함께 설계해야 합니다. 인증 방식은 저장 위치가 아니라 전체 흐름으로 평가해야 합니다.


4. 실무 체크리스트

  • refresh token이 JavaScript에서 읽히지 않는 저장소에 있는가
  • Cookie에 HttpOnly, Secure, SameSite가 설정되어 있는가
  • refresh token rotation과 재사용 탐지가 구현되어 있는가
  • 동시 refresh 요청에 대한 grace 정책이나 클라이언트 단일화가 있는가
  • 상태 변경 요청에 CSRF token 또는 Origin 검증이 적용되는가
  • 로그아웃 시 서버 세션과 refresh token이 폐기되는가
  • 의심스러운 재사용 발생 시 관련 세션을 모두 무효화하고 알림을 남기는가
  • XSS 방어와 CSP가 인증 설계와 함께 검토되었는가

5. 세션 저장소는 보안 이벤트를 기록해야 한다

Refresh token rotation을 제대로 운영하려면 서버가 세션 상태를 알아야 합니다. 단순히 token 유효 여부만 저장하는 것으로는 부족합니다. 세션 생성 시각, 마지막 사용 시각, 마지막 IP 대역, user agent, 기기 이름, 회전 횟수, 폐기 이유를 기록하면 의심스러운 행동을 판단할 수 있습니다. 사용자가 자신의 로그인 기기 목록을 볼 수 있게 하는 기능도 이 데이터에서 출발합니다.

재사용 탐지가 발생하면 단순히 해당 refresh token만 막는 것으로 끝내지 않을 수 있습니다. 이미 탈취가 의심되는 상황이므로 같은 세션 family를 모두 폐기하고, 필요하면 사용자에게 재로그인을 요구합니다. 보안 민감도가 높은 서비스라면 이메일 알림이나 보안 이벤트 기록도 남깁니다. 탐지 후 조치가 없으면 rotation의 효과가 제한됩니다.

세션 저장소는 성능도 고려해야 합니다. 모든 요청마다 세션 DB를 조회하면 병목이 될 수 있습니다. Access token은 짧은 시간 stateless하게 검증하고, refresh 시점에만 세션 저장소를 조회하는 구조가 흔합니다. 단, 강제 로그아웃이나 권한 회수의 즉시성이 필요한 서비스라면 token blacklist나 session version을 함께 고려해야 합니다.


6. 로그아웃과 권한 변경은 토큰 수명과 연결된다

사용자가 로그아웃했는데 access token이 만료될 때까지 계속 유효하다면 보안 기대와 실제 동작이 다릅니다. Access token 수명을 짧게 잡는 이유 중 하나가 이 간극을 줄이는 것입니다. 로그아웃 시 refresh token은 즉시 폐기하고, access token은 짧은 만료 시간으로 자연 소멸시키거나 blacklist를 사용합니다. 서비스 위험도에 따라 선택이 달라집니다.

권한 변경도 비슷합니다. 관리자가 사용자의 권한을 회수했는데 기존 access token에 예전 권한이 들어 있으면 만료 전까지 접근이 가능할 수 있습니다. 이를 막으려면 token 수명을 짧게 유지하거나, 서버에서 권한 version을 확인하거나, 민감한 작업은 매번 서버 권한을 조회해야 합니다. JWT에 모든 권한을 넣는 방식은 빠르지만 즉시 회수에는 약합니다.

비밀번호 변경, 2FA 활성화, 의심 로그인 감지 같은 이벤트에서는 기존 세션을 어떻게 처리할지 정책이 필요합니다. 모든 기기 로그아웃, 현재 기기만 유지, 고위험 세션만 폐기 같은 선택지가 있습니다. 사용자 경험과 보안 위험을 기준으로 제품 정책을 정해야 합니다.


7. 인증 오류 UX도 보안 설계다

인증 실패 메시지는 너무 자세하면 공격자에게 힌트를 주고, 너무 모호하면 사용자 경험이 나빠집니다. 로그인에서는 "이메일 또는 비밀번호가 올바르지 않습니다"처럼 계정 존재 여부를 숨기는 편이 안전합니다. 반면 이미 로그인한 사용자의 권한 부족은 어떤 권한이 필요한지 안내해야 할 수 있습니다.

세션 만료 UX도 중요합니다. 사용자가 작성 중이던 폼이 세션 만료로 사라지면 큰 불편을 겪습니다. Access token 만료는 조용히 refresh하고, refresh 실패 시에는 현재 작업을 보존한 뒤 로그인으로 유도하는 흐름이 좋습니다. 보안은 사용자를 자주 끊어내는 것이 아니라 안전한 방식으로 작업을 이어가게 하는 것입니다.

CSRF 실패, Origin 검증 실패, refresh token 재사용 감지 같은 이벤트는 사용자에게 같은 메시지로 보일 수 있지만 운영 로그에서는 구분되어야 합니다. 사용자에게는 "다시 로그인해 주세요"라고 안내하더라도, 서버에는 원인 code와 requestId를 남겨야 분석할 수 있습니다.


8. 기기 관리 화면은 보안 기능이다

사용자가 자신의 활성 세션을 볼 수 있는 화면은 단순 편의 기능이 아닙니다. 알 수 없는 기기나 오래된 브라우저 세션을 사용자가 직접 종료할 수 있으면 계정 탈취 피해를 줄일 수 있습니다. 기기명, 대략적인 위치, 마지막 사용 시각, 현재 기기 여부를 보여주고, 개별 로그아웃과 전체 로그아웃을 제공할 수 있습니다.

이 기능을 만들려면 서버 세션 모델이 필요합니다. refresh token을 단순 문자열로만 다루면 사용자가 어떤 기기를 끊는지 알기 어렵습니다. session id와 device metadata를 저장하고, refresh token rotation이 같은 session family 안에서 일어나도록 모델링해야 합니다. 보안 UX는 데이터 모델에서 시작됩니다.

의심스러운 세션 종료 후에는 비밀번호 변경과 2FA 설정으로 이어지는 흐름을 제공하는 것이 좋습니다. 사용자가 문제를 발견했을 때 다음 행동을 바로 할 수 있어야 합니다. 인증 보안은 서버 정책뿐 아니라 사용자가 자신의 계정을 통제할 수 있게 하는 경험까지 포함합니다.


9. 토큰 저장 전략은 위협 모델로 결정한다

모든 서비스에 같은 인증 저장 전략이 맞지는 않습니다. 내부 관리자 도구, 일반 소비자 서비스, 금융 서비스, 임베디드 WebView 앱은 위협 모델이 다릅니다. XSS 가능성이 높은 환경인지, CSRF 위험이 큰지, 강제 로그아웃이 즉시 필요하지, 여러 기기 동시 로그인을 허용하는지에 따라 설계가 달라집니다.

예를 들어 보안 민감도가 높은 관리자 도구는 짧은 세션, 강한 SameSite, 재인증, IP 제한, 2FA를 요구할 수 있습니다. 일반 콘텐츠 서비스는 사용자 편의를 위해 refresh 기간을 더 길게 둘 수 있지만, 재사용 탐지와 기기 관리 화면을 제공해야 합니다. 모바일 WebView에서는 cookie 정책이 브라우저와 다를 수 있어 별도 검토가 필요합니다.

중요한 것은 선택 이유를 문서화하는 것입니다. HttpOnly cookie를 쓰는 이유, access token 수명, refresh token 만료, CSRF 방어 방식, 로그아웃 정책을 한 문서에 남기면 나중에 보안 리뷰와 장애 대응이 쉬워집니다. 인증은 코드 조각이 아니라 서비스 보안 계약입니다.


결론: 인증 보안은 여러 방어선을 겹치는 일이다

안전한 웹 인증은 localStorage와 cookie 중 하나를 고르는 문제로 끝나지 않습니다. 토큰 수명, 저장 위치, refresh rotation, CSRF 방어, XSS 완화, 세션 폐기, 재사용 탐지가 함께 맞아야 합니다. 하나의 방어선이 실패해도 피해가 제한되도록 설계해야 합니다.

사용자는 로그인 상태가 자연스럽게 유지되기를 기대하고, 서비스는 탈취된 세션을 빠르게 제한해야 합니다. 이 균형을 만드는 것이 웹 인증 세션 설계의 핵심입니다.