로그인을 구현한다면 꼭 알아야 할 개념들이다.
이번에 프로젝트를 진행하면서 Next-Auth를 사용해 소셜 로그인을 구현했는데, 이때 공부한 내용들과 실제 프로젝트에서 사용한 인증, 인가 방법에 대해 소개하겠다.
쿠키(Cookie)란?
먼저 쿠키다.
쿠키는 사용자를 기억하기 위해 서버가 사용자의 브라우저에 저장하는 데이터(작은 기록 정보 파일)라고 할 수 있다.
사용자가 사이트에 방문하면,
1. 브라우저는 서버에 요청(request)을 보내고
2. 서버는 응답(response)한다.
이 응답에는 사용자가 찾던 페이지 정보, 데이터뿐만 아니라 브라우저에 저장하고자 하는 쿠키가 있을 수 있다.
이 쿠키를 브라우저에 저장하게 되면, 사용자가 해당 웹 사이트를 방문할 때마다 브라우저가 자동으로 해당 쿠키를 요청과 함께 보내게 된다.
이런 쿠키에는 몇 가지 특징이 있다.
- 도메인에 따라 제한됨 (-> 네이버에서 준 쿠키는 네이버에만 보내지게 됨)
- 유효기간이 있음 (유효기간이 있는 경우 브라우저가 종료되어도 인증이 유지됨)
이런 특징을 가진 쿠키는 로그인 인증 뿐만 아니라 웹사이트 언어설정 정보와 같은 여러가지 정보를 저장할 수 있다.
그렇다면 세션과 토큰은 뭐지, 세션과 토큰은 왜 필요할까?
다음으로 세션과 토큰을 살펴보겠다.
이 세션과 토큰은 왜 필요할까?
웹 사이트를 이용할 때 쓰는 프로토콜인 HTTP가 stateless하기 때문이다.
stateless하다는 것은 서버로 가는 모든 요청이 이전 요청(request)와 독립적으로 다뤄진다는 뜻이다.
즉, 메모리가 따로 없고 요청끼리 종속되어 있지 않은 것이므로 서버는 요청이 끝나면 새로운 요청이 들어왔을 때 누구인지 알지 못한다.
이러한 이유로 요청을 할 때마다 누구인지를 계속 알려줘야한다.
이런 역할을 하는 것이 바로 세션과 토큰이다.
그렇다면 이 세션과 토큰이 정확히 무엇인지, 로그인 할 때 정확히 어떻게 사용되는 것인지에 대해 자세히 알아보겠다.
인증(Authentication)이란, 인가(Authorization)란?
로그인 기능 구현은 크게 두 가지 부분으로 나뉠 수 있다.
바로 인증과 인가이다.
한글로도 그렇고 영어로도 철자가 비슷해서 무척 헷갈린다.
인증(Authentication)은 쉽게 말해 로그인이라고 생각하면 된다.
어떤 사이트에 가입된 회원, 즉 특정 서비스에 일정 권한이 주어진 사용자임을 아이디와 패스워드를 통해 확인 받는 과정이다.
인가(Authorization)는 이렇게 한 번 인증을 받은 사용자가 이후 사이트의 여러 기능들을 사용할 때 서버가 해당 사용자가 로그인 되어있음을 알아보고 허가해주는 것을 말한다.
인스타그램 사용을 예시를 들자면 인스타그램에 로그인하는 것은 '인증'하는 것이고,
로그인 된 계정으로 친구들의 스토리를 보고, 피드를 보거나 댓글을 다는 등 해당 사용자 계정으로'만' 할 수 있는 활동을 시도할 때 서버에서 로그인 되어있음을 알아보고 허가해주는 것이 '인가'이다.
이번 글에서는 둘 중 '인가(Authorization)'을 구현하는 기술에 대해 알아보겠다.
어떤 사이트나 서비스에 사용자가 로그인해있다는 사실을 서버가 인지할 수 있도록 하는 방법에 무엇이 있을까?
지금 이 순간에도 많은 사람들이 네이버에 들어와 블로그, 카페, 메일 등의 서비스들을 사용하고 있을 것이다.
웹 사이트의 이런 클릭들은 모두 요청이기에 네이버는 이렇게 많은 사용자들의 동시다발적 요청에 응답해주고 있는 것이다.
문제는 이 사용자들 중 어떤 사용자가 인증을 받은 상태인지 확인해서 그에 따라 로그인이 필요한 기능들에 허용해줄지 말지를 결정해서 응답해야 한다는 것이다.
이를 위해 단순하게 사용자의 브라우저에 아이디, 패스워드 값을 저장하고 매 요청마다 로그인을 반복하기엔 로그인이라는 것 자체가 무거운 과정이기에 무척 비효율적이다. 또한 보안적 문제도 있을 것이다.
이런 인가 문제를 해결하기위해 나온 것이 바로 앞서 말한 세션과 토큰이다.
세션(Session)이란?
세션은 전통적으로 많이 사용되온 방식이다.
세션은 영화관 티겟을 생각하면 쉽다.
사용자가 로그인에 성공하면 서버는 티켓을 생성해 쭉 찢은 뒤, 반쪽은 사용자의 브라우저로 보내고 다른 반쪽은 자기 책상인 메모리에 올려놓는다. 경우에 따라서는 하드디스크나 데이터베이스에 올려놓기도 하지만 메모리가 속도가 가장 빠르다.
앞서 비유한 표현을 다시 정확히 표현해보자면, 로그인 성공 시 서버는 세션 DB라는 곳에 저장하고 해당 Session ID라는 것을 쿠키를 통해 브라우저로 돌아와 해당 사용자의 브라우저에 저장된다. 즉, 세션도 쿠키를 사용한다고 볼 수 있다.
이렇게 Session ID를 저장한 브라우저는 해당 사이트를 이용할 때 Session ID가 저장된 쿠키를 자동으로 보내 응답을 받는다. 서버는 해당 Session ID를 가지고 세션 DB를 확인하고, 해당 사용자가 누구인지 확인후 허가한다.
이후 다른 페이지로 이동하게 되면 위 프로세스를 반복한다.
이처럼 Session ID를 사용해 어떤 사용자가 서버에 로그인 되어있음이 지속되는 이 상태를 '세션'이라고 한다.
하지만 이런 세션 방식에는 문제가 있다.
사용자가 동시에 많이 접속을 하면 메모리가 부족해질 수 있고, 혹시나 서버에 문제가 있어 메모리가 꺼져버린다면 휘발성이라 죄다 날라간다는 것이다. 이렇게 된다면 모든 사용자의 로그인이 튕길 것이다.
또 이를 해결하기 위해 하드디스크나 데이터베이스를 이용하기엔 속도가 느리다는 문제가 있다.
더 문제는 서비스 규모가 클 때인데, 서버를 여러대 두고 로드밸런싱을 하는 경우이다. 이 경우 각 서버마다 각자의 책상, 즉 메모리가 있게 되는데 만약 로그인은 1번 서버에서 하고 이메일 페이지로 가는 요청은 3번 서버에서 하게된다면 3번 서버의 책상에는 해당 사용자의 티켓이 없기에 세션 유지가 안된다.
그래도 다행히 이 경우에는 해결책이 있는데, 바로 레디스나 MemCached 같은 메모리형 데이터베이스 서버를 사용하는 것이다. 이는 길다란 공용 책상을 따로 두는 것으로 생각하면 된다. 즉, 공용 메모리를 두고 거기서 사용자 인가를 담당하는 것이다.
하지만 레디스도 세션의 근본적인 문제를 해결해주지는 못한다.
서버가 복잡한 구성과 환경에서 어떤 상태를 기억해야된다는 것은 무척 부담스러운 일이다.
그래서 이런 부담 없이 인가를 구현하기 위해 고안된 것이 바로 '토큰 방식'인 JWT이다.
JWT란?
JWT는 JSON Web Token의 줄임말이다.
즉, JSON으로 된 웹 토큰이라는 뜻이다.
JWT를 사용하는 서비스에서는 사용자가 로그인을 하면 토큰이라는 티켓을 출력해서 건네준다.
이번에는 세션처럼 찢어서 주는 것이 아니라 그냥 주는데, 서버가 무언가를 기억하지 않는다는 이야기다.
이것이 어떻게 가능한지는 JWT에 대해 더 알아보면 알 수 있다.
JWT는 인코딩 또는 암호화된 3가지 데이터를 이어붙인 것이다.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c |
위는 JWT의 예시이다.
잘 보면 중간에 마침표가 두 군데 들어있는 것을 알 수 있다. 이 마침표를 기준으로 세 부분으로 나뉘게 된다.
이는 각각 헤더(Header), 페이로드(Payload), 서명(Verify Signature)으로 구분된다.
먼저 페이로드를 Base64로 디코딩해보면 JSON형식의 데이터들이 나온다.
데이터는 토큰을 누가 누구에게 발급했는지, 언제까지 유효한지, 서비스가 사용자에게 공개하기 원하는 내용(사용자의 닉네임, 서비스 상의 레벨, 관리자 여부)등의 정보들이다.
이렇게 토큰에 담긴 사용자 정보 등의 데이터를 Claim이라고 한다.
(더 자세한 내용은 https://jwt.io ← 링크에서 확인할 수 있다.)
이렇게 사용자는 로그인 후 토큰을 받고 Claim이라는 것으로 실려온 정보들을 이후 요청들마다 서버한테 보내게 된다.
그런데 Base64로 인코딩 되어있으면 쉽게 디코딩해서 볼 수 있으니 보안상 문제가 될 수 있지 않을까?
가령 사용자가 Base64로 페이로드 부분을 디코딩한 후, 관리자 여부등을 변경해서 서비스에 문제를 일으킬 수 있지 않을까?
이런 이유로 앞부분에 헤더와 뒷부분에 서명이 존재한다.
먼저 헤더부터 보겠다. 헤더를 디코딩 하면 두 가지 정보가 담겨있는데, 토큰의 type과 alg이다. 토큰의 type은 JWT로 고정값이고 alg는 알고리즘을 뜻하는데 3번 서명 값을 만드는데 사용될 알고리즘이 저장된다. HS256 등 여러 암호화 방식 중 하나를 지정할 수 있다.
서버는 사용자가 요청과 함께 실어보낸 토큰의 1번 헤더와 2번 페이로드, 그리고 '서버에 감춰놓은 비밀 값' 이 셋을 헤더에서 명시한 암호화 알고리즘에 넣고 돌린다. 그리고 그것이 3번 서명 값과 일치한다면 인가하게 된다.
암호화 알고리즘은 한쪽 방향으로만 계산이 되기 때문에 서버만 알고 있는 비밀 값을 찾아낼 수가 없다.
따라서 토큰을 탈취해 페이로드의 값을 변경한다하더라도 서명 값이 완전히 달라지기에 조작이 불가능해진다.
(누가 생각했을까.. 정말 똑똑한거 같다. 찾아보니 Open ID라는 곳에서 초안을 제시했다고 한다.)
이렇게 JWT 방식은 세션과 다르게 사용자들의 상태를 저장할 필요가 없이 요청이 들어올 때마다 서버의 비밀값으로 확인만 해주면 되기에 앞서 언급했던 세션의 문제점을 해결해줄 수 있다.
하지만 아쉬운 것은 대부분의 IT 대기업들은 이 JWT를 사용하지 못한다는 것이다.
이는 세션을 대체하기에 JWT에 큰 결점이 있기 때문이다.
세션처럼 Stateful해서, 모든 사용자들의 상태를 기억하고 있다는 건 구현하고 부담되고 고려사항도 많지만 구현만 하면 기억하는 대상의 상태들을 언제든 제어할 수 있다는 의미가 된다.
예를 들어 한 기기에서만 로그인 가능한 서비스를 만들려고 하는 경우 PC에서 로그인한 상태의 어떤 사용자가 핸드폰에서 또 로그인하면 PC에서는 로그아웃되도록 기존 세션을 종료할 수 있는 것이다.
JWT 방식에서는 이미 줘버린 토큰을 뺏을 수도 없고 그 토큰의 발급 내역이나 정보를 서버에서 기록하거나 추적하고 있지 않기에 통제가 안되는 것이다.
또 어떤 해커가 토큰을 탈취한 경우 해당 토큰을 무효화 할 방법도 없게된다.
이러한 이유로 실 서비스 중 JWT만으로 인가를 구현하는 곳은 많지 않다.
그럼 JWT는 못쓰는 것인가?
이를 나름대로 보완한 방법들이 몇 있긴 하다. 그 중 많이 쓰는 방법은 토큰을 두 개 주는 방식이다.
이 두 개의 토큰은 각각 수명이 몇 시간이나 몇 분 이하로 짧은 access 토큰과 2주 정도로 꽤 길게 수명을 받는 refresh 토큰으로 구성된다.
이 둘을 구현하는 방법은 여럿이 있다. 그 중 하나를 소개하겠다.
1. access 토큰과 refresh 토큰을 발급하고 클라이언트에게 보냄
2. 서버는 refresh 토큰은 상응값을 데이터베이스에도 저장
3. 이후 사용자의 access 토큰이 수명을 다하면 사용자는 refresh 토큰을 서버에 보냄
4. 서버는 해당 토큰을 데이터베이스에 저장한 값과 대조해보고 맞다면 새 access 토큰을 발급
이제 이 refresh 토큰만 안전하게 관리된다면 이것이 유효할 동안은 access 토큰이 만료될 때마다 다시 로그인을 할 필요 없이 새로 발급을 받을 수 있다.
정리하면, 매번 인가를 받을 때 쓰는 수명 짧은 토큰이 access 토큰이고 이 access 토큰을 재발급 받을 때 쓰는 것이 refresh 토큰인 것이다. 이렇게 하면 중간에 access 토큰이 탈취당해도 access 토큰은 수명이 짧기에 오래 쓸 수 없다. 또 누군가를 강제 로그아웃 시키고 싶은 경우 refresh 토큰을 데이터베이스에서 지워 토큰 갱신이 안되도록 하면 되므로 관리가 가능해진다. (물론 access 토큰이 살아있는 짧은 시간 동안은 바로 차단이 불가능하다. 즉, 확실한 해결책이라 보긴 어렵다.)
위와 같이 JWT에는 한계가 있기에 실 서비스에서는 세션 방식을 많이 사용한다.
따라서 JWT를 적용하기 전, 각 서비스의 성격을 잘 분석하고 적용해봐야 할 것 같다.
'Base > 용어 개념' 카테고리의 다른 글
CDN이란? (What is Content Delivery Network?) (0) | 2023.01.25 |
---|---|
콜백 함수란? (What is a callback function?) (javascript) (1) | 2022.06.17 |
디자인 패턴이란? (Software Design Pattern) (0) | 2022.03.26 |
객체 지향 프로그래밍이란?(OOP : Object Oriented Programming) (0) | 2021.03.01 |
margin과 padding이란? (margin, padding의 의미 및 속성) (2) | 2021.01.10 |