한 줄 요약: HTTP 인증은 Authorization 헤더로 자격 증명을 전달하고, 쿠키의 HttpOnly·Secure·SameSite 속성으로 세션 탈취 공격을 방어한다.

비유로 이해하는 인증과 세션

놀이공원 입장 시스템을 생각해보자.

  • 최초 입장 = 로그인 (티켓 구매 → 팔찌 수령)
  • 팔찌 = 세션 쿠키 (가지고 있으면 어디든 입장 가능)
  • 팔찌 확인 = 서버의 세션 검증
  • 팔찌 분실 = 세션 만료 또는 탈취

팔찌(쿠키)만 있으면 누구든 입장할 수 있다. 그래서 팔찌가 타인에게 넘어가지 않도록 보호하는 것이 핵심이다.


인증(Authentication) vs 인가(Authorization)

구분 의미 HTTP 헤더 실패 코드
인증(Authentication) 당신이 누구인지 확인 Authorization 401 Unauthorized
인가(Authorization) 당신이 무엇을 할 수 있는지 확인 (별도 없음) 403 Forbidden
sequenceDiagram
    participant C as "클라이언트"
    participant S as "서버"
    Note over C,S: "인증 실패 (401)"
    C->>S: "1. GET /mypage (로그인 안 함)"
    S-->>C: "2. 401 Unauthorized\nWWW-Authenticate: Bearer realm='api'"
    Note over C,S: "인가 실패 (403)"
    C->>S: "3. GET /admin (일반 사용자로 로그인)"
    S-->>C: "4. 403 Forbidden (로그인은 됐지만 권한 없음)"

Authorization 헤더 인증 방식

Basic 인증

사용자 ID와 비밀번호를 Base64로 인코딩해서 전송한다.

Authorization: Basic dXNlcjpwYXNzd29yZA==
# 디코딩: user:password
graph LR
    C["클라이언트"] -->|"GET /protected"| S["서버"]
    S -->|"401 + WWW-Auth"| C
    C -->|"base64 인코딩"| C
    C -->|"Authorization:Basic"| S
    S -->|"200 OK"| C

주의: Base64는 암호화가 아니라 인코딩이다. 반드시 HTTPS와 함께 사용해야 한다.


Bearer 토큰 인증 (JWT)

현재 API 인증에서 가장 널리 사용되는 방식이다.

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0IiwibmFtZSI6IuupleuqqeyeqiIsImV4cCI6MTcwMDAwMDAwMH0.signature

JWT 구조:

헤더.페이로드.서명
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9    ← 헤더 (알고리즘, 타입)
.
eyJzdWIiOiIxMjM0IiwibmFtZSI6IuupleuqqeyeqiIsImV4cCI6MTcwMDAwMDAwMH0    ← 페이로드 (사용자 정보)
.
signature    ← 서명 (위변조 검증)

세션 방식 vs JWT 방식:

항목 세션 방식 JWT 방식
상태 저장 서버(DB/Redis)에 저장 클라이언트에 저장
확장성 서버 증설 시 세션 공유 필요 서버 무상태(Stateless)
보안 서버에서 즉시 무효화 가능 만료 전 무효화 어려움
크기 sessionId만 전송 (작음) 토큰 자체가 큼

쿠키 상세 — Stateless 극복

쿠키 미사용 시 문제

graph LR
    U["사용자"] -->|"POST /login"| S["서버(Stateless)"]
    S -->|"200 OK"| U
    U -->|"GET /welcome"| S
    S -->|"안녕하세요 손님!(기억 못함)"| U

쿠키 사용 시 해결

graph LR
    U["브라우저"] -->|"POST /login"| S["서버"]
    S -->|"Set-Cookie: sid"| U
    U -->|"GET /welcome+Cook"| S
    S -->|"안녕하세요 홍길동님!"| U
    U -->|"GET /orders+Cookie"| S
    S -->|"주문 목록"| U

쿠키 생명주기 상세

세션 쿠키

Set-Cookie: tempPref=dark

Expires 또는 Max-Age를 지정하지 않으면 세션 쿠키가 된다. 브라우저 탭을 닫거나 브라우저를 종료하면 삭제된다.

영속 쿠키

# Expires: 절대 만료 날짜 지정
Set-Cookie: userId=hong; Expires=Thu, 01 Jan 2026 00:00:00 GMT

# Max-Age: 상대적 유효 시간 (초 단위, Expires보다 우선)
Set-Cookie: userId=hong; Max-Age=604800    # 7일
Set-Cookie: userId=hong; Max-Age=0         # 즉시 삭제

쿠키 보안 속성 심화

HttpOnly — XSS 방어

