프론트엔드 개발자로 살다보면, 꼭 구현하게 되는 기능이 있다. 유저마다 자신에게 맞는 서비스를 이용할 수 있도록 해주는 로그인 기능이다. 사실 일반 유저가 로그인에 머무르는 시간은 굉장히 짧다. 그리고 거의 모든 사이트에 있다보니 로그인을 당연하게 여기고, 별거 아닌 기능이라고 생각한다.
하지만 개발자들은 알겠지만 로그인이 간단한 기능만은 아니다. 당장 구글에만 검색해봐도 쿠키, 세션, JWT 뭐가 정답이지?!
라는 형식의 글이 많은 것만 봐도 이 로그인에 얼마나 많은 고민이 있는지 알 수 있다. 오늘은 로그인에서 사용하는 인증 방식인 쿠키, 세션, JWT
에 대해서 알아보고, 현업에서는 어떤 방법을 많이 사용하는지 얘기해보는것이 핵심이다.
사실 위에서 언급한 인증 방식을 고민하는 이유는 유저의 개인 정보 때문이다. 하지만 개인 정보까지는 필요 없는 간단한 로그인이라면 어떨까?
구딩처럼 닉네임만 입력하면 로그인이 성공하여 유저가 서비스를 이용할 수 있는 프로젝트는 유저의 민감한 정보를 받지 않는다. 보안에 대해 신경 쓸 이유가 없어 로그인을 빠르게 구현할 수 있었다. 로컬 스토리지에 유저의 닉네임을 저장하고, 이 유저에게 맞는 데이터를 불러와 제공할 수 있도록 구현했다.
하지만 회사의 서비스에서 로그인을 구현할 때는 위처럼 로컬 스토리지를 이용했다가는 어떤 대참사가 일어날지 모른다. 예를 들어, 쿠팡에 로그인을 했는데 나의 로그인 정보가 암호화도 이루어지지 않은 채 로컬 스토리지에 있다고 가정해보자. 자리를 잠시 비운 사이에 개발자도구 조금만 만질 줄 아는 7살짜리 꼬마가 와도 탈취가 가능하다.
로그인을 어떤 방법을 사용해서 구현하는지에 따라 플로우는 조금씩 다르다. 그래도 큰 틀로 묶어보았다. 일단 유저가 웹 사이트(Front)에서 로그인을 하기 위해 ID와 Password를 입력하면, 이 정보가 Back으로 넘어가게 된다. 자체적으로 ID와 Password가 정상적인 형식인지 체크하고, 통과되면 회원 DB에서 사용자를 확인한다.
이제부터 FrontEnd와 BackEnd는 어떤 인증 수단을 사용할 것인지에 따라 로그인 구현 방향과 개념이 달라진다. 두 가지 방법을 얘기하기 전에, 혹시 "이미 저 정도면 로그인 구현 다 된거 아닌가?" 라는 생각을 했다면 아래의 내용도 확인해보자.
웹에서는 데이터를 교환하기 위해서는 HTTP 통신을 해야한다. 로그인 인증을 하기 위해서도 마찬가지다. FrontEnd에서 ID와 Password를 입력하여 BackEnd로 데이터를 보낼 때, fetch나 axios같은 HTTP 비동기 통신 라이브러리를 사용하게 된다.
HTTP 통신은 OSI 7계층에서 Application에 해당한다. HTTP의 특징으로는 비연결성과 무상태성(Stateless)이 있다. 서버와 클라이언트가 항상 연결되어 있는 것이 아니라 클라이언트가 요청(request)을 보내면 순간 연결이 되고, 서버가 응답(response)을 보내면 통신은 종료된다는 것이다. 상태를 저장하지 않아 자원 낭비를 방지한다는 장점이 있다.
하지만 이 특징으로 인해 로그인 페이지에서 ID와 Password을 입력하여 서버에서 "로그인 성공!!"을 반환하더라도 마이페이지에 들어갔을 때, 서버는 내가 방금 로그인에 성공한 유저인지 모른다. 유저는 다시 로그인을 해야만 마이 페이지에 들어갈 수 있게 된다. 이제 매번 어떤 행동을 할때마다 무한 로그인이다.
이 문제점을 해결하기 위해서는 인증 수단이 필요하다. 세션/쿠키를 이용한 방법과 토큰 기반 인증(JWT)를 알아보자.
HTTP는 무상태성(Stateless)이다. 하지만 유저 정보, 지역에 따른 언어 등 여러 기능들을 지원하기 위해서 Cookie와 Web Storage가 생기게 되었다. 원래는 로그인에 Web Storage는 필요 없지만, 위에서 한 번 언급했었으므로 설명하고 넘어가겠다.
Web Storage는 Cookie에 비해 큰 데이터를 저장할 수 있고, 브라우저에 로컬하게 저장된다. 그리고 서버로는 데이터가 전송되지 않는다는 특징이 있다. Web Storage안에서도 Local Storage와 Session Stroage로 나눌 수 있다.
Local Storage는 만료 기간이 없고, 도메인이 다른 경우 접근이 불가능하며 브라우저를 종료해도 유지된다. 이에 반해 Session Storage는 탭에 따라 개별적으로 저장되며, 탭이 종료될 때 데이터가 만료되고 같은 도메인이여도 세션이 다르면 데이터에 접근이 불가능하다. 그래도 둘 다 window객체 안에 있다는 공통점도 있다.
XSS 공격으로 정보를 쉽게 탈취당할 수 있다는 단점도 존재한다.
Web Storage에 비하면 작은 데이터를 저장할 수 있지만 서버에 전송할 수 있기 때문에 서버 데이터를 공유하는 용도로 사용된다. 로그인 성공시 쿠키에 "저 로그인 했어요^0^" 라는 값을 보내준다면 무상태성을 보완할 수 있는 것이다. 이번에는 쿠키의 종류도 간단하게 알아보자.
만료기간
영구 쿠키(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)
인가(Authorization)
유저는 세션ID만 가지고 있고, 중요한 정보는 서버에 있다. 탈취당하더라도 노출될 정보는 세션ID밖에 없어 비교적 안전하다. 하지만 해커가 이 세션ID를 이용해서 클라이언트인척 위장하고 나의 마이페이지를 마구마구 만질 수도 있다. 그래도 세션/쿠키 방식은 서버에서 세션을 관리하므로 해킹이 의심되면 끊어버릴 수 있다. 안전하게 유효기간을 두는 것도 좋은 방법이 될 것이다.
그리고 세션/쿠키 방식은 고유한 세션ID만을 사용하기 때문에 서버에서 일일이 유저 정보를 확인하지 않아도 어떤 유저인지 쉽게 알 수 있다.
단점으로는 역시 서버에서 세션을 관리하다보니 요청이 많아질 경우 부하가 생길 수 있다는 것이다. 만약이라도 서버가 터지면.. 모두 끝장인 것이다. 서버를 확장할 때에도 복잡한 방법을 사용해야한다.
웹 어플리케이션에서 세션을 관리 할 때 자주 사용되는 쿠키는 단일 도메인 및 서브 도메인에서만 작동하도록 설계되어있습니다. 따라서 쿠키를 여러 도메인에서 관리하는것은 좀 번거롭습니다.
위와 같은 단점도 있다고 한다..
FrontEnd에서 일어나는 단점 아니니까 무시..는 농담~
메모리, 과부하, 확장, 개발 및 유지보수 비용 등 세션 인증 방식의 문제점들을 해결할 수 있는 토큰 방식에 대해 알아보자.
JWT(Json Web Token)는 무상태(Stateless)가 가장 큰 특징이다. 유저의 인증 정보를 서버에 저장하지 않아도 된다는 특징 하나로 세션 인증 방식의 여러 문제점들을 해결할 수 있게 된 것이다. 메모리를 잡아먹을일도 없고, 여러 사용자가 접속해도 과부하가 없으며 확장에 용이하고 개발 및 유지보수 비용이 따로 많이 들지 않는다.
그렇다고 JWT가 완벽한것은 아니다. 세션 인증 방식에서는 유저가 해킹당하거나 여러 기기에서 접속할 경우에 유저의 정보가 서버에 있으므로 통제가 가능하다. 하지만 토큰 인증 방식은 무상태이고, 스스로 만료 주기를 갖고 있어 강제 로그아웃과 같은 통제가 불가능하다는 단점이 있다. 한마디로 한 번 탈취당하면, 심각한 상황이 올 수 있다는 것이다. 그래서 해킹 당한 경우를 고려하여 만료 기간을 짧게 두는 방식으로 보완 하며 사용한다.
JWT를 이용한 인증 방식에 대해 알아보기 전에, JWT가 무엇인지 부터 간단하게 알아보자. JWT는 무상태가 특징이라고 위에서 언급했다. 인증에서 어떻게 무상태가 가능할까? 바로 인증에 필요한 정보가 모두 담겨있어 Stateless가 가능한 것이다.
토큰의 타입(jwt)과 해싱 알고리즘 방식을 규정하는 header, 만료 기간과 같은 토큰의 여러 정보를 담는 payload, 토큰의 유효성을 검증할때 필요한 signature로 구성되어 있다.
인코딩된 토큰이 원래는 어떻게 생겼는지 보여주고 있다. 모두를 위한 JWT 참고
동작 방식은 세션 인증 방식과 비슷하다. Backend에 Access Token을 보낼 때, 쿠키 또는 Authorization을 사용한다. 둘다 HTTP Request header 내부 필드이므로 보안성은 동일하다. 어디에 담을지는 그냥 백엔드 개발자와 협의하면 된다. http-only으로 보안성을 증가시킬 수 있다는 것을 잊지 말자!
토큰을 이용한 인증 방식에서 해커에게 탈취되었을 경우를 언급하며 유효 기간을 짧게 한다고 했었다. Access Token의 유효 기간을 짧게 잡으면, 유저가 아직 서비스를 이용하고 있는데 갑자기 로그아웃 될 수 있다. 이 불편함을 해결하고 Access Token의 유효 기간도 짧게 설정하기 위해 사용하는 것이 Refresh Token이다.
Access Token이 만료되기 전까지의 동작 방식은 Access Token만 이용하여 토큰 인증 방식을 설명했을 때와 유사하다. 토큰 발급시 Refresh Token도 발급된다는 것 빼면 말이다. 그러므로 Access Token이 만료된 이후부터 설명해보겠다.
동작 방식을 보면 알 수 있듯이 구현하기는 상당히 복잡해보인다. 그리고 Access Token의 유효 기간이 얼마나 짧은지에 따라 HTTP 요청수가 증가하여 서버의 자원 낭비가 일어날 수 있다. 그래도 이 방법을 많이 사용하는데는 장점이 더 많기 때문이라고 생각한다.
이렇게 우리는 토큰 인증 방식에서 현업에서는 어떤식으로 사용하는지 알아보았다. 그렇다면 프론트에서 어떻게 Access Token과 Refresh Token을 관리하는지 궁금해진다. 흔히 사용하는 방법은 Access Token을 서버에서 받으면 보통 상태관리에 넣고, Refresh Token은 http-only넣는다고 한다.
문득 궁금해져서 구글링을 했더니 Stackoverflow에 좋은 글이 있었다.
- 데이터베이스에 각 사용자에 1대1로 맵핑되는 Access Token, Refresh Token 쌍을 저장한다.
- 정상적인 사용자는 기존의 Access Token으로 접근하며 서버측에서는 데이터베이스에 저장된 Access Token과 비교하여 검증한다.
- 공격자는 탈취한 Refresh Token으로 새로 Access Token을 생성한다. 그리고 서버측에 전송하면 서버는 데이터베이스에 저장된 Access Token과 공격자에게 받은 Access Token이 다른 것을 확인한다.
- 만약 데이터베이스에 저장된 토큰이 아직 만료되지 않은 경우, 즉 굳이 Access Token을 새로 생성할 이유가 없는 경우 서버는 Refresh Token이 탈취당했다고 가정하고 두 토큰을 모두 만료시킨다.
- 이 경우 정상적인 사용자는 자신의 토큰도 만료됐으니 다시 로그인해야 한다. 하지만 공격자의 토큰 역시 만료됐기 때문에 공격자는 정상적인 사용자의 리소스에 접근할 수 없다.
역시 구글링하면 다나와!!!
사실 딥하게 들어가면 설명할게 끝도 없지만, 중요한 핵심 내용만 정리해보았다. 오늘 설명한 토큰 인증 방식에서 Refresh Token까지 이용한 방법은 나중에 직접 구현해보며 코드로 설명하는 글도 작성해보겠다!
Refresh Token 탈취당할 생각은 안해봤는데 역시 해결책은 있었네요! 로그인은 정말 생각보다 어려운 것 같아요.
자세히 설명해주신 덕분에 잘 읽었습니다!!
JWT 를 조금만 Stateful하게 구현해서 Refreseh Token을 탈취당하는 문제를 해결하는 방식도 있습니다!
https://velog.io/@blackbean99/22-JWT%EB%A1%9C-%EB%B3%B4%EC%95%88%EC%88%98%EC%A4%80-%EB%86%92%EC%9D%B4%EA%B8%B0-JWT%EB%A5%BC-Stateless%ED%95%98%EA%B2%8C
한번 놀러와서 읽어보심 좋을 것 같아요~
같이 일하고싶은동료네요