웹 인증·인가와 상태 관리

Jerry·2025년 8월 11일

TL;DR

  • 쿠키(Cookie)는 브라우저가 서버에 자동 전송하는 작은 key-value storage이며, 보안 플래그 설정이 핵심입니다.
  • 세션(Session)은 서버 측 상태 저장(세션 ID만 클라이언트 보관)으로 무효화·강제 로그아웃이 용이합니다.
  • JWT는 서명된 토큰으로 stateless 검증이 장점이나 폐기, 회수가 어렵습니다. (회피: 짧은 수명 + refresh token 회전)
  • 브라우저 스토리지(localStorage/sessionStorage/IndexedDB)는 스크립트 접근 가능 → XSS에 취약하므로 민감 비밀 보관 금지가 원칙입니다.
  • 권장 기본기: 짧은 수명의 Access Token + HttpOnly·Secure·SameSite 쿠키 기반, BFF 패턴 또는 서버 세션/Redis 세션으로 운용, CSRF/XSS를 별도로 방어합니다.

1. 배경: 인증·인가·상태 관리

  • 인증(Authentication): 사용자의 신원을 확인합니다(로그인).
  • 인가(Authorization): 인증된 주체가 어떤 리스소·행위를 허용받는지 결정합니다.
  • 상태 관리(State Management): 본질적으로 무상태(Stateless)인 HTTP 위에서 "누가 이미 로그인했는가, 권한은 무엇인가"를 요청 간 이어붙이는 기법입니다.

대표 전략:

  1. 서버 세션(Stateful): 서버가 사용자 상태를 저장, 클라이언트는 세션 식별자만 가짐
  2. 토큰 기반(Stateless): 서버가 토큰을 발급하고 요청마다 토큰 자체를 검증(서명 확인)

2. 쿠키(Cookie)

개요와 동작

  • 서버 → 클라이언트: Set-Cooke 헤더로 쿠키 설정
  • 클라이언트 → 서버: 동인 도메인/경로/보안 조건에 맞으면 자동 전송
Set-Cookie: SID=abc123; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=1200

주요 속성

  • Secure: HTTPS에서만 전송
  • HttpOnly: JS document.cookie로 접근 불가 → XSS로부터 쿠키 탈취 완화
  • SameSite: Lax(기본) / Strict / None(+Secure 필수)
    • Lax: 동일 사이트 요청에는 전송. 타 사이트에서 온 "최상위 GET 탐색"에도 전송
    • Strict: 동일 사이트 요청에만 전송
    • None: 교차 사이트 요청에도 전송
  • Domain/Path: 전송 범위 제한
  • Expires/Max-Age: 만료 시점

보안 포인트

  • CSRF: 쿠키는 자동 전송되므로 SameSite + CSRF 토큰 조합을 사용합니다.
  • XSS: HttpOnly로 탈취 난이도 상승. 그러나 쿠키 값 자체가 비밀이면 XSS에 취약합니다(예: 쿠키 값을 JS로 읽지 못해도, 악성 요청이 자동 전송될 수 있음).
  • 쿠키에 토큰 저장 시: 가급적 HttpOnly + Secure + SameSite=Lax/Strict로 설정하고, 도메인 최소화가 좋습니다.

3. 세션(Session)

개념

  • 클라이언트는 오직 세션 ID(대개 쿠키로 전달, 예: JSESSIONID)만 보유
  • 서버는 세션 저장소(메모리, DB, Redis 등)에 사용자 상태(인증·권한·부가정보)를 보관

장단점

  • 장점:
    • 서버가 상태를 통제 → 강제 로그아웃(세션 무효화), 권한 변경 즉시 반영이 용이
    • 토큰 유출 대비면에서 상대적으로 안전(서명·클레임 노출 없음)
  • 단점:
    • 확장성: 서버 클러스터에서는 세션 공유 저장소(예: Redis) 필요 or Stricky Session 의존
    • 서버 메모리/저장소 사용량 증거

공격·대응

  • 세션 고정(Session Fixation): 로그인 성공 시 세션 ID 재발급(migrate)으로 예방
  • 세션 하이재킹: Secure/HttpOnly/SameSite, 짧은 타임아웃, IP/UA 바인딩, 2FA 등 병행
  • 로그아웃 처리: 서버 저장소에서 세션 무효화 + JSESSIONID 쿠키 삭제

예시 (Spring)

application.yml

server:
  servlet:
    session:
      timeout: 30m
      cookie:
        http-only: true
        secure: true
        same-site: lax

SecurityFilterChain

@Bean
SecurityFilterChain security(HttpSecurity http) throws Exception {
    http
      .csrf(csrf -> csrf.enable()) // 쿠키 세션 기반이면 기본 활성 권장
      .sessionManagement(sm -> sm
          .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
      )
      .logout(lo -> lo
          .deleteCookies("JSESSIONID")
          .invalidateHttpSession(true)
      );
    // Spring Security는 기본이 세션 고정 보호(migrateSession)입니다.
    return http.build();
}

Redis 세션 공유

// build.gradle: implementation "org.springframework.session:spring-session-data-redis"
@EnableRedisHttpSession // 분산 환경에서 서버 간 세션 공유

4. JWT(JSON Web Token)

구조

{header}.{payload}.{signature} (Base64URL 인코딩)

  • Header: alg(서명 알고리즘), typ(JWT), kid(키 식별자)
  • Payload(Claims): iss(발급자), sub(주체), aud, exp, nbf, iaf, jti, scope/roles 등 민감정보 제외
  • Signature: HMAC(대칭) 또는 RSA/ECDSA(비대칭)

