본문으로 건너뛰기

버그 제보

이상한 점을 알려 주시면 고칠게요. 지금 보고 있는 페이지와 브라우저 정보가 함께 담겨요.

페이지:

Backend

세션 기반 인증과 토큰 기반 인증

작성 2026. 10. 17.추가
목차11개 항목
출처와 참고

팔만코딩경(42Seoul 커뮤니티 위키)의 「세션과 토큰 기반 인증방식」을 바탕으로 재구성하고, G마켓 기술 블로그의 「인증/인가는 어디에 어떻게 구현해야 할까?」(2022.09)로 다중 서버 세션 부분을 보탠 노트다. 신원 관리 방식의 계보, JWT 서명 방식, PASETO 부분은 ByteByteGo의 System Design 101 가이드 「Token, Cookie, Session」, 「JWT 101」, 「Cookies vs Sessions vs JWT vs PASETO」 등을 바탕으로 재구성해 보탰다. 무상태 HTTP와 세션의 관계, 서명 계산식, 토큰 탈취 대응은 「fru1tworld/cs-basic 모의 면접 질문 목록」(네트워크, 기타)의 질문을 기준으로 빠진 개념을 보강했다.

왜 인증 방식이 필요한가

HTTP는 상태가 없다(stateless). 요청마다 "이 사람이 아까 로그인한 그 사람"이라는 증거를 다시 보내야 한다. 그 증거를 어디에 저장하고 무엇으로 검증하느냐에 따라 세션 방식과 토큰 방식이 갈린다.

쿠키 자체의 동작(속성, 수명, 퍼스트·서드파티)은 쿠키와 세션에 있다. 이 노트는 서버 쪽에서 두 방식을 고르는 기준을 다룬다.

구분세션 기반토큰 기반(JWT)
상태 저장 위치서버(메모리, Redis, DB)토큰 자체(클라이언트가 보관)
클라이언트가 보내는 것의미 없는 세션 ID(보통 쿠키)사용자 정보와 서명이 담긴 토큰(Authorization: Bearer 또는 쿠키)
서버 검증 방법세션 저장소 조회서명과 만료 시각 검증, 저장소 조회 없음
강제 로그아웃저장소에서 지우면 즉시 끝만료 전까지는 어렵다(별도 차단 목록 필요)
수평 확장서버 간 세션 공유 필요(Redis 등)비밀키·공개키만 공유하면 된다
요청 크기작다토큰이 길어 요청마다 수백 바이트 이상
잘 맞는 곳같은 도메인의 웹 앱, 즉시 무효화가 중요한 서비스모바일 앱, 여러 서비스가 같은 신원을 쓰는 API, MSA

신원 관리 방식의 계보

로그인은 세 단계로 나뉜다. 식별(identification) 은 "나는 누구다"라고 밝히는 것, 인증(authentication) 은 그 주장이 맞는지 확인하는 것, 인가(authorization) 는 확인된 사용자에게 권한을 주는 것이다. 아래 방식들은 대부분 앞의 둘을 다루고, OAuth 2.0만 인가 위임을 다룬다. 단순한 것부터 차례로 쌓였다.

방식동작해결한 것남은 한계
HTTP Basic (WWW-Authenticate)브라우저가 띄운 창에 아이디·비밀번호를 넣고, 요청마다 Base64로 실어 보낸다가장 단순한 인증로그아웃·만료 같은 로그인 수명 주기를 서버가 제어할 수 없다. 내부 도구 외에는 거의 쓰지 않는다
세션-쿠키서버가 세션을 저장하고 브라우저는 세션 ID만 쿠키로 든다로그인 수명을 서버가 제어쿠키는 브라우저 중심이라 모바일 앱·여러 도메인에서 다루기 번거롭다
토큰신원을 담은 토큰을 클라이언트가 들고 Authorization 헤더로 보낸다브라우저 밖 클라이언트와 호환토큰 형식·검증 방식이 제각각이다
JWT토큰 형식을 표준화하고 서명을 토큰 안에 넣는다서버가 세션 저장 없이 검증즉시 무효화가 어렵다(아래 대책 참고)
SSO중앙 인증 서버 한 곳에 로그인하면 여러 서비스가 그 결과를 믿는다한 번 로그인으로 여러 사이트중앙 서버가 단일 장애점이 된다 → OAuth 2.0의 SSO 섹션
OAuth 2.0비밀번호 대신 범위가 제한된 토큰으로 다른 서비스의 자원 접근을 위임제3자 앱에 비밀번호를 주지 않는다인증은 그 위의 OIDC가 맡는다 → OAuth 2.0
QR 코드 로그인PC 화면의 QR에 일회용 난수 토큰을 담고, 이미 로그인된 휴대폰이 스캔해 그 토큰을 승인한다PC에서 비밀번호를 치지 않는다토큰 수명을 짧게, 승인 화면에 어느 기기·위치의 요청인지 보여 줘야 피싱을 막는다

