로그인 기능 구현에 필요한 사전 지식
인증(Authentication)
- 클라이언트의 신원을 확인하는 것
- 로그인하지 않은 클라이언트라면 ID/PW를 입력 받아 DB에 저장된 User 정보와 일치하는지 확인
- 로그인한 클라이언트라면 로그인시 발급한 정보(상태 유지 기술)를 통해 신원을 확인한다.
인가(Authorization)
- 리소스에 접근하는 접근자의 권한을 확인하고 허가 여부를 결정
- 신원 확인(인증)이 이루어진 클라이언트를 대상으로 행해진다.
상태 유지 기술
- 로그인 이후 클라이언트가 서버와 지속적인 요청/응답을 주고 받는 경우에, stateless 하고 connectionless한 http프로토콜로는 서버가 클라이언트가 로그인한 유저가 맞는지 확인할 수 없다.
- 즉, 지속적인 인증이 힘들다. 이를 위해 서버는 로그인 성공시 클라이언트에게 자신을 입증할 신분증을 제공하여 이후 인증 절차를 ID/PW 요구 없이 수행할 수 있도록 한다.
- 신분증 발급 기능을 위한 기술이 상태 유지 기술이며 쿠키, 세션, jwt가 있다.
쿠키
- 클라인언트의 브라우저에 저장되는 key-value형식의 데이터
- 쿠키는 유효기간을 지정할 수 있으면 만료시 삭제된다.
- 서버에서 http 응답시 Set-Cookie 헤더 옵션을 통해서 클라이언트에 쿠키를 전달할 수 있다.
- 쿠키는 브라우저에 저장되면 클라이언트의 매요청마다 요청 헤더에 쿠키를 담아 요청한다.
- 활용 예시) 로그인, 쇼핑몰 장바구니, 팝업 "더 이상 이 창을 보지 않음" ...
세션
- 세션은 쿠키를 기반으로 동작한다.
- 세션은 쿠키(클라이언트 브라우저)에 데이터를 저장하는 대신 서버에 특정 클라이언트의 데이터를 전달한다.
- 해당 클라이언트의 데이터가 저장된 세션의 ID 값을 쿠키로 보내고 클라이언트는 이후 요청에 쿠키에 저장된 세션 ID를 서버에 제공한다. 서버는 세션 ID를 통해 클라이언트를 확인 할 수 있다.
- 세션은 클라이언트 정보가 서버에 저장되므로 쿠키보다는 보안상 안전하다.
- 세션은 클라이언트 정보를 확인하기 위해 자주 참조 되므로 서버의 DB가 아닌 메모리상에 저장한다.
- 동시 접속자가 많은 경우 서버에 부하가 발생한다.
- 서버의 메모리에 저장되니 scale-out 환경의 경우 세션 데이터를 모든 서버가 동일하게 갖고 있기 힘든 데이터 정합성을 고려해야하는 문제가 있다.
jwt
- jwt의 동작 방식은 세션보다는 쿠키에 가깝다. 왜냐하면 클라이언트의 정보를 클라이언트측에 저장하기 때문이다.
- 브라우저의 쿠키에 저장하는 대신 jwt라는 토큰에 클라이언트 정보를 담아서 암호화한 후 클라이언트에게 제공한다. 클라이언트는 이를 스크립트 변수(메모리), 로컬 스토리지, 쿠키 등에 저장하여 이후 요청에 jwt 실어 보내서 인증을 진행한다.
- 서버에서 관리하는 비밀키를 이용하여 암호화함으로 jwt내에 저장된 민감한 클라이언트 정보를 확인할 수 없다.
- jwt는 크게 3개의 영역 header, payload, signature으로 나뉜다.
- header는 토큰의 타입과 암호화 알고리즘으로 구성된다.
- payload는 클레임을 담는 영역이다. 클레인은 정보의 조각을 의미한다. 클레임에는 등록된 클레임, 공개 클레임, 비공개 클레임 세 종류가 있다. 등록된 클레임을 통해 토큰 만료시간을 설정할 수 있다.
- signature영역은 jwt의 무결성을 보장한다. header와 payload의 base64 인코딩 값을 합쳐서 서버에서 관리하는 비밀키로 해싱한 값이 signature 영역이다. 해시값을 통해 토큰의 유효성 검증시에 토큰이 변조되었는지 확인 할 수 있다.
로그인 플로우
쿠키/세션/jwt 어떤 기술을 사용하는가에 따라 로그인 플로우는 달라진다.
하지만 기본적으로 아래와 같은 흐름으로 동작한다.
- 클라이언트에서 서버로 ID/PW 제출 - 로그인 시도
- DB에 저장된 ID/PW 정보와 클라이언트로 부터 전달 받은 ID/PW 일치 여부 확인(최초인증)
- 인증 성공시 쿠키/세션/jwt 방식에 맞게 인증 정보 발행(ex. userID를 담은 쿠키, 세션 ID를 담은 쿠키, userID를 담은 jwt)
- 인증 정보를 클라이언트에게 전달
- 클라이언트의 리소스 요청(인증 정보와 함께)
- 클라이언트에서 보낸 인증정보를 확인하여 클라이언트 인증
- 인증된 클라이언트의 권한을 확인하여 리소스 접근에 대한 인가 진행
- 인가된 사용자라면 리소스 응답
위의 로그인/인증/인가 기능 외적으로 인증 정보타입(쿠키/세션/jwt)에따라 이를 관리해주는 로직이 필요할 수 있다.
로그인 구현에 jwt를 사용하고자하는 이유
- 쿠키는 스크립트에서 쉽게 접근할 수 있다. 또한 쿠키안에있는 정보를 쉽게 확인 할 수 있고 이를 위변조하여 사용하여도 서버측에서 이를 확인 할 수 없다.
- 또한 쿠키가 공격자에게 탈취되면 이를 이용해 공격자가 인증을 수행할 수 있게된다.
- 이러한 이유로 로그인 구현에 있어 쿠키보다는 조금더 안전한 세션을 사용하는 것이 나아보임.
- 왜냐하면, 세션은 인증에 사용되는 민감한 정보들을 서버에 저장을 하고 세션 ID만을 클라이언트가 관리하게하여 보안상 안전하게 사용할 수 있기 때문이다.
- 하지만, 세션을 이용한다면 서버 성능에 부하가 발생한다. 세션을 저장하기위해 서버의 메모리가 소비되고 인증 절차는 로그인한 대상의 거의 모든 요청에서 이루어지기에 매우 빈번하게 조회된다.
- 세션은 빈번하게 조회되기에 DB단에 저장되면 오버헤드가 커서 서버의 메모리단에서 저장된다. 그렇기 때문에 scale-out 환경에서 서버간에 세션 데이터 정합성 문제가 발생한다.
- 이러한 문제들로 인해 로그인 구현에 쿠키/세션 대신 jwt가 사용된다.
- jwt는 인증 정보를 jwt의 payload에 저장하며 토큰 발급시에 비밀키를 이용하여 암호화를 진행한다. 또한 signautre로 토큰의 위변조를 탐지한다.
- 또한 클라이언트사이드에서 관리하기에 서버의 부하와 세션데이터 정합성 문제에서 자유롭다.
- jwt도 제대로 사용하려면 refresh 토큰을 서버사이드에 저장하여 관리한다. 하지만, refresh토큰의 참조 빈도가 세션에비해 훨씬 낮기에 DB 단에서 저장 관리 할 수 있고 때문에 데이터 정합성 문제에서 자유로울 수 있다.
jwt 로그인 구현시 고려할 점
- refresh rotate를 할 것 (보안)
- access / refresh 토큰의 클라이언트단의 저장 위치
- access는 스크립트 변수로
- refresh는 http only 쿠키로 관리하는 것이 좋아 보임
- refresh 토큰을 통한 access 토큰 reissue 로직
- refresh 토큰의 서버에서 어떻게 관리할 것인지?
- 토큰의 기간은 어떻게 설정하는 것이 합리적일까