회원가입과 로그인을 구현하는 과정에서 인증 처리를 하는 방식에는 크게 세션방식과 토큰방식(JWT)이 있다. 대부분 프로젝트에서는 JWT를 사용하는 것을 많이 봤고, 나 또한 JWT를 사용했었다. 문득 JWT가 더 선호되는 이유가 궁금해져 세션과 토큰방식의 차이점을 정리해보려고 한다.
HTTP는 기본적으로 무상태(stateless) 프로토콜이기 때문에, 서버는 방금 요청을 보낸 사람을 기억하지 못한다. 로그인이라는 기능은 본질적으로 이 사용자가 누구인지 계속 기억하고 있어야 성립하기 때문에, 상태를 유지해야 한다. 상태를 유지하는 방식 중 하나가 세션과 JWT 방식이다.
✅ 세션과 JWT 비교
세션 방식

세션방식은 서버에서 사용자의 상태를 저장해두고, 클라이언트에게는 그 정보를 가리키는 무의미한 식별자만 쥐여주는 방식이다.
로그인 단계
클라이언트가 로그인을 시도하면, 서버는 그 자격증명이 맞는지 DB에서 사용자를 조회해 확인한다. 확인되면 서버는 이 사용자를 위한 세션을 하나 만들고, 그 세션에 사용자 정보를 담아 서버측 세션저장소에 보관한다. 세션을 만들 때 이 세션을 식별할 세션ID를 함께 발급하고, 클라이언트로 응답을 보낼 때 이 세션ID를 Set-Cookie 헤더에 실어 클라이언트로 보낸다.
이때 세션ID는 아무 의미 없는 난수로 발급되며, 쿠키에 넣는 이유는 브라우저가 저장과 전송을 자동으로 해주기 때문이다. 개발자가 코드를 쓰지 않아도 알아서 저장하고 다음 요청에 알아서 동봉한다.
로그인 이후 단계
이후 클라이언트가 같은 서버로 요청을 보낼 때마다 브라우저는 저장해둔 세션ID 쿠키를 Cookie 헤더에 자동으로 붙여서 보낸다. 서버는 해당 세션ID를 키로 저장소를 뒤져 세션을 찾고, 해당 사용자 정보를 복원한다. 복원에 성공하면 해당 사용자를 인식하는 과정을 반복하면서 HTTP위에서 로그인 상태가 유지되는 것처럼 보이게 된다.
세션 저장소는 설정에 따라 달라지지만, 기본값은 톰캣의 메모리 힙에 저장된다. 그렇기 때문에 서버를 재시작하면 세션이 전부 사라진다는 단점이 있고, 각 서버가 자기 메모리에만 세션을 갖고있기 때문에, 다중 서버 환경이라면 세션을 못찾는 문제가 생길 수 있다.
이러한 문제를 해결하기 위해 Redis 같은 외부 저장소에 저장한다. 모든 서버가 같은 Redis를 바라보게 하면 어느 서버로 요청이 가던 세션을 찾을 수 있게 된다.
JWT 방식