세션 기반 인증

sequenceDiagram
  participant B as 브라우저
  participant S as 서버
  participant R as 세션 저장소
  B->>S: 1. POST /login (아이디·비밀번호)
  S->>R: 2. 검증 성공 → { sessionId: userId, 만료 } 저장
  S-->>B: 3. Set-Cookie: sid=abc123#59; HttpOnly#59; Secure#59; SameSite=Lax
  B->>S: 4. 이후 요청: Cookie: sid=abc123 (브라우저가 자동 첨부)
  S->>R: 5. 저장소에서 sid 조회
  Note over S: 사용자 확인
  S-->>B: 응답

세션은 HTTP의 무상태성과 모순되지 않는다. 무상태는 HTTP 프로토콜이 요청 사이를 기억하지 않는다는 뜻이고, 세션은 그 위의 응용이 상태를 따로 저장하는 것이다. 요청은 여전히 세션 ID라는 필요한 정보를 매번 담고 오며, 서버는 그 ID로 저장소를 찾아 처리한다. 대가는 무상태가 주던 장점 일부를 다시 내주는 것이다. 웹 서버 자체는 무상태로 두고 상태를 공용 세션 저장소 한 곳에 모으면, 서버를 늘리는 자유는 지키고 저장소의 가용성만 따로 책임지면 된다. 토큰 방식은 상태를 클라이언트 쪽 토큰에 넣어 저장소마저 없앤 것이지만, 즉시 무효화를 위해 결국 일부 상태(차단 목록, 리프레시 토큰)를 다시 둔다.

  • 세션 ID는 무작위 문자열이라 유출돼도 개인정보는 드러나지 않는다. 다만 탈취하면 그 사용자로 위장할 수 있다(세션 하이재킹).
  • 로그인 직후 세션 ID를 새로 발급해 세션 고정(session fixation) 공격을 막는다.
  • 사용자마다 저장소 항목이 생기므로 동시 접속자가 많으면 저장소가 부담이 된다.
  • 세션 ID 대신 아이디·비밀번호를 쿠키에 담아 매번 보내는 방식은 쓰지 않는다. 저장소가 필요 없고 확장도 쉽지만, 요청마다 자격 증명이 오가므로 쿠키 하나만 새도 계정이 넘어간다. 세션은 이 문제를 「의미 없는 식별자」로 바꾼 것이다.

서버가 여러 대일 때

세션 저장소가 서버 메모리에 있으면(예: 톰캣의 기본 세션 관리자), 로드밸런서가 요청을 다른 서버로 보내는 순간 세션을 찾지 못한다.

방법동작대가
스티키 세션(sticky session)로드밸런서가 같은 사용자를 늘 같은 서버로 보낸다그 서버가 죽으면 세션이 사라진다. 부하가 고르게 퍼지지 않는다
세션 복제(session replication)서버끼리 세션을 서로 복사해 똑같이 유지한다서버가 늘수록 복제 트래픽과 메모리가 커진다
공용 세션 저장소Redis 같은 외부 저장소 하나를 모든 서버가 함께 쓴다저장소의 부하와 가용성만 신경 쓰면 된다. 가장 흔한 선택이다

토큰 기반 인증 (JWT)

JWT(JSON Web Token)는 header.payload.signature 세 부분을 .으로 이은 문자열이다.

부분내용
Header토큰 타입(JWT)과 서명 알고리즘(HS256, RS256 등). Base64URL 인코딩
Payload클레임(key-value). Base64URL 인코딩. 대표 클레임은 sub(대상, 보통 사용자 ID), exp(만료 시각), iat(발급 시각), jti(토큰 ID)
SignatureHeader + Payload를 서버의 키로 서명한 값. 위변조 검증용

Header와 Payload는 인코딩일 뿐이라 누구나 디코딩해 읽을 수 있다(jwt.io). 위변조를 막는 것은 Signature뿐이다. NestJS에서 Passport로 구현하는 방법은 NestJS JWT 인증과 Passport에 있다.

sequenceDiagram
  participant C as 클라이언트
  participant S as 서버
  C->>S: 1. POST /login
  Note over S: 2. 검증 성공 → Payload { sub: userId, exp } 에 비밀키로 서명
  S-->>C: Access Token 발급
  Note over C: 3. 토큰 보관
  C->>S: 4. 이후 요청: Authorization: Bearer #lt;token#gt;
  Note over S: 5. 서명 검증(HS256은 같은 비밀키, RS256·ES256은 공개키) + exp 확인
  S-->>C: 응답

