그 것은 바로 세션(Session) 과 토큰(Token) 입니다.
여태까지 가장 일반적으로 쓰여 왔던 방식은 서버 사이드 세션(Server Side Session) 입니다.
이 방식을 간단하게 요약하자면,
유저는 아이디(이메일)과 비밀번호 등 필요한 로그인 정보를 HTTP 요청을 통해서 서버에 제출합니다.
서버는 이 정보를 받아 일반적으로 DB 에 담겨져 있는 유저의 로그인 정보와 대조하고,정보가 일치한다면 세션 정보를 x 에다 저장하고 유저의 요청에 대해서 Session ID 로 응답합니다.
이 session ID 를 클라이언트(유저)의 브라우저가 Key, Value 쌍을 저장할 수 있는 공간인 쿠키에 저장합니다.
쿠키에 저장 된 Session ID 는 유저가 이후에 동일한 웹사이트에서 새로운 요청을 할 때, 즉 유저 정보를 관리하거나 회원만 볼 수 있는 부분을 열람하고자 할때 서버에 보내게 됩니다.
이 방식은 개발 하는 입장에서 매우 간편하지만, CSRF(Cross-site Request Forgery) 에 대한 위험성을 안고 있습니다. 보안에 대한 깊은 내용을 다루고자 하는 것이 아닙니다. 글의 맥락을 이해 하시는데에 조금 더 편하게 요약하자면, CSRF는 공격자가 유저로 하여금 유저 본인들이 의도하지 않은 요청을 서버에게 보내도록 패스워드를 바꾸거나 결제를 하도록 유도 하는 것 입니다. 다행히도 개발 하실 때 django, ruby on rails, node js 등의 현대 프레임 워크를 사용하신 다면 CSRF 에 대한 위험성은 극히 적으니, 일반적인 사용 범위 내에는 크게 지장이 없습니다. Session ID 를 이용하는 것의 가장 큰 문제점은, 클라이언트 사이드 에서의 문제가 아닌 바로 서버 메모리나 데이터베이스에 저장 하게 된다는 부분 입니다.
현대의 클라우드 어플리케이션 들은 감당해야 할 트래픽이 늘어났을 때 대부분 가로 확장 (Horizental Scailing) 을 통해서 확장합니다. 가로 확장은, 일반적으로 우리가 아는, 물리적인 서버를 늘리는 방식입니다. 참고로 확장의 종류는 크게, 방금 이야기 한 Horizental Scailing 과, CPU나 램을 추가하는 등 성능 향상을 통해서 확장을 하는 Vertical Scailing 이 존재합니다. 더 자세한 내용은 추후의 db 관련 포스팅에서 다루도록 하겠습니다.
특히 node.js 와 같이 Single Thread 로 작동하는 기반한 서버들은, 이러한 서버 메모리에 대한 부담이 상대적으로 클 수 밖에 없습니다.
다행히도 이 문제에 대해서는 대안이 존재합니다.
방식은 위에서 설명한 Session 기반 인증 방식이랑 기본적으로 유사합니다.
유저가 로그인 정보를 서버에 전송합니다.
개발자가 서버 내에서 설정한 Private Key 를 기반으로 JWT를 생성하고 (JSON Web Token) 이 JWT 토큰 정보를 유저의 쿠키(httpOnly)에 저장합니다.
이후의 요청에 있어서 유저는 Header 에 Authorization: Bearer 의 형태로 서버에 요청을 보냅니다.
서버는 이러한 토큰을 데이터 베이스나 세션에 저장하지 않고 프로그래밍 적으로 token의 signature(서명) 을 인증하고 유저의 자료 열람을 허가합니다.
JSON Web Token, 이하 JWT 는 어떤 JSON 데이터의 소유자를 인증하는 하나의 방식입니다. JWT는 인코드 된 string 형태로 URL 이 보호되며, cookie 와는 다르게 무제한으로 데이터를 저장할 수 있습니다. 또한 가장 중요한 것은 암호로 서명 되었다는 것 입니다. JWT는 근본적으로 3자의 개입이 전혀 없이 토큰을 발급한 출처인 서버에서 직접 인증을 하기 때문에, 내부의 데이터의 확실성에 대한 보장이 있습니다.
비유를 하자면 대략 이렇습니다. 내가 계약서를 작성하고 서명할 때, 내 자신이 "서명"을 하게 됩니다. 따라서 이 계약서를 서명한 것은 '나' 라는 것이 보장됩니다. 이 서명은 고유의 것이기 때문에, 출처의 확실성에 있어서 의문이 없기 때문입니다. JWT의 암호화 서명도 마찬가지의 원리로 작동하게 됩니다. JWT 는 흔히 symmetric 혹은 assymetric 형태로 서명되며, 두 가지 방법 모두 이 서명에 대한 확실성을 보장합니다. (물론 보안에서 완전히 안전하다는 뜻은 아닙니다.)
JWT 는 특히 이러한 방식은 특히 클라우드의 distributed system 에 와 같은, Server-to-Server 인증에 있어서 탁월한 선택이 될 수 있습니다. 구글이 API에 대한 인증방식도 이러한 방식으로 구현되어 있습니다. 서버는 토큰을 인증할 때 데이터베이스와 커뮤니케이션을 하는등 어떠한 네트워크 요청을 하지 않아도 되기 때문입니다. 이러한 방식은 세션 관리를 훨씬 빠르게 할 수 있게 만들어 줍니다. 또한 요청이 있을 때 마다 데이터베이스나 캐시를 통해서 유저 정보를 불러오지 않아도 됩니다. JWT 는 Stateless 하기 때문이죠.
또한 JWT 정보 내부에 유저의 권한이나 개인 정보 등에 대해서 안전하게 넣을 수 있다는 점도 굉장히 이점으로 작용할 수 있습니다.
위와 같은 복잡한 Distributed System에 속하지 않는 대부분의 웹사이트들은 우리가 생각하는 것 보다 굉장히 간단합니다. 대게 회원 로그인 방식은 다음과 같이 이루어져 있습니다.
가령 유저의 ID를 그리고 혹은 JWT에 저장한다고 가정 해봅시다. 단순히 ID 를 쿠키에 저장한다면 사이즈는 단 6 바이트에 지나지 않을 것 입니다.
반면 유저의 ID를 JWT에 저장한다고 가정했을 때는 가장 일반적인 경우 그 용량은 300 바이트 가량이 될 것 입니다. 이미 프레임워크가 알아서 암호화 하는 정보를 굳이 JWT 를 통해 암호화를 하는 댓가는 약 50배의 용량 차이 입니다. 만약에 한 웹사이트가 월 10만 뷰를 달성한다면, 24MB 의 트래픽을 추가로 소모하고 있다는 이야기가 됩니다. 다시 말하지만, 이 것은 어디까지 "하나의 페이지" 에 대한 요청인 것이지, 일반적으로 유저가 웹사이트를 방문한다면 평균적으로 3~4개의 페이지를 본다고 생각하면 꽤 큰 용량을 차지하게 됩니다.
앞서 말한 CRUD를 이용해서 동적으로 콘텐츠를 생성하는 대부분의 웹사이트들은, 캐시나 데이터베이스를 통해 유저 정보를 불러와야 합니다. 다음의 예시들을 살펴보죠.
만약 이러한 기능을 하고 있는 웹사이트라면 Stateless하다는 것이 장점인 JWT는 그 장점을 잃어버리게 됩니다. 앞서 말한 용량 문제는 물론이고, 암호화 된 서명을 계산하기 위해서 더 많은 컴퓨팅 성능을 필요로 하기 때문에, 오히려 세션 기반의 인증보다 더 비효율적인 경우도 발생합니다.
따라서 결론적으로 만약 단순히 클라이언트 사이드의 유저와 상호작용 하는, 가장 일반적인 형태의 웹사이트를 만들고자 한다면, JWT를 용하지 않고 User ID를 쿠키에 직접 저장하면 됩니다. 위에서 말했듯, 대부분의 현대 프레임워크들은 단순한 형태의 웹사이트 정보를 보호하기에 충분한 보안을 제공해주기 때문입니다.
만약 더 큰 규모의 웹사이트를 운용하고 서버에 부담을 줄여야한다면, 그때는 redis 나 mecached 같은 외부 메모리 서버를 이용하면 됩니다.
결론2
직접 커스텀으로 유저인증을 구현을 해보셨다면, 정신건강을 위해 라이브러리를 이용하십시오.
node.js 패스포트 라이브러리
[1]https://developer.okta.com/blog/2017/08/17/why-jwts-suck-as-session-tokens
[2]https://blog.logrocket.com/jwt-authentication-best-practices/#:~:text=A%20very%20common%20use%20of,you%20authenticate%20to%20their%20APIs
[3]https://medium.com/better-programming/jwt-tokens-the-what-how-and-why-6ae3bad26661
[4]https://tools.ietf.org/html/rfc7519
[5]코드스테이츠 최민철님.
참고자료가 인상깊네요!