Hyen Dev
쿠키, 세션, 토큰 (+ JWT 토큰) 본문
Auth 기능을 구현할 때마다 쿠키로 구현할지, 세션으로 구현할지, 토큰으로 구현할지 고민하는 과정을 거치는데,
그때마다 기억이 휘발되어 다시 찾아보는 과정을 거치는게 불편해서 그냥 이참에 제대로 정리를 해보려고한다.
인증 & 인가
인증(Authentication)
인증은 "신원을 확인하는 과정"으로, 간단한 예시로 ID와 password를 통해 로그인하는 행위가 있다.
인가(Authorization)
인가는 "접근을 허가 또는 거절하는 과정"으로, 인증된 사용자가 어떤한 자원에 접근할 수 있는지를 확인하는 절차이다.
HTTP의 특성
HTTP(HyperText Transfer Protocol)는 인터넷에서 데이터를 주고 받을 수 있는 프로토콜이며, 다음과 같은 특징을 가지고 있다.
Connectionless : 한 번 통신이 이뤄지고 난 후에 연결이 바로 끊어진다.
Stateless : 서버는 클라이언트의 이전 사앹를 유지/기억하지 않는다.
즉, 한 번 통신이 일어나고 나면 연결이 끊어지고, 다시 연결해도 이전 상태를 유지하지 않아 과거에 어떤 정보를 보냈었는지 기억하지 못한다는 것이다.
HTTP 통신 위에서 동작하는 웹사이트 내의 모든 요청과 응답은 stateless한 특성을 가진다는 말은,
로그인을 통해 인증을 거쳐도 이후 요청에서는 이전의 인증된 상태를 유지하지 않아 반복적으로 ID와 password를 입력해야하는 문제가 생긴다.
따라서, 서버에게 클라이언트가 누군지 알려주기 위해 쿠키, 세션, 토큰과 같은 방식을 사용한다.
Cookie
쿠키란 서버에서 사용자 브라우저로 전송하는 작은 데이터를 뜻한다.
브라우저는 서버에서 받은 데이터(Cookie)를 저장해 놓앗다가 동일한 서버로 재요청 시 제공받았던 데이터(cookie)를 함께 전송한다.
✅ Stateless 하다 -> 인증 상태를 서버가 관리하지 않고 매번 클라이언트의 인증 정보를 담은 쿠키의 요청을 받을 때 처리한다.
Cookie 인증 방식
- 브라우저(클라이언트)가 서버에 request를 보낸다.
- 서버는 클라이언트의 요청에 대한 response를 작성할 때, 클라이언트 측에 젖아하고 싶은 정보를 응답 header의 Set-Cookie에 담는다.
- 이후 해당 클라이언트는 request를 보낼때마다, 매번 저장된 쿠키를 요청 header의 Cookie에 담아서 보낸다. 서버는 쿠키에 담긴 정보를 바탕으로 해당 request의 클라이언트가 누군지 식별한다.
Cookie 방식의 단점
- 단점에 취약하다.
- 쿠키에 데이터 값을 그대로 담은 상태에서 보내는데, 이는 브라우저의 설정 화면이나 개발자 도구에서 확인하고 수정, 삭제가 가능하기 때문에 당사자뿐만 아니라 누구나 조회가 가능하다.
- 따라서, 남에게 탈취되거나 조작되어도 크게 문제되지 않을 정보만 담도록 해야한다.
- 용량 제한이 있어 많은 정보를 담을 수 없다.
- 쿠키에 담는 정보의 양이 많아져 쿠키의 사이즈가 커질수록 네트워크 부하가 심해진다.
Session
쿠키의 보안 문제때문에 ID, password 같은 중요한 정보를 클라이언트가 아닌 서버에서 저장하고 관리하도록 나온게 Session이다.
Session은 고객을 식별할 수 있는 값인 session ID를 생성해 Cookie로 주고받는다. (세션은 쿠키를 기반으로 한다)
✅ Stateful 하다 -> 클라이언트의 정보를 서버에서 관리한다.
Session 인증 방식 (= 서버 기반 인증 방식)
- 사용자가 로그인한다.
- 서버에서 계정 정보를 읽어 사용자를 확인하고, 사용자에게 고유한 ID를 부여하여 세션 저장소에 저장한 후, 이와 연결된 세션 ID를 발급한다.
- 사용자는 서버에서 해당 세션 ID를 받아 쿠키에 저장한 후, 인증이 필요한 요청마다 쿠키를 헤더에 실어보낸다.
- 서버는 쿠키를 받아 세션 저장소에서 대조 후 대응되는 정보를 가져온다.
- 인증이 완료되면 서버는 사용자에 맞는 데이터를 보내준다.
중요한 유저 정보는 모두 서버에 있으며, 유저가 가지고 있는건 오직 세션 ID뿐이다.
세션 방식에서의 쿠키는 그저 세션 ID를 전달하기 위한 매개체일 뿐이다.
Session 방식의 단점
- 서버에서 세션 저장소를 사용하므로 요청이 많아지면 서버에 부하가 심해진다.
- 현재 로그인한 모든 유저의 세션 ID를 서버에 저장해야한다는 말인데, 유저가 늘어남에 따라 (즉, request가 늘어남에 따라) DB 리소스가 더 필요하게된다. 또한, 해당 유저의 정보를 찾고 데이터 매칭을 하는 과정도 오래 걸리면서 서버에 부하가 가해지게 된다.
Token
쿠키처럼 기존 로그인 정보를 그대로 사용하지 않으면서도 서버에서 사용자 식별 값을 저장하지 않아도 되는 방법이 없을까? 해서 나온게 토큰이다.
Token 방식은 사용자를 인증할 수 있는 정보가 숨겨진 암호화된 Access Token을 발행하고, request할 때마다 header에 토큰을 심어서 보낸다. 서버는 저장된 데이터가 아닌 Token 해독을 통해 Token 속에서 사용자를 식별할 수 있는 정보들을 알아내고 이를 바탕으로 인증/인가가 진행된다.
✅ Stateless 하다 -> 클라이언트가 인증 정보를 관리하는 점에서 쿠키와 비슷하다
Token에도 다양한 유형과 종류가 존재하지만 이 글에서는 JWT에 대해 다뤄보려고 한다.
JWT(Json Web Token)
JWT는 인증에 필요한 정보들을 암호화시킨 JSON 토큰을 의미한다. JSON 데이터를 Base64 URL-safe-Encode를 통해 인코딩을 직렬화 한 것이며, 토큰 내부에는 위조나 변조 방지를 위해 개인키를 통한 전자서명도 들어가있다.
📍JWT 구조
JWT는 .을 구분자로 나누어지는 Header, Payload, Signature 세가지 문자열의 조합이다.
📍Header
- alg : Signature에서 사용하는 알고리즘
- Signature에서 사용하는 알고리즘은 대표적으로 RS256(공개키/개인키)와 HS256(비밀키/대칭키)가 있다. (사실, 이와 관련해서도 SHA 256 알고리즘, Hash 알고리즘, HMAC 등등 말할 내용이 정말 많지만 나중에 따로 글 쓰는걸로..)
- typ : 토큰 타입
📍Payload
사용자 정보의 한 조각인 claim이 들어있다.
- sub : 토큰 제목 (subject)
- aud : 토큰 대상자 (audience)
- iat : 토큰이 발급된 시각 (issued at)
- exp : 토큰의 만료시각 (expired)
📍Signature
Signature는 Header와 Payload의 문자열을 합친 후에, 헤더에서 선언한 알고리즘과 key를 이용해 암호환 값이다.
Header와 Payload는 단순히 Base64 url로 인코딩되어있어 누구나 쉽게 복호화할 수 있지만, Signature는 key가 없으면 복호화할 수 없다. 즉, 보안상 안전하다.
앞서 언급한 것처럼 header에서 선언한 알고리즘에 따라 key는 개인키가 될 수도 있고, 비밀키가 될 수도 있다. 개인키로 서명했다면 공개키로 유효성 검사를 할 수 있고, 비밀키로 서명했다면 비밀키를 가지고있는 사람만이 암호화, 복호화, 유효성 검사를 할 수 있다.
JWT 인증 과정
< 로그인 전 >
- 사용자가 서버에 로그인 요청을 보낸다.
- 서버는 secret key를 사용해 json 객체를 암호화한 JWT 토큰을 발급한다.
- JWT를 헤더에 담아 클라이언트에 보낸다.
< 로그인 후 >
- 클라이언트는 JWT를 로컬에 저장해놓는다.
- API 호출을 할 때마다 Authorization header에 JWT를 실어 보낸다.
- 서버는 헤더를 매번 확인하여 secret key로 복호화한 후, 사용자가 신뢰할만한지 체크하고, 인증이 되면 API에 대한 응답을 보낸다.
JWT 방식의 단점
- 강제 로그아웃과 같은 기능을 구현할 수 없다.
- 이미 발급된 JWT에 대해서는 돌이킬 수 없다. 한 번 발급되면 유효기간이 완료될 때까지 계속 사용이 가능하다. 따라서 악의적인 사용자는 유효기간이 지나기 전까지 정보를 털어갈 수 있다.
Refresh Token을 이용한 인증
위에서 설명한 JWT 방식은 Access Token만을 이용한 방식인데, 이는 위에서 말한 것처럼 제 3자에게 탈취당할 경우 보안에 취약하다.
유효기간을 짧게 설정하면 그만큼 사용자가 로그인을 자주해서 새롭게 토큰을 발급받아야하고, 유효기간을 늘리면 토큰을 탈취당했을 때 보안에 더 취약해진다.
그래서 등장한 것이 Refresh Token이다.
Access Token보다 유효기간을 좀 더 길게 설정한 Refresh Token은 Access Token이 만료되었을 때 새로 발급해주는 열쇠가 된다.
즉, Access Token의 유효기간을 짧게 만들고, 유효기간이 만료될 때마다 Refresh Token을 통해 새로운 Access Token을 만들어서 보안을 조금이라도 더 안전하게 한 것이다.
Refresh Token을 사용하는 인증 과정
< Access Token 만료 전 + 로그인 전 >
- 사용자가 로그인을 요청
- 서버에서 회원 DB에서 값을 비교한다 (보통 Password는 암호화해서 들어간다)
- 사용자 인증이 되면 서버에서 Access Token, Refresh Token을 발급하고, Refresh Token을 서버측에 저장한다 (보통 DB에 유저 정보와 같이 저장한다)
- 서버는 사용자에게 Access Token, Refresh Token을 보낸다.
- 클라이언트느 Refresh Token을 안전한 저장소에 저장 후, Access Token을 헤더에 실어 요청을 보낸다.
- 서버는 Access Token을 검증 후, 이에 맞는 데이터를 사용자에게 보내준다.
< Access Token 만료 후 >
- 사용자가 만료된 Access Token을 헤더에 실어 요청을 보낸다.
- 서버는 Access Token이 만료되었음을 확인한다.
- 만료된 토큰임을 알리고 권한없음(403 에러)을 신호로 보낸다.
- 권한없음 신호를 받은 사용자는 Refresh Token과 Access Token을 함께 서버로 보낸다.
- 서버는 받은 Access Token이 조작되지 않았는지 확인하고, 받은 Refresh Token과 DB에 저장되어있던 사용자의 Refresh Token을 비교한다. Refresh Token이 동일하고 유효기간도 지나지 않았다면 새로운 Access Token을 발급해서 클라이언트에 전달해준다.
- 클라이언트는 새로 발급된 Access Token을 헤더에 실어 다시 API 요청을 진행한다.
1~3 과정은 프론트엔드 단에서 Access Token에서 Payload를 통해 유효기간을 알 수도 있다. 따라서 프론트엔드 단에서 API 요청 전에 토큰이 만료되었다면 refresh token을 함께 서버로 보내 토큰 재발급 요청을 하도록 구현할 수도 있다.
✅ 로그아웃을 하면 Access Token과 Refresh Token을 모두 만료시킨다
Session vs JWT
장점 | 단점 | |
Session | - 서버 쪽에서 Session 통제 가능 - 네트워크 부하 낮음 |
세션 저장소 사용으로 인한 서버 부하 |
JWT | - 인증을 위한 별도의 저장소가 필요 없음 - 별도의 I/O 작업 없는 빠른 인증 처리 - 확장성이 우수함 |
- 토큰의 길이가 늘어날수록 네트워크 부하 - 특정 토큰을 강제로 만료시키기 어려움 |
각각 장단점이 있기 때문에 그때마다 서비스의 비즈니스 로직 등을 고려하여 알맞은 인증 방식을 선택하면 된다.