서명은 Header와 Payload를 각각 Base64URL로 인코딩해 .으로 이은 문자열에 서명 알고리즘을 적용하고, 그 결과를 다시 Base64URL로 인코딩해 만든다.

signingInput = base64url(header) + "." + base64url(payload)
HS256:  signature = base64url( HMAC-SHA256(signingInput, 비밀키) )
RS256:  signature = base64url( RSA-PKCS1-v1_5-Sign(SHA-256(signingInput), 개인키) )
ES256:  signature = base64url( ECDSA-P256-Sign(SHA-256(signingInput), 개인키) )
JWT = signingInput + "." + signature
  • 서명은 복호화하는 것이 아니라 검증하는 것이다. 서버는 받은 Header·Payload로 서명을 다시 계산해 일치하는지 본다. 비밀키가 없으면 Payload를 고친 뒤 맞는 서명을 만들 수 없다.
  • Payload는 암호화되지 않는다. 비밀번호, 개인정보는 넣지 않는다.
  • 서버가 상태를 갖지 않으므로 확장이 쉽고, 여러 서비스가 같은 공개키로 토큰을 검증할 수 있다.

클레임의 종류

종류의미예
등록된 클레임(registered)표준(RFC 7519)이 이름과 뜻을 정해 둔 것. 선택 사항이지만 쓰면 뜻을 지킨다iss(발급자), sub, aud(대상 서비스), exp, nbf(이전엔 무효), iat, jti
공개 클레임(public)여러 시스템이 함께 쓰려고 IANA에 등록하거나 URI로 충돌을 피한 이름OIDC의 email, name
비공개 클레임(private)발급자와 소비자가 합의해 쓰는 이름role, tenantId

검증하는 쪽은 서명뿐 아니라 exp·nbf·iss·aud까지 확인한다. 특히 aud를 보지 않으면 다른 서비스용으로 발급된 토큰이 내 서비스에서 통한다.

서명 방식: 대칭과 비대칭

방식알고리즘 예서명 키 / 검증 키맞는 상황
대칭 서명HS256 (HMAC-SHA256)같은 비밀키 하나발급과 검증을 같은 서버(또는 신뢰하는 한 팀)가 할 때. 검증하는 모든 곳이 비밀키를 가져야 하므로 그중 하나만 새도 토큰을 위조할 수 있다
비대칭 서명RS256(RSA), ES256(ECDSA), EdDSA개인키로 서명, 공개키로 검증인증 서버 한 곳이 발급하고 여러 서비스가 검증할 때. 공개키는 JWKS(/.well-known/jwks.json)로 배포하고 kid로 키를 교체한다

검증할 알고리즘은 서버가 정해 둔다. 토큰 Header의 alg를 그대로 믿으면 alg: none(서명 없음)이나, RS256 공개키를 HS256 비밀키로 착각하게 만드는 알고리즘 혼동(algorithm confusion) 공격에 뚫린다. 라이브러리에 허용 알고리즘 목록을 명시한다.

PASETO: 설정 실수를 줄인 대안

PASETO(Platform-Agnostic Security Tokens) 는 JWT의 이런 함정을 없애려고 만든 토큰 형식이다. 알고리즘을 Header에서 고르게 하지 않고 버전(v4)과 용도(local·public)가 암호 방식을 고정한다.

구분JWT(JOSE)PASETO
알고리즘 선택토큰의 alg로 지정. 선택지가 많아 잘못 고르거나 검증을 빼먹기 쉽다버전이 정한다. 예: v4.local은 대칭 암호화, v4.public은 Ed25519 서명
페이로드서명만 하면 누구나 읽는다(암호화하려면 JWE를 따로 쓴다)local은 암호화까지 하므로 내용이 숨겨진다
none 같은 위험한 선택지표준에 있다없다
생태계OIDC·OAuth와 맞물려 사실상 표준라이브러리·IdP 지원이 JWT보다 좁다

토큰을 자체 시스템 안에서만 주고받는다면 PASETO가 실수 여지를 줄인다. OIDC처럼 외부 표준과 맞물리는 곳에서는 JWT를 쓰되 허용 알고리즘을 고정한다.

토큰 방식의 약점과 대책

토큰은 발급하면 만료될 때까지 유효하다. 탈취당하면 서버가 막을 방법이 없다는 것이 가장 큰 약점이다. 그래서 실무에서는 아래 대책을 조합한다.

