필요성
우리가 흔히 쓰는 HTTP(S) 프로토콜은 기본적으로 서버가 클라이언트의 이전 요청 상태를 저장하지 않는다는 무상태성을 가집니다. 이러한 무상태성에 따르면 방금 분명 로그인을 했는데도 불구하고 로그인 된 상태가 유지되지 않아요. 만약 매 페이지에 접근할 때마다 새로 로그인을 해야하는 서비스가 있다면 그 서비스가 굉장히 좋은 서비스여도 절대 사용하고 싶지 않을 거예요.
그렇다면 매번 로그인 요청을 다시 보내야 하는 걸까요? 너무 비효율적이진 않을까요? 그래서 사용할 수 있는 방법이 세션과 쿠키입니다. 즉, 세션과 쿠키는 사용자의 상태를 어디엔가 저장하기 위한 방법이에요. 여기서 ‘어디에’ 저장하느냐에 따라 둘을 구분지을 수 있습니다.
쿠키와 세션
쿠키는 클라이언트에 정보를 저장하고, 세션은 서버에 정보를 저장합니다. 여기서 정보란 핵심 정보를 말합니다.
로그인 기능을 예로 들어 볼게요.
쿠키
- 사용자가 로그인 폼을 제출한다.
- 서버에서 로그인 여부를 판단하고, 성공시 사용자 정보를 쿠키에 저장해서 응답한다.
- 브라우저는 그 쿠키를 저장한다.
- 이후 브라우저가 서버에 요청을 보낼 때마다 해당 쿠키가 자동으로 전송된다.
- 서버는 쿠키를 보고 사용자가 누구인지 식별할 수 있다.
세션
- 사용자가 로그인 폼을 제출한다.
- 서버에서 로그인 여부를 판단하고, 성공시 세션을 생성한 뒤 세션ID를 발급한다.
- Set-Cookie: sessionId=abc123 과 같은 헤더로 클라이언트에 전송한다.
- 브라우저는 sessionId를 쿠키에 저장한다.
- 이후 모든 요청에 sessionId를 자동으로 전송한다.
- 서버는 이 ID를 보고 서버에 저장된 세션 데이터를 꺼내 사용자를 식별한다.
몇 가지 의문점 해소하기
✅ 쿠키는 자동으로 전송된다.
브라우저는 현재 열려 있는 페이지의 도메인과 일치하는 쿠키가 있다면, 매 요청마다 그 쿠키를 HTTP 요청 헤더에 자동으로 포함시킵니다. 개발자가 API 요청을 보낼 때 따로 쿠키를 넣지 않아도, 자동으로 보내준다는 점을 기억해주세요.
다만, 브라우저에 쿠키를 저장하는 것은 개발자(특히 서버)가 해야할 일입니다. 서버에서 Set-Cookie 헤더로 쿠키를 내려주면 브라우저는 자동으로 저장하고, 자동으로 전송합니다.
✅ 세션도 결국 쿠키에 세션ID를 저장하는 방식 아님?
맞아요. 세션 방식도 결국에는 쿠키에 무엇인가를 저장합니다. 다만 다음과 같은 차이가 있어요.
- 쿠키 방식: 사용자 정보 자체를 쿠키에 저장
- 세션 방식: 사용자 정보는 서버의 세션에 저장하고 해당 세션에 접근할 수 있는 열쇠인 세션ID만 쿠키에 저장
따라서 꼬롬한 마음을 먹은 누군가가 쿠키를 턴다면 둘 다 정보를 털릴 수 있는 건 매한가지입니다. 다만 한 번에 사용자 정보를 홀랑 털어가는가, 아님 세션ID를 탈취해 그것을 가지고 서버에 요청을 보내서 털어가는가의 차이가 있어요.
✅ 쿠키만 지키면 반은 먹고 들어간다.
쿠키에 저장된 정보를 털리지 않으면 쿠키 방식이든 세션 방식이든 문제 될 것이 없겠죠. 쿠키를 지키기 위한 방법이 몇 가지 있습니다.
- Secure 쿠키 사용: HTTPS 환경에서만 쿠키가 전송되게 함으로써 중간 공격자가 평문 그대로의 쿠키를 털어가는 일을 방지한다.
- HttpOnly 쿠키 사용: JS에서 쿠키에 접근하지 못하게 막는다.
- IP/UA 검사: 서버에서 세션 요청마다 사용자의 IP, 브라우저 정보 등을 검사한다. 네이버나 구글에서 평소와 다른 IP로 로그인 했을 때 본인 인증을 거치는게 이 방식이다. (단, IP는 VPN을 사용하거나 모바일 환경이면 자주 바뀔 수 있어 완벽한 보안책은 아니다.)
- SameSite 쿠키 설정: 다른 사이트에서 쿠키를 전송하지 못하게 제한해서 CSRF 공격 방지
✅ 그럼 쿠키 방식이 세션 방식보다 무조건 나은 거 아님?
쿠키만 지키면 되고, 서버 부담 없고, 아니 그럼 세션 방식 왜씀?! 돈도 아낄겸 쿠키 잘 지켜서 쿠키 방식 쓰면 이득 아님?! 하는 생각이 드는데요. 아쉽게도 쿠키 방식에는 실질적인 위험이 있습니다.
- 일단 정보를 쿠키에 저장한다는 점 (암호화 해도 해독한다면 어쩔건지?)
- 인증 취소를 서버에서 할 수 없다는 점(누군가 홀라당 털어먹고있어도 손 놓고 보기만 해야한다는 점)
- 사용자가 정보를 조작했을 때 서버의 검증 방법이 까다로워짐(세션 방식은 sessionId를 서버 DB나 Redis에서 조회해서 확인하면 되기에 비교적 간단함)
따라서 보안이 얼마나 중요한가, 그리고 내가 서버에 얼마나 돈을 쓸 수 있는가에 따라 둘 중 판단해서 사용하면 좋을 것 같습니다.
JWT 기반 인증
앞서 쿠키 방식을 사용하면 서버의 검증 방법이 까다롭다고 했죠. 서버에서 직접 정보를 관리하며 키를 건네준 것이기 아니기 때문에 서버 입장에서는 해당 데이터가 위변조 되지 않았는지 판단하기가 어렵습니다. 이런 아쉬운 점을 해결하기 위해 등장한 것이 JWT(JSON Web Token)입니다. 즉, JWT는 쿠키 방식의 위변조 취약성을 해결하기 위해 나온 구조입니다. 또한 JWT 토큰의 경우 인증된 사용자임을 증명하기 위해 서버가 직접 서명된 토큰을 만들어주는 방식인지라 좀 더 안전합니다.
기존 쿠키방식은 아래와 같이 userId와 같은 민감한 정보를 평문으로 담아 전송하는 구조예요.
Cookie: userId=1&role=admin
이런 식이라면, JWT 방식은
{
"header": { alg: "HS256", typ: "JWT" },
"payload": { "userId": 1, "role": "admin" },
"signature": HMACSHA256(base64Url(header) + "." + base64Url(payload), secret)
}
이런 식으로 진짜 정보는 payload에 담고, 서명을 따로 두어 정보를 바꾸더라도 서명 불일치로 거부되도록 합니다.
하지만 서명만 서버에서 만들어주는 것일뿐, 쿠키방식과 동일하기에 토큰이 살아 있는 한 강제로 만료시키기가 어렵습니다. 서버에서 세션을 기억하고 있지 않으니까요.
JWT에서는 로그아웃을 어떻게 구현할 수 있을까?
토큰이 살아 있는 한 무효화가 어렵다고 했죠. 그러면 클라이언트에서 토큰만 홀랑 지워버리면 되지 않을까요?
아쉽게도 그렇지 않습니다. 서버가 상태를 기억하고 있지 않아서, 클라이언트에서 토큰을 지우더라도 서버는 여전히 해당 정보가 유효한 줄 알고 있습니다. 그래서 이를 해결하기 위한 방법이 몇가지 있습니다.
- 블랙리스트 방법: 로그아웃한 토큰의 고유ID를 서버에 저장하는 방식. 확실하지만, 서버 용량을 잡아먹으니 사실상 세션을 사용하는 거와 다를 바가 없다.
- Access + Refresh Token 조합: 로그인 시 두개의 토큰을 모두 발급하고, Access Token 만료 시 Refresh 토큰으로 새로 발급하는 구조다. 로그아웃 시 Refresh Token을 DB에서 삭제함으로써 강제 만료를 구현한다.
실무에서는 Access Token과 Refresh Token을 HttpOnly Secure 쿠키에 저장하는 방식이 많이 쓰인다고 합니다.