ID
와 Password
를 입력하여 BackEnd로 데이터를 보낼 때, fetch
나 axios
같은 HTTP 비동기 통신 라이브러리를 사용하게 된다.요청(request)
을 보내면 순간 연결이 되고, 서버가 응답(response)
을 보내면 통신은 종료된다는 것이다. ID
와 Password
을 입력하여 서버에서 "로그인 성공!!"을 반환하더라도 마이페이지에 들어갔을 때, 서버는 내가 방금 로그인에 성공한 유저인지 모른다. Cookie
와 Web Storage
가 생기게 되었다. Web Storage
는 Cookie
에 비해 큰 데이터를 저장할 수 있고, 브라우저에 로컬하게 저장된다. Web Storage
안에서도 Local Storage
와 Session Stroage
로 나눌 수 있다.Local Storage
는 만료 기간이 없고, 도메인이 다른 경우 접근이 불가능하며 브라우저를 종료해도 유지된다. Session Storage
는 탭에 따라 개별적으로 저장되며, 탭이 종료될 때 데이터가 만료되고 같은 도메인이여도 세션이 다르면 데이터에 접근이 불가능하다. Web Storage
에 비하면 작은 데이터를 저장할 수 있지만 서버에 전송할 수 있기 때문에 서버 데이터를 공유하는 용도로 사용된다. 만료기간
- 영구 쿠키(Persistent Cookie) : 만료 기간이 있다.
- 세션 쿠키(Session Cookies) : 만료 기간이 없어 브라우저 종료시 삭제된다.
도메인
- First party Cookie : 같은 도메인 또는 서브 도메인에서 생성된 쿠키
- Third party Cookie : 다른 도메인에서 생성된 쿠키
SSR(Server Side Rendering)에서는 Local Storage
의 값을 알 수 없기 때문에 쿠키
를 활용하여 FrontEnd의 생산성을 높이기도 한다.
또한, 쿠키의 HttpOnly 옵션을 통해 Script를 이용한 XSS 공격을 방지할 수 있고, Secure 옵션을 통해 쿠키를 HTTPS로만 전송되게 만들어 보안 수준을 높일 수 있다.
하지만 로그인 정보를 클라이언트 브라우저의 작은 텍스트 조각인 쿠키에 저장하기에는 여전히 보안이 취약하다.
스니핑과 같은 공격으로 언제든지 탈취당할 수 있다.
그리고 쿠키에 대한 정보를 HTTP Header에 계속 추가해서 보내게 되어 많은 트래픽을 발생시킬 수 있다는 단점도 존재한다.
쿠키
나 세션
에 대해서 검색하면 항상 이 둘은 비교된다. 쿠키
는 취약하고 세션
은 괜찮다는 식의 얘기이다. 쿠키
와는 달리 세션
은 클라이언트의 인증 정보를 서버에서 저장하고 관리 하기 때문이다. 세션
과 쿠키
가 전혀 상관없지는 않다. 세션
인증 방식에서는 세션 id
를 쿠키
에 담아서 통신하기 때문이다. 인증(Authentication)
- 유저가 ID와 Password를 입력하여 Back에 HTTP Request를 보내면서 로그인을 시도한다.
- BackEnd는 DB에서 유저를 확인하고, 성공시 다음 스탭을 수행하고 실패시 Front에 에러를 던져줄 것이다.
- 유저가 확인되면, 서버는 세션을 생성하고 저장한다.(메모리, DB, Redis 등)
- 여기서 얻어진 세션ID를 쿠키에 담아 Front에 반환한다. Front는 받은 세션ID를 쿠키에 저장한다.
인가(Authorization)
- 마이페이지와 같은 유저의 정보가 필요한 Request에 세션ID가 저장된 쿠키를 함께 보낸다.
- 서버에서 세션ID를 확인한다.
- 로그인한 유저의 마이페이지 정보를 찾는다.
- Front가 요청한 정보를 Response한다.
토큰
방식이 있다.JWT(Json Web Token)
는 무상태(Stateless)가 가장 큰 특징이다. Stateless
가 가능한 것이다.header
, 만료 기간과 같은 토큰의 여러 정보를 담는 payload
, 토큰의 유효성을 검증할때 필요한 signature
로 구성되어 있다.
- 유저가 ID와 Password를 입력하여 Back에 HTTP Request를 보내면서 로그인을 시도한다.
- BackEnd는 DB에서 유저를 확인한다.
- 성공시 Access Token을 발급한다.
- FrontEnd는 응답과 Access Token을 받는다.
- 유저의 정보가 필요한 요청에 Access Token을 담아 보낸다.
- Access Token을 검증한다.
- 유저 정보가 검증되면 요청했던 데이터를 응답해준다.
Access Token
을 보낼 때, 쿠키
또는 Authorization
을 사용한다. 토큰을 이용한 인증 방식에서 해커에게 탈취되었을 경우를 언급하며 유효 기간을 짧게 한다고 했었다.
Access Token의 유효 기간을 짧게 잡으면, 유저가 아직 서비스를 이용하고 있는데 갑자기 로그아웃 될 수 있다.
이 불편함을 해결하고 Access Token의 유효 기간도 짧게 설정하기 위해 사용하는 것이 Refresh Token
이다.
Access Token이 만료되기 전까지의 동작 방식은 Access Token만 이용한 토큰 인증 방식과 유사하다.
토큰 발급시 Refresh Token도 발급된다는 것 빼규
- 평소와 같이 유저의 정보가 필요한 요청과 함께 Access Token을 Back에 넘겨준다.
- 서버는 Access Token이 만료된 것을 확인한다.
- 서버는 Front에 Access Token이 만료되었다는 것을 알려준다.
- Front는 Access Token과 함께 Refresh Token도 같이 넘겨준다.
- 서버는 Refresh Token이 검증되면, 새로운 Access Token을 발급한다.
- 새로 발급받은 Access Token을 Front에 넘겨준다.
- 이전에 요청했던 것을 새로운 Access Token을 담아 서버에 보낸다.
동작 방식을 보면 알 수 있듯이 구현하기는 상당히 복잡해보인다.
그리고 Access Token의 유효 기간이 얼마나 짧은지에 따라 HTTP 요청수가 증가하여 서버의 자원 낭비가 일어날 수 있다.
그렇다면 프론트에서 어떻게 Access Token과 Refresh Token을 관리하는지 궁금해진다.
흔히 사용하는 방법은 Access Token을 서버에서 받으면 보통 상태관리에 넣고, Refresh Token은 http-only 넣는 방법이다.
Refresh Token이 탈취당하면..?
- 데이터베이스에 각 사용자에 1대1로 맵핑되는 Access Token, Refresh Token 쌍을 저장한다.
- 정상적인 사용자는 기존의 Access Token으로 접근하며 서버측에서는 데이터베이스에 저장된 Access Token과 비교하여 검증한다.
- 공격자는 탈취한 Refresh Token으로 새로 Access Token을 생성한다. 그리고 서버측에 전송하면 서버는 데이터베이스에 저장된 Access Token과 공격자에게 받은 Access Token이 다른 것을 확인한다.
- 만약 데이터베이스에 저장된 토큰이 아직 만료되지 않은 경우, 즉 굳이 Access Token을 새로 생성할 이유가 없는 경우 서버는 Refresh Token이 탈취당했다고 가정하고 두 토큰을 모두 만료시킨다.
- 이 경우 정상적인 사용자는 자신의 토큰도 만료됐으니 다시 로그인해야 한다.
- 하지만 공격자의 토큰 역시 만료됐기 때문에 공격자는 정상적인 사용자의 리소스에 접근할 수 없다.
참조:
오늘만큼은 로그인을 부셔보자.