대책방법대가
짧은 만료Access Token 수명을 수 분~수십 분으로혼자 쓰면 로그인이 너무 잦다
슬라이딩 세션활동 중인 사용자에게 만료를 자동 연장(글쓰기·결제 시작 때 재발급)계속 쓰는 탈취자도 연장된다
리프레시 토큰짧은 Access Token + 긴 Refresh Token. Access가 만료되면 Refresh로 재발급Refresh Token을 서버에 저장·관리해야 한다
리프레시 토큰 회전재발급할 때마다 Refresh Token도 새로 주고 옛것은 폐기. 이미 쓴 토큰이 다시 오면 탈취로 보고 그 계열 전체를 무효화구현이 복잡하다
차단 목록로그아웃·비밀번호 변경 시 토큰 ID(jti)를 만료 때까지 차단결국 서버 저장소 조회가 생긴다
서명 키 교체키가 샜거나 대량 탈취가 의심되면 서명 키를 바꿔(kid 교체) 그 키로 발급한 토큰을 한꺼번에 무효화모든 사용자가 다시 로그인한다
발신자 제약 토큰토큰을 클라이언트의 키에 묶는다. DPoP(RFC 9449)는 요청마다 클라이언트 개인키로 서명한 증명을 함께 보내게 해, 토큰만 훔쳐서는 쓸 수 없게 한다. 서버 간에는 mTLS 인증서에 묶는다클라이언트가 키를 관리해야 하고 지원하는 인가 서버가 아직 적다

리프레시 토큰을 서버에 두는 순간 "완전한 무상태"라는 JWT의 장점은 일부 사라진다. 대신 원할 때 강제로 로그아웃시킬 수 있다. 대부분의 서비스는 이 거래를 받아들인다.

탈취된 토큰의 종류에 따라 대응 순서가 다르다.

탈취된 것피해 범위대응
Access Token남은 수명(수 분~수십 분) 동안 그 권한(scope)으로 API 호출수명이 짧아 피해 시간이 제한된다. 급하면 jti 차단 목록에 올리고, 그 사용자의 리프레시 토큰 계열을 끊어 재발급을 막는다. 대량이면 서명 키를 교체한다
Refresh Token새 Access Token을 계속 발급받아 오래 위장서버가 저장한 리프레시 토큰을 폐기하고 사용자의 모든 세션을 끊는다. 회전과 재사용 감지가 있으면 정상 사용자와 공격자 중 늦게 쓴 쪽의 요청이 「이미 쓴 토큰」으로 걸려 계열 전체가 자동 폐기된다. 비밀번호 변경을 유도하고, 리프레시 토큰은 HttpOnly 쿠키·기기 바인딩으로 애초에 꺼내기 어렵게 둔다

공통으로, 이상 징후(다른 국가·기기에서의 재발급, 짧은 시간의 대량 호출)를 감지해 재인증을 요구하는 장치가 탈취를 알아채는 계기가 된다.

토큰을 어디에 보관하나 (브라우저)

보관 위치XSSCSRF비고
localStorage스크립트가 읽을 수 있어 취약자동 전송이 없어 안전구현은 가장 쉽다
메모리(JS 변수)새로고침하면 사라짐, 탈취 범위가 좁다안전Refresh Token 쿠키와 함께 쓴다
HttpOnly; Secure; SameSite 쿠키스크립트가 못 읽는다SameSite와 CSRF 토큰으로 방어브라우저 앱에서 권장되는 기본값

흔한 조합은 Access Token은 메모리, Refresh Token은 HttpOnly 쿠키다. 브라우저 전용 웹 앱이라면 처음부터 세션 쿠키를 쓰는 편이 단순하고 안전한 경우도 많다.

실제로 어디에 무엇이 저장됐는지(쿠키의 HttpOnly·Secure·SameSite, localStorage의 토큰)는 Chrome DevTools의 Application 패널에서 확인한다 → DevTools Application 패널.

정리

  • 세션은 상태를 서버에, 토큰은 상태를 토큰 안에 둔다. 차이는 즉시 무효화와 확장성 사이의 거래다.
  • 세션 ID는 의미 없는 열쇠라 유출돼도 정보는 안전하지만, 열쇠 자체를 훔치면 위장할 수 있다.
  • JWT의 Payload는 누구나 읽는다. 서명은 위변조 검증용이지 암호화가 아니다.
  • 검증 쪽은 허용 알고리즘을 고정하고 exp·iss·aud까지 확인한다. 설정 실수를 구조적으로 막고 싶으면 PASETO를 검토한다.
  • 토큰을 쓰면 짧은 Access Token과 회전하는 Refresh Token을 함께 쓰고, 브라우저에서는 HttpOnly 쿠키를 우선 고려한다.
  • 같은 도메인의 웹 앱만 있다면 세션, 모바일·다중 서비스 API라면 토큰이 기본 선택이다.

관련 노트: 쿠키와 세션, NestJS JWT 인증과 Passport, OAuth 2.0, 웹 보안: CSRF와 XSS, API 보안 체크리스트, Django User와 인증, 인증·인가 로직은 어디에 두나: 필터, 인터셉터, API 게이트웨이

원본 기록12

댓글

GitHub 계정으로 댓글을 남길 수 있어요.