장단점

  • 장점:
    • 무상태 검증 → 고성능/고확장(리소스 서버는 서명만 확인)
    • 분산 마이크로서비스에서 중앙 세션 공유 없이 검증 가능
  • 단점:
    • 폐기·회수 어려움: 이미 발급된 토큰은 만료 전까지 유효
    • 과다 클레임/큰 토큰은 헤더 크기 증가 → 네트워크 비용 증가
    • 키 관리(회전·유출 대응) 필수

권장 패턴

  • Access Token: 짧은 수명(분 단위)
  • Refresh Token: 더 긴 수명(일~주), 서버 저장소/화이트리스트 + 회전(Rotation)
    • 매 갱신 시 새 리프레시 발급, 이전 토큰 사용 시 재사용 탐지하고 세션 강제 종료
  • Key Rotation: kid와 JWKS로 주기적 키 교체
  • 전달 위치: Authorization: Bearer <access_token> 헤더 권장(쿠키 자동 전송에 의존하지 않음 → CSRF 표면 축소)

예시 (Spring Security)

@Bean
SecurityFilterChain api(HttpSecurity http) throws Exception {
    http
      .csrf(csrf -> csrf.disable()) // 순수 Bearer 토큰 기반 API는 일반적으로 CSRF 비적용
      .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
      .oauth2ResourceServer(oauth2 -> oauth2
          .jwt(jwt -> jwt
              .jwkSetUri("https://idp.example.com/.well-known/jwks.json")
          )
      );
    return http.build();
}

5. 브라우저 스토리지: localStorage·sessionStorage·IndexedDB

항목localStoragesessionStorageIndexedDB
수명반영구(삭제 시까지)탭/창 세션 동안반영구(삭제 시까지)
접근JS로 직접 접근 가능JS로 직접 접근 가능JS로 직접 접근 가능(비동기)
용량수 MB수 MB수십~수백 MB
동기/비동기동기동기비동기
보안XSS에 취약(비밀 보관 금지)XSS에 취약XSS에 취약
용도비민감 설정/캐시탭 한정 임시 데이터오프라인 캐시/대용량 데이터

원칙: 민감 비밀(Access/Refresh Token, 세션 키 등)을 브라우저 스토리지에 저장하지 않습니다.

6. CSRF, XSS와의 관계

  • CSRF: “사용자 브라우저가 의도치 않게 인증된 요청을 보내는” 공격
    • 해결: SameSite=Lax/Strict + CSRF 토큰(동기화 토큰, Double Submit Cookie) + 쿠키 범위 최소화.
    • Bearer 헤더 기반 API는 자동 전송이 없으므로 CSRF 표면이 상대적으로 작음.
  • XSS: 신뢰되지 않은 스크립트가 DOM에서 실행됨
    • 해결: CSP(Content Security Policy), 입력 검증/출력 인코딩, 템플릿 이스케이프, 라이브러리 최신화.
    • 브라우저 스토리지/비HttpOnly 쿠키에 저장된 비밀은 XSS에 취약합니다.

Spring에서 쿠키 기반 폼 로그인이라면 CSRF 기본 활성 유지 권장. SPA라면 프록시/BFF 뒤에서 쿠키 + CSRF 토큰 헤더 전략이 안정적입니다.

7.SPA와 BFF(Backend For Frontend)

문제: SPA가 직접 API에 호출하고 토큰을 브라우저가 관리하면 XSS·토큰 유출 표면 증가.
해법(권장): BFF가 브라우저와 동일 출처로 동작하고, HttpOnly 쿠키를 통해 세션/토큰을 관리합니다.

  • 브라우저 ↔ BFF(동일 도메인): 쿠키 기반(자동 전송)
  • BFF ↔ 리소스 API: 내부 네트워크에서 Bearer 토큰 사용
  • 이점: 브라우저는 토큰을 직접 보관하지 않음, CSRF·CORS 제어가 쉬움

8. OAuth 2.1 / OIDC 맥락의 권장 플로우

  • Authorization Code + PKCE(공개 클라이언트, SPA/모바일)
  • Access Token(짧게) + Refresh Token(길게, 회전)
  • Refresh Token은 HttpOnly·Secure·SameSite 쿠키로 브라우저에 보관하거나, 아예 BFF 서버 세션에 보관
  • 리소스 서버는 aud/iss/exp/nbf 검증, 스코프 기반 인가

9. 실전 설계 레시피

서버 렌더링 + 세션(전통적, 안정)

  • 로그인 성공 → 서버 세션에 사용자 컨텍스트 저장, JSESSIONID 쿠키 발급
  • Redis 세션 공유 + SameSite=Lax + Secure + HttpOnly
  • CSRF 기본 활성, 로그인 직후 세션 ID 재발급
  • 장점: 무효화 쉽고 구현 단순. 대규모 트래픽은 Redis 필요.

SPA + BFF(권장)

  • BFF가 HttpOnly 쿠키로 사용자 세션/토큰 관리, 프론트는 Fetch 시 자격증명 포함
  • CSRF 토큰 헤더(예: X-CSRF-TOKEN) + SameSite
  • 리소스 서버는 순수 Bearer 토큰 검증
  • 장점: 프론트가 토큰 비밀을 직접 다루지 않음.

SPA 직접 토큰(불가피할 때만)

  • Access는 메모리 저장, 페이지 리로드 시 재인증(또는 짧은 수명의 Refresh를 HttpOnly 쿠키로 보관하여 재발급 전용)
  • localStorage/sessionStorage에 비밀 보관 금지
  • CSP/XSS 방어를 매우 엄격히 적용
profile
Backend engineer

0개의 댓글