사용자의 상태를 서버가 가지고있는 세션과 달리, JWT는 사용자 정보를 토큰 자체에 담아 클라이언트에게 통째로 맡기는 방식이다. 서버는 그 정보를 따로 보관하지 않고, 클라이언트가 요청할 때 보내는 토큰이 위조되지는 않았는지만 검증한다.
로그인 단계
클라이언트가 로그인을 시도하면, 서버는 그 자격증명이 맞는지 DB에서 사용자를 조회해 확인한다. 확인되면 사용자 정보를 담은 토큰을 생성하고, 서버의 비밀키로 서명한다. JWT 구조는 아래 링크에 간단히 정리해두었다.
https://wanna-developer02.tistory.com/166
[시큐리티] RSA, JWT 기본개념 정리
스프링 시큐리티를 공부하기 앞서 보안과 관련된 개념을 정리하고자 한다. 목차는 다음과 같다.RSA 암호화공개키와 개인키JWT에 대하여RFC?JWT 구 데이터를 전달할 때 발생하는 보안이슈를 해결
wanna-developer02.tistory.com
이때 세션과 달리, 보통 응답 바디나 헤더에 담아서 클라이언트로 보내고, 서버에는 저장하지 않는다.
로그인 이후 단계
이후 요청마다 클라이언트는 보관해둔 토큰을 직접 헤더 (Authorization) 에 실어 보낸다. 쿠키에 들어있지 않기 때문에 클라이언트 코드가 수동으로 챙겨야한다! 서버는 토큰의 서명을 검증하여 일치하면 사용자를 신뢰하게 된다. 클라이언트에서 정보를 가지고있기 때문에, DB나 별도의 저장소를 조회하지 않는다.
서버에서 별도로 저장하지 않기 때문에, 어디에 저장할지에 대한 고민은 하지 않아도 괜찮다. 다만, JWT의 경우 상태를 서버에서 관리하지 않기 때문에 토큰을 한 번 발급하고 나면 무효화를 하기 어렵다는 문제점이 있다. 탈취를 당하더라도 해당 토큰을 무효화 할 수 없는 것이다.
✅ JWT 보안 취약점 비교하기
인증 과정에서의 보안 취약점을 이야기 할 때 주로 xss와 csrf가 거론된다. 각각을 먼저 알아보자.
XSS (Cross-Site Scripting)
xss는 공격자가 악성 자바스크립트를 심고, 그 자바스크립트가 피해자의 브라우저에서 실행되는 공격이다. 브라우저 입장에서는 그 사이트가 보낸 정상 스크립트와 악성 스크립트를 구분하지 못하기 때문에 발생한다.
xss의 공격 유형
- Stored (저장형) : 악성 스크립트가 DB에 저장됐다가 그 데이터를 보는 모두에게 실행
- Reflected (반사형) : 요청에 담긴 값이 응답에 그대로 되돌아오면서 실행됨 (ex| 악성 링크 클릭 유도)
- DOM-based : 서버를 거치지 않고 클라이언트 js가 URL 등의 값을 DOM에 직접 꽂으면서 실행. 서버 응답에는 흔적이 없기 때문에 탐지가 까다로움
XSS는 스크립트가 피해자 브라우저에서 돌기 때문에, JS로 접근 가능한 건 다 빼갈 수 있다. 즉, 로컬스토리지에 저장된 JWT 정도는 쉽게 탈취가 가능하다는 이야기이다.
방지 대책
- 출력 이스케이프 : HTML을 그대로 사용하지 않고 특수문자를 무력한 글자로 변환하여 그대로 실행되는 것을 방지
- 보통 요즘 프레임워크는 이를 기본으로 해주기 때문에 자동 이스케이프를 끄는 위험한 기능을 쓰지 않으면 된다
- ex| thymeleaf 의 th:utext는 이스케이프를 끄므로 th:text를 사용
- 보통 요즘 프레임워크는 이를 기본으로 해주기 때문에 자동 이스케이프를 끄는 위험한 기능을 쓰지 않으면 된다
- CSP (Content-Security-Policy) : 응답헤더로 이 페이지에서 실행 가능한 스크립트의 출처를 브라우저에 선언하는 것
- 악성 스크립트가 들어와도 실행 자체를 막아줌
- 설정이 까다로워서 보강 용도로 보통 사용함
- 액세스 토큰 수명을 짧게 하고, 리프레시 토큰 로테이션으로 재사용 탐지
리프레시토큰은 액세스토큰보다 수명이 길어 더 위험하기 때문에 읽지 못하도록 쿠키에 넣어두는 방식으로 설계한다. 그래서 이렇게 구현할 경우, 리프레시토큰은 xss 공격에는 강하지만 csrf 공격에는 취약해진다.
CSRF (Cross-Site Request Forgery)
로그인된 피해자의 브라우저를 속여 피해자가 의도하지 않은 요청을 신뢰된 사이트로 보내게 만드는 공격이다. 한마디로 악성 코드를 실행하는 건 아니지만, 피해자가 의도하지 않은 요청을 피해자의 이름으로 보내게 하는 것이다.
앞서 이야기 했듯, 브라우저는 해당 도메인으로 요청을 보낼 때 도메인의 쿠키를 자동으로 붙인다.그래서 사용자가 로그인된 상태에서 공격자가 만든 악성 페이지에 들어가면 그 페이지가 사용자가 로그인한 페이지에 요청을 쏘게 하고, 브라우저는 친절히 쿠키를 자동으로 동봉한다. 이렇게 되면 서버는 유효한 쿠키가 왔기 때문에, 요청을 처리해버리게 된다. 이 점을 이용하면 원치않은 송금을 해버릴 수도 있다!
방지 대책
- SameSite 쿠키 속성 : SameSite 속성을 Lax나 Strict로 두면 브라우저가 외부 사이트 요청에는 쿠키를 자동으로 붙이지 않아 CSRF 공격을 막을 수 있다.
- CSRF 토큰 : 서버가 예측 불가한 토큰을 발급해 페이지에 심어두어 요청시 해당 토큰을 함께 보내게 하는 방식
- 공격자의 외부 페이지는 해당 토큰을 알 수 없기 때문에 위조 요청을 만들 수 없게 된다
프로젝트의 구조마다 트레이드오프가 존재하기 때문에, 본인 프로젝트를 보고 방식을 선택해야 할 거 같다!
'백엔드 > 스프링 Security' 카테고리의 다른 글
| [시큐리티] RSA, JWT 기본개념 정리 (3) | 2024.09.30 |
|---|