Set-Cookie: sessionId=abc123; HttpOnly
// HttpOnly 없을 때: 스크립트로 쿠키 접근 가능
document.cookie  // "sessionId=abc123" ← 탈취 위험

// HttpOnly 있을 때
document.cookie  // "" ← 접근 불가

XSS 공격자가 악성 스크립트를 삽입해도 쿠키를 읽을 수 없다.

Secure — HTTPS 전용

Set-Cookie: sessionId=abc123; Secure
  • HTTPS 연결에서만 쿠키를 전송한다
  • HTTP 평문 전송 시 쿠키가 포함되지 않아 도청 방지

SameSite — CSRF 방어

CSRF(Cross-Site Request Forgery)는 다른 사이트에서 사용자 브라우저를 이용해 요청을 위조하는 공격이다.

graph LR
    U["사용자"] -->|"로그인"| B["bank.com"]
    U -->|"evil.com 방문"| E["악성 사이트"]
    E -->|"위조 요청 유도"| U
    U -->|"Cookie 자동 포함"| B
    B -->|"정상 처리 → 이체 실행!"| X["피해"]

SameSite로 방어:

# Strict: 외부 사이트에서 오는 요청에 쿠키 미포함
Set-Cookie: sessionId=abc; SameSite=Strict

# Lax: 안전한 메서드(GET) + 최상위 탐색에만 전송 (현재 기본값)
Set-Cookie: sessionId=abc; SameSite=Lax

# None: 항상 전송 (Secure 필수)
Set-Cookie: sessionId=abc; SameSite=None; Secure
SameSite 값 외부 사이트 링크 클릭 외부 사이트 이미지 요청 외부 사이트 폼 POST
Strict X X X
Lax O X X
None O O O

실무 보안 쿠키 설정

Set-Cookie: sessionId=abc123;
           Expires=Thu, 01 Jan 2026 00:00:00 GMT;
           Path=/;
           Domain=.example.com;
           Secure;
           HttpOnly;
           SameSite=Lax

Spring Boot에서의 쿠키 보안 설정

// application.yml
server:
  servlet:
    session:
      cookie:
        http-only: true
        secure: true
        same-site: lax
        max-age: 3600  # 1시간

// 직접 쿠키 생성 시
@PostMapping("/login")
public ResponseEntity<Void> login(@RequestBody LoginDto dto,
                                   HttpServletResponse response) {
    String sessionId = authService.login(dto);

    Cookie cookie = new Cookie("sessionId", sessionId);
    cookie.setHttpOnly(true);
    cookie.setSecure(true);
    cookie.setPath("/");
    cookie.setMaxAge(3600);
    response.addCookie(cookie);

    return ResponseEntity.ok().build();
}

로그아웃 처리

sequenceDiagram
    participant C as "클라이언트"
    participant S as "서버"
    participant D as "세션 저장소"
    C->>S: "1. POST /logout\nCookie: sessionId=abc123"
    S->>D: "2. sessionId=abc123 세션 삭제"
    S-->>C: "3. 200 OK\nSet-Cookie: sessionId=; Max-Age=0; HttpOnly"
    Note over C: "4. 쿠키 즉시 삭제"

웹 스토리지 vs 쿠키

쿠키 외에 브라우저가 데이터를 저장하는 방법이 두 가지 더 있다.

항목 쿠키 localStorage sessionStorage
서버 전송 자동 전송 전송 안 함 전송 안 함
만료 Expires/Max-Age 영구 (수동 삭제) 탭 닫으면 삭제
크기 4KB 5~10MB 5~10MB
보안 HttpOnly로 JS 접근 차단 가능 JS로 항상 접근 가능 JS로 항상 접근 가능
용도 인증, 세션 사용자 설정, 캐시 임시 상태

핵심 포인트 정리

  • 인증(401) 은 “로그인 해주세요”, 인가(403) 는 “권한이 없습니다”다
  • Basic 인증은 HTTPS 없이 사용하면 위험하다. ID/PW가 Base64로 노출된다
  • JWT(Bearer)는 서버가 상태를 저장하지 않아 수평 확장에 유리하다
  • HttpOnly는 XSS 공격으로부터, SameSite는 CSRF 공격으로부터 쿠키를 보호한다
  • Secure 속성은 HTTPS에서만 쿠키가 전송되도록 강제한다
  • 실무에서 세션 쿠키에는 반드시 HttpOnly + Secure + SameSite=Lax 를 적용한다

함께 읽으면 좋은 글

카테고리:

업데이트:

댓글

이 글이 도움이 됐다면?

같은 카테고리의 다른 글도 확인해보세요

더 많은 글 보기 →