[Develog] 골치아픈 로그인 및 인증 - 1. 이론 정리 (세션, 쿠키, JWT)
2024.04.13- -
Overview
지금까지 꽤 많은 프로젝트를 진행해봤지만 로그인 관련 부분은 항상 걸림돌이 되는 부분 중 하나였다. ‘세션’이니 ‘jwt’니 하는 수많은 용어들을 제대로 익히지도 못한 채 로그인을 구현하려 했었고 항상 벽에 막혀 제대로 구현하지 못한 경험을 했었다.
그리하여 이번 프로젝트에서는 제대로 된 로그인 기능을 구현해보려 여러 삽질들을 해보았고 어떤 과정들을 거쳐 최종적으로 어떻게 구현할 수 있었는지와 관련된 기록들을 이 포스팅에 남기려 한다.
"There is no silver bullet"
시작에 앞서 가장 중요한 점을 말하자면 소프트웨어 공학에는 모든 곳에 적용 가능한 솔루션은 없다는 의미로 "silver bullet은 없다"라는 말이 존재하는데, 로그인 즉 인증 방식에도 이 말이 적용되는 것 같다고 생각했다. 내 프로젝트의 규모와 요구사항 기술 트렌드들을 적절히 고려하여 괜찮은 방식을 고르고 해당 방식의 레퍼런스를 여러 개 찾아 내 프로젝트에 맞게 적절히 적용하면 되는 것이다.
1. 인증(Authentication)
1-1. 인증에 대한 정의
인증은 사용자의 신원을 검증하는 행위로서 보안 프로세스의 가장 첫 번째 단계이다.
예를 들어, ‘공항 보안 검색’에 비유를 들어보겠다.
- 공항 입구에 도착 (웹 사이트 방문)
- 우리는 여행을 가기 전 먼저 공항에 도착하게 된다.
- 이후 보안 검색대로 가서 자신이 누구인지를 증명해야 하는데, 이와 마찬가지로 웹 사이트에 방문했을 때 로그인 페이지에 도착한 것에 예를 들 수 있다.
- 사용자는 웹 사이트를 이용하기 위해 자신의 신분(id, password)을 증명해야 한다.
- 신분증 제시 (id, password 입력)
- 공항에서는 신분증을 통해 여행자의 신원을 확인한다.
- 웹 애플리케이션에서는 사용자가 아이디와 비밀번호(credentials)를 입력함으로써 이와 같은 과정을 수행할 수 있다. 사용자가 제공한 정보는 마치 공항의 신분증과 마찬가지로, 사용자가 누구인지를 웹 애플리케이션에 알려준다.
- 보안 검색대 통과 (인증과정)
- 신분증이 유효하다고 판단되면, 보안 검색대를 통과할 수 있다.
- 웹 애플리케이션에서는 사용자가 입력한 아이디와 비밀번호가 데이터베이스에 저장된 정보와 일치하는지 확인한다. 이 정보가 정확하다면, 사용자는 웹 애플리케이션의 보안 검문소를 ‘통과’하고, 사이트 내부로 ‘입장’할 수 있다.
- 보안 검사 통과 후 (인증 후 액세스)
- 보안 검색대를 통과하고 나면, 더 내부로 들어가 여행자는 공항의 다양한 서비스와 시설을 이용할 수 있다.
- 마찬가지로 웹 애플리케이션에서 사용자 인증을 성공적으로 마치면, 사용자는 더 이상 자신의 자격증명을 제시하지 않고도 해당 페이지 내에 로그인 상태에서만 접근할 수 있는 다양한 서비스와 정보에 액세스할 수 있다.
- 만약 ‘보안 검색대’라는 것이 없었다면 우리는 공항의 시설을 이용할 때마다 돈과 더불어 자신의 신분증을 계속해서 제시해야 할 것이다. 이를 웹 페이지에 적용하여 말하면, 사용자가 페이지 내의 어떤 버튼을 누를 때마다 로그인 화면이 등장하여 매번 자격 증명을 요구할 것임을 의미한다.
- 탑승권 확인 (세션 관리)
- 공항 내에서 여행자가 비행기에 탑승하기 전, 다시 한 번 탑승권을 확인한다.
- 웹 애플리케이션에서는 세션 관리가 이에 해당한다. 사용자가 로그인한 후에도, 웹 애플리케이션은 사용자의 활동을 추적하고 사용자가 계속해서 자신임을 확인(즉, 여전히 같은 ‘탑승권’을 소지하고 있음)하기 위해 세션을 사용한다.
- 혹은 추가적인 인증정보가 필요할 때 사용자에게 정보를 요구하는 것도 이에 포함된다고 할 수 있겠다.
이러한 과정들을 통해 웹 애플리케이션은 사용자의 신원을 확인하고, 사용자에게 적절한 서비스를 제공할 수 있다. 인증 과정은 사용자와 시스템 사이의 신뢰를 구축하는 첫 단계로, 보안의 핵심 요소 중 하나이다.
1-2. 인증이 필요한 이유
앞서 인증의 정의를 설명하면서 같이 비유를 든 ‘공항 보안 검색’은 반드시 한 사람마다의 인증이 필요한 마땅한 이유가 있었다.
- 해외여행에 결격사유가 있는 자인지를 확인하기 위해
그런데 웹 애플리케이션에는 사용자가 본인임을 증명하는 인증 과정이 왜 필요한 것일까?
이를 이해하기 위해선 HTTP의 특성에 대해서 알 필요가 있다.
HTTP(Hyper Text Transfer Protocol)은 인터넷에서 사용되는 프로토콜(통신 규약) 중 하나로, 이름에서 알 수 있듯 하이퍼 텍스트를 전송하는 데 특정 규칙을 따르기 위해 만들어진 것이다.
Hyper Text는 프로그래머라면 알 수 있는 HTML과 같은 하이퍼미디어 문서를 의미하며 이러한 문서를 애플리케이션 계층(Application Layer)에서 어떤 방식으로 주고 받을 지를 정한 약속으로 인터넷 페이지에서 사용되는 대부분의 통신은 이 HTTP 방식을 따른다.
HTTP는 클라이언트가 요청을 하기 위해 연결을 연 다음, 응답을 받을 때까지 대기(listen)하는 전통적인 클라이언트-서버 모델을 따른다.
여기서 중요한 점은 HTTP 프로토콜이 본질적으로 상태를 유지하지 않는 stateless한 특성(무상태성)을 가지고 있다는 것이다. 이는 각 HTTP 요청이 독립적이며, 이전 요청의 정보를 기억하지 않는다는 것을 의미한다.
그래서 사용자의 인증을 요구하는 이유는 이 HTTP의 stateless한 성질 때문인데 이에 대해 더 자세히 알아보자.
- 상태 유지의 부재:
- HTTP의 stateless 특성 때문에, 서버는 클라이언트가 이전에 인증을 받았는지 여부를 자동으로는 알 수 없다. 그렇기 때문에 같은 출처의 요청임에도 불구하고 서버는 이 요청이 전에 왔던 요청인지를 알 방법이 없어 인증 없이는 매번 사용자 맞춤 정보를 줄 수 없다는 것을 의미한다.
- 예를 들어, 사용자가 로그인을 한 상태에서 다른 페이지로 이동하면, 새로운 HTTP 요청이 발생하게 된다. 하지만 HTTP 자체는 별도의 처리 없이는 이전 요청에서 사용자가 이미 인증을 받았다는 사실을 ‘기억’하지 못한다. 따라서 이 경우에 웹 애플리케이션은 사용자가 이동할 때마다 그 사용자가 누구인지, 그리고 인증된 사용자인지를 지속적으로 확인해야 하는 문제가 생기게 된다.
- 인증과 세션 관리의 필요성:
- HTTP의 이러한 특성으로 인해, 웹 애플리케이션은 사용자 인증을 통해 사용자의 신원을 확인하고, 세션 관리를 통해 사용자의 상태(예: 로그인 상태)를 계속해서 유지시켜 줄 방법을 마련해야 하는 것이다.
- 사용자가 처음 로그인할 때 인증 과정을 거치고, 이후의 요청에서는 세션 쿠키나 토큰 같은 메커니즘을 사용하여 사용자가 인증된 사용자임을 확인할 수 있다.(뒤에서 다시 설명함)
- 보안과 사용자 경험:
- 앞서 말했듯 HTTP가 상태를 유지하지 않기 때문에, 매 요청에 대해 독립적으로 보안을 유지해 주어야 한다.
- 인증은 이 과정에서 중요한 역할을 한다. 사용자가 누구인지 식별하고, 그에 따른 권한을 부여하여 사용자에게 안전하고 맞춤화된 서비스를 제공할 수 있다.
- 상태 정보의 관리:
- 인증과 세션 관리는 상태 정보(사용자의 로그인 상태, 사용자의 권한 등)를 서버 측 혹은 클라이언트 측에 저장하여, HTTP 요청이 독립적임에도 불구하고 일관된 사용자 경험을 제공하는 방법이다. 즉, 상태가 없는 프로토콜 상에서 상태 정보를 유지할 수 있고 관리할 수 있는 역할을 한다.
이처럼 사용자가 시스템과 상호작용하는 동안 일관된 상태와 보안을 유지하기 위해, 웹 애플리케이션은 이러한 메커니즘을 적절히 구현하고 관리해야 하는 것이다.
2. 인가(Authorization)
인가는 사용자가 액세스할 수 있는 리소스 또는 수행할 수 있는 작업에 대해 권한을 부여하는 과정을 말한다. 즉, 액세스 권한을 확인하는 프로세스인 것이다.
실생활에서 인가는 어떻게 사용될까?
회사 건물 내의 특정 공간에 들어가기 위해서 사원증을 찍는 광경을 본 적 있을 것이다. 해당 사원증에는 내 사진과 이름, 부서 등 사내에서 나를 식별하기 위한 몇몇 정보가 포함되어 있다.
회사 내의 보안 시스템에 내 카드키를 대서 나의 신원을 확인함과 동시에 내가 해당 공간에 들어갈 수 있는 사람인지를 확인 후 문이 열리게 된다.
그러나 이런 카드키가 있다고 무조건 회사 내의 모든 공간에 들어갈 수는 없듯이 인가는 어떠한 액세스에 대해 해당 사용자의 접근 가능 여부를 결정해주는 역할을 하게된다.
그래서 사실 일반적인 개념에서 인증과 인가의 과정은 특정 시나리오에서 상호 교환 가능하기도 하기 때문에 어디까지가 인증이고 어디까지가 인가인지 나누는 것은 모호할 수가 있다.
2-1. 인가의 특징
이전에 설명한 인증 개념을 바탕으로 인가가 인증과는 어떻게 다른지 설명하자면, 인증에서 인가로(인증→인가)로 프로세스가 이어질 수는 있지만 인가에서 인증으로(인가→인증)으로는 이어질 수 없다는 점을 들 수 있을 것 같다.
앞선 비유에서 회사의 카드키를 예로 들었는데 카드키에는 내 정보가 포함되어 있기 때문에 사내 정보망에 등록된 내 정보를 토대로 내가 해당 공간에 들어갈 수 있는지 여부를 자동으로 알 수 있다. 하지만 반대로 내가 해당 공간에 들어갈 수 있는 권한이 있다고 해서 들어가는 주체가 항상 나임을 특정할 수는 없는 것이다.
따라서 인증은 권한 부여 결정의 요소로 사용될 수 있지만, 인가는 인증을 하기에 유용하지 않을 수 있다.
2-2. IAM과 인가: 인가와 액세스 정책
위의 예시를 보면 알겠지만 "사원증으로 특정 공간에 출입 가능 권한을 얻는 것"은 사내 전산 시스템에서 IAM(Identity and management)이라는 IT 규율의 일부로 표현할 수도 있다. 그렇기에 인가를 통해 권한 부여를 하는 것은 액세스 정책과도 밀접한 관계가 있다고 말할 수 있다.
이는 권한 부여 정책을 통해 액세스 정책을 구현할 수 있기 때문인데, 몇 가지 권한 부여 정책에 대해서 짧게 알아보자.
ABAC(Attribute-Based Access Control)
컴퓨터 시스템이 사용자에게 작업을 실행할 권한을 부여할 때 사용하는 기준 자체를 사용자와 관련된 특성을 가지고 판단하는 것을 말한다.
예를 들어, 편의점과 같이 물건 판매 시스템이 있을 때, 주류 등과 같이 성인만이 구매할 수 있는 제품의 경우는 ‘사용자의 나이’라는 특성을 바탕으로 물건 구매에 대한 권한이 결정된다.
이에, 온라인 물건 판매자는 해당 리소스의 소유자로 "서버"에 빗댈 수 있고, 주류는 "자원"에 빗대어 질 수 있다. 그래서 이 경우 인증과정에서 사용자의 신원을 증명함에 더불어 나이에 대한 증명도 처리된다.
RBAC (Role-Based Access Control)
사용자의 직접적 정보가 아닌 "사용자 역할"을 기반으로 자원에 대한 액세스를 제어하는 것을 말한다.
역할이란 단순히 권한의 모음으로, AWS IAM을 통해 배포 자동화 시스템을 만드는 경우를 한 번 생각해보자.
Code Deploy를 통해 코드 배포 요청이 들어오면 S3로부터 압축 파일을 가져와 새로운 EC2 인스턴스를 생성해야 하는데, 이 경우 권한은 ‘S3로부터 압축 파일을 가져오는 데 필요한 권한’, ‘새로운 EC2 인스턴스를 생성할 권한’이 필요하게 된다.
따라서 역할이란 이러한 권한들을 묶은 ‘배포자 권한’ 정도로 즉, 사용자와 권한을 하나씩 처리하지 않고 범주의 개념으로써 처리 가능한 것을 말한다.이렇게 하면 권한 관리가 용이하다는 장점이 있다.
3. 다양한 인증 방식 및 변천사
세션(session)은 컴퓨터 과학에서, 특히 네트워크 분야에서 반영구적이고 상호작용적인 정보 교환을 전제하는 둘 이상의 통신 장치나 컴퓨터와 사용자 간의 대화나 송수신 연결상태를 의미하는 보안적인 다이얼로그(dialogue) 및 시간대를 가리킨다. 따라서 세션은 연결상태를 유지하는 것보다 연결상태의 안전성을 더 중요시 하게 된다. (by위키피디아)
이 글에서 다양한 인증 방식에 대한 설명들은 항상 사용자가 이미 회원가입을 마친 상태에서 로그인을 하는 경우를 가정한다.
3-1. Request Header 활용하기
전통적인 방식의 인증은 사용자가 인증 과정을 거쳐 시스템에 로그인한 후, 이후 발생하는 모든 요청에 사용자의 자격 증명(credentials)을 포함시켜 서버에 전송하는 방식이었다. 이 과정에서 매 요청마다 Request Header에 자격 증명 정보를 담아 인증을 수행한다. 가장 일반적인 형태로는 HTTP Basic Authentication이 사용된다.
username과 password를 raw한 상태로 보내면 탈취의 위험이 있기 때문에 cdragon:p1234 와 같은 형태의 문자열을 Base64 방식으로 인코딩하여 변환과정이 수반된다.
정리하면, HTTP Basic Authentication은 가장 간단하고 전통적인 방식으로, 사용자의 이름과 비밀번호를 콜론(’:’)으로 연결한 후, Base64로 인코딩하여 HTTP 요청의 ‘Authorziation’ 헤더에 포함시키는 방식이다. 서버에서는 이 헤더를 디코딩하여 사용자 이름과 비밀번호를 추출하고, 이를 검증하여 요청을 인증한다.
- 물론 이때 인코딩은 사용자가 직접할 필요없이 브라우저가 요청을 보낼 때 자동으로 해준다.
작동 방식 정리
- 로그인 시도: 사용자가 웹 애플리케이션에 로그인을 시도할 때, username과 password를 입력한다.
- 요청 헤더 구성: 클라이언트(브라우저)는 사용자 이름과 비밀번호를 ‘username:password’ 형태로 조합하고, 이를 Base64로 인코딩한다. 그런 다음, 이 값을 ‘Authorization’ 헤더에 ‘Basic [인코딩 된 값’ 형태로 추가한다.
- 서버 인증: 서버는 HTTP 요청 헤더에 포함된 ‘Authorization’ 필드의 값을 확인하여 인코딩된 값을 다시 디코딩한 다음, username과 password를 얻어내 검증에 사용하고, 검증에 성공하면 요청에 대한 처리를 계속하지만, 만약 실패했다면 인증 실패 응답을 반환한다.
취약점
이 방식에는 너무 잘 보이는 취약점 몇 가지가 존재한다.
- 텍스트 전송의 위험성: Basic Authentication에서는 사용자 이름과 비밀번호가 Base64로 인코딩되어 전송되긴 하지만, 이는 암호화 방식이 아니기 때문에 중간자 공격(MiTm)에 취약하다. 이 텍스트 자체를 중간에 가로챌 수만 있다면 그냥 아무나 Base64 문자열을 디코딩해주는 사이트에 들어가 해당 정보를 까볼 수 있기 때문에 HTTPS를 사용하지 않는 경우 정보가 도난될 위험이 매우 크다.
- 인증 정보의 재사용: 인증 정보가 매 요청에 포함되어야 하기 때문에 한 번 취득된 인증 정보를 다른 요청에 재사용해야 한다. 이는 특히 크로스 사이트 요청 위조(CSRF) 같은 공격에 취약할 수 있다.
- 상태 유지의 부담: 서버는 상태를 유지하지 않는 HTTP 프로토콜 상에서 매 요청마다 사용자를 인증해야 하므로, 이 과정이 서버에 부담을 줄 수 있다. 특히 대규모 시스템에서는 인증 처리로 인한 오버헤드가 문제가 되기도 한다.
- 인증 정보 관리의 어려움: 클라이언트 측에서 인증 정보를 안전하게 관리하는 것은 다소 어렵다. 특히 사용자의 디바이스가 손상된 경우, 인증 정보가 노출될 위험이 더욱 크다.
3-2. Browser를 활용하기
앞선 방식의 매번 인증을 해줘야 함으로써 생기는 여러 취약점들을 보완하기 위해 나온 방식이 인증 상태를 사용자 마다의 브라우저를 활용하여 저장하자고 해서 나온 방식들이다.
브라우저의 힘을 빌린다는 의미는 더 정확하게 말하자면 그 브라우저가 가진 스토리지(저장소)의 힘을 빌리겠다는 의미이다.
브라우저의 스토리지에는 local storage, session storage, cookies와 같이 다양한 방식의 저장 방식이 존재하지만 특히 쿠키를 활용한다고 가정해보면 단순히 해당 저장소에 사용자의 ID와 비밀번호를 key, value 형태로 저장을 해 놓았다가, 요청을 할 때마다 요청 Header에 사용자의 자격 증명 정보를 저장소로부터 가져와 보내기만 하면 된다.
그렇게 하면 사용자가 매번 username과 password를 입력하지 않아도 미리 저장한 값이 요청에 자동으로 포함되어 가기 때문에 꽤 편리한 방식이기도 하다.
취약점
이러한 방식은 굉장히 간편하게 정보를 주고 받을 수 있을 것처럼 보이고 실제로 편리한 방식 중 하나로 꼽히지만, 이는 전문 해커 입장에서 봤을 때 아무래도 서버로부터 데이터를 탈취하는 것은 까다롭고 어렵기 때문에 클라이언트 쪽으로부터 데이터를 탈취하는 것이 더 쉽고 그 탈취한 데이터 자체가 날 것 그대로이기 때문에 탈취 당했을 경우 상당히 취약하다는 단점이 존재한다.
3-3. Session을 활용하기
그렇게 인증 기술은 앞선 방식의 클라이언트에 정보를 저장함으로써 취약점들이 생겼기 때문에 역으로 서버에 유저 정보를 저장하는 방식을 기반으로 다시 등장하게 된다.
이 방식은 크게 사용자의 로그인 정보를 전송하는 초기 단계와, 이후의 요청에서 사용자의 인증 상태를 유지하는 방법으로 나뉘게 된다.
초기 로그인 과정
- 로그인 정보 전송: 사용자가 웹 애플리케이션의 로그인 input form에 자신의 id와 비밀번호를 입력한다. 클라이언트는 이 정보를 ‘Authorization: Basic’ 헤더에 인코딩하여 로그인 요청과 함께 서버로 전송한다. 사용자 이름과 비밀번호는 콜론(’:’)으로 구분하고, Base64로 인코딩하는 방식으로 헤더에 포함된다.
- 서버에서의 인증: 서버는 ‘Authorization’ 헤더를 받아 Base64 디코딩을 수행하여 id와 password를 추출한다. 서버는 이 정보를 데이터베이스에 저장된 사용자 정보와 비교하여 인증 검증 과정을 수행한다.
- 세션 생성: 인증 검증에 성공했다면, 서버는 각 사용자마다 고유한 임의의 세션 ID를 생성한다. 이 세션 ID는 서버의 메모리나 DB에 저장되는 세션 정보와 연결되며, 세션 정보에는 '사용자의 인증 상태', '권한' 등을 비롯한 다양한 정보들이 포함될 수 있다.
- 쿠키 설정: 서버는 생성된 세션 ID를 클라이언트에게 되돌려 보내기 위해 HTTP 응답의 ‘Set-Cookie’ 헤더를 사용하여 클라이언트의 쿠키에 세션 ID를 저장시킨다. 예를 들어, ‘Set-Cookie: sessionid=12345; Path=/; HttpOnly’와 같은 형태로 헤더에 담아 클라이언트에게 전달된다.
이후의 요청과 세션 유지 방법
- 쿠키 전송: 로그인 후 사용자가 다른 요청을 보낼 때, 브라우저는 자동으로 해당 도메인의 쿠키를 요청에 포함시켜 서버로 전송해준다. 이때, 브라우저에서 ‘Cookie’ 헤더를 통해 세션 ID가 포함되어 요청이 전달된다.
- 세션 검증: 서버는 요청을 받을 때마다 ‘Cookie’ 헤더에서 세션 ID를 추출하고, 이를 서버 측에 저장된 세션 정보와 비교한다. 세션 ID가 유효하고 현 시점에서 인증된 상태라면, 요청을 허가하고 사용자가 본인의 계정과 관련된 작업을 수행할 수 있도록 해준다.
- 세션 만료 관리: 세션은 보안상의 이유로 일정 시간이 지나면 만료되도록 설정된다. 사용자가 활동을 계속하는 경우에는 알아서 자동으로 세션의 유효 시간이 갱신되지만, 일정 시간 동안 활동이 없는 경우 세션은 만료되고 사용자는 다시 로그인을 하도록 리다이렉트 된다.
이러한 방식의 장점은 아무래도 사용자 정보와 관련된 직접적인 데이터를 클라이언트가 가지고 있지 않기 때문에 해커가 데이터를 탈취하더라도 날 것 그대로의 정보가 아닌 그저 sessionId를 탈취한 것이기 때문에 사용자의 정보가 직접적으로 노출되지 않는다는 점이다.
또한 앞서 말했듯 세션의 만료 시간을 정할 수 있기 때문에 해커가 sessionId를 탈취하더라도 금방 유효하지 않게 되어 보안적으로 상당한 이점을 가질 수 있다는 점이 있다.
취약점
대규모 분산 시스템에서 이러한 세션 기반 인증을 사용하게 되면 서버를 여러 대 두어야 하는 경우가 생긴다. 그런데 이때, 세션 정보는 각 서버에서 별도로 관리를 하게 되는데 로드 밸런서를 통해 한 사용자의 요청이 한 서버에만 할당될 것이라는 보장이 없기 때문에 사용자의 요청이 처음 도달한 서버라면 해당 사용자에 대해 다시 또 세션을 생성해야 하므로 DB에 다소 불필요한 쿼리를 날리는 문제가 생긴다.
- 그렇다고 한 사용자의 요청을 한 서버에 요청을 하는 방식도 문제가 존재하기도 한다.(Sticky Session)
이러한 문제를 해결하기 위해 시스템의 DB와 별개로 Session DB를 마련하여 각각의 서버가 관리하는 세션 정보를 한 저장소에서 관리하도록 하는 방식이 도입되기도 한다.
외부 세션 스토리지는 입출력이 잦은 세션의 특성 상 I/O 성능이 느린 데이터베이스는 사용하기 적절하지 않아 보통 In-Memory DB인 Redis를 많이 사용한다.
그런데…
이 역시도 클라이언트가 매우 많아지게 되었을 때는 세션 DB는 하난데 무수한 Read를 한 DB에서 처리해야 하기 때문에 DB의 성능이 저하되거나 심하게는 DB가 터질 수도 있다는 문제가 있다. 또한 해당 세션 스토리지에 장애가 발생하면 모든 서버가 세션 인증을 정상적으로 사용할 수가 없게 되어 서비스 중지 문제가 발생하게 될 수 있다.
그래서 세션 스토리지도 여러 대 마련하여 관리하는 방식을 사용하기도 한다.
* 여기서 잠깐) 로그인 정보(ID, 비밀번호)는 어디에 담아 보낼까?
Authorization 헤더에 담는 방식
이 방식은 앞서 계속해서 설명했던 방식으로 HTTP 요청의 헤더 부분에 Authorization 필드를 사용하여 사용자의 인증 정보를 포함시켜 서버로 전송하는 방식이다. 가장 흔한 예는 HTTP Basic Authenticaion이며 여기서 사용자 ID와 비밀번호를 콜론으로 구분하고 Base64 인코딩하여 전송한다.
이 방식은 웹 표준으로 널리 지원되며, 서버에서는 헤더를 파싱하여 사용자의 인증 정보를 추출할 수 있다. 그렇기 때문에 상태를 유지하지 않는 HTTP 프로토콜 상에서 클라이언트의 인증 상태를 간단히 전달할 수 있는 방법을 제공한다. 보안을 위해 HTTPS와 함께 사용될 때 인증 정보가 암호화되어 전송될 수 있다.
Body에 담는 방식
실제 프론트엔드 개발을 해보면 알겠지만 id, password 정보는 보통 body에 담아서 보내는 방식으로 요청을 전송하는 경우가 많았다.(내 경우에)
Body에 담아 보내는 방식은 주로 POST 요청의 본문에 사용자의 로그인 정보를 포함시켜 전송하는 방식이다. 이는 주로 웹 form 태그를 통한 로그인이나 REST API에서 JSON 또는 다른 데이터 포맷을 사용하여 인증 정보를 전송할 때 사용된다.
이 방식은 요청 body에 데이터를 포함시키기 때문에, 더 구조화되고 복잡한 데이터를 전송할 수 있다는 장점이 있다. 이 방식 역시 HTTPS를 사용하여 데이터의 안전성을 보장한다
결론적으로 Authorization 헤더는 인증 메커니즘의 표준화와 간결함의 이유로 선택될 수 있으며, 특히 상태를 유지하지 않는 HTTP 프로토콜에서 클라이언트의 인증 상태를 효과적으로 전달할 수 있다.
- 쉽게 말해 인증 정보를 HTTP 요청의 일부로 명확히 분리하여 서버에서 쉽게 처리할 수 있기 때문이다.
반면 Body는 보다 복잡한 인증 메커니즘이나 추가적인 로그인 정보(예: 로그인 토큰, CSRF 토큰)를 전송할 필요가 있을 때 선택되며, RESTful API 디자인에서 특히 선호된다.
3-4. 다시 돌아와서 그럼 사용자 정보를 어디서 유지해야 하나…
클라이언트, 서버, 세션 저장소 이 세 곳 모두에 한 번씩 사용자의 상태를 유지하려고 해보았지만 세 방식 모두 문제가 있었다.
어떤 방식을 골라도 계속 문제가 존재하는 이유는 무엇일까?
그 이유는 서두에서 언급했듯이 아무래도 웹 표준에서 통신을 할 때 사용하는 HTTP와 서버 자체가 지향하는 RESTful 한 API가 무상태성(stateless)을 지향하는데,
우리가 실제로 인증과 인가를 서비스에서 구현할 때는 사용자의 상태를 클라이언트나 서버가 가지도록 맡겨버리기 때문에 결국 상태성을 부여하는 꼴이 되어버린다.
따라서 두 패러다임이 충돌을 하고 있는 상황이기 때문에 앞선 문제들을 없애기 위해서는 이 충돌을 해소해야 하지만 이것이 그렇게 쉬운 문제는 아니다.
아직 포기하기 이르다
다시 돌아와서 위 그림을 보면 우리는 앞서 클라이언트와 서버 각각에 사용자의 상태를 맡겨보았지만 결국 두 방법 모두 수포로 돌아갔다.
사실 서버와 클라이언트 말고도 인증 상태를 유지시킬 수 있는 공간이 하나더 존재한다.
바로 화살표(콘텐츠의 흐름)에 상태를 유지해보자는 것이다. - 토큰 방식
3-5. 토큰(Token)
토큰 기반의 인증은 웹 뿐만 아니라 모바일 애플리케이션에서 사용자의 신원을 확인하고 인증 상태를 유지하는 현대적인 방법이다. 이 방식에서는 사용자가 로그인할 때 서버가 고유한 인증 토큰을 생성하고 이를 사용자에게 전달한다.
사용자는 이후의 모든 요청에 이 토큰을 포함시켜 서버에 전송하며, 서버는 이 토큰을 검증하여 요청이 유효한지 판단한다.
토큰 기반의 인증은 상태를 유지하지 않는 무상태성 때문에 RESTful API와도 잘 어울리며, DB에 접근하지 않아도 되기 때문에 스케일링과 분산 시스템에 있어도 유리한 점을 가진다.
토큰은 주로 JSON Web Token, 약칭 JWT를 통해서 만들어지며, 이는 간단히 말하자면 secret key를 사용해서 JWT를 만들어내고 다시 또 secret key를 통해 해당 JWT가 본 서버에서 만든 것인지 검증하는 과정들을 거치게 되는 것이라고 보면 된다.
사실 JWT 값 자체는 디코딩이 쉽기 때문에 누구나 탈취 시 그 안의 내용을 확인할 수가 있어 JWT 내에는 민감한 정보를 담지 않도록 해야 한다.
또한 secret key가 JWT의 서명(signature) 단계에 사용되기 때문에 이 키가 공격자에게 노출이 된다면 해당 key로 유효한 JWT를 자유롭게 생성하여 특정 사용자를 가장하거나 권한이 더 높은 사용자의 권한을 얻어 애플리케이션 내에서 비정상적인 활동을 수행할 수 있게 되기 때문에 이 키에 대한 보안에 신경을 써야 한다.
작동 방식은 다음과 같다.
- 로그인: 사용자가 로그인 정보(예: 사용자 ID, 비밀번호)를 입력하여 로그인 요청을 보낸다.
- 토큰 생성 및 전달: 서버는 요청으로부터 로그인 정보를 검증하고 유효하다면, 고유한 인증 토큰(JWT)을 생성한다. 이 토큰은 사용자의 신원, 인증 시간, 만료 시간 등을 포함할 수 있다. 이렇게 생성된 토큰은 사용자에게 전달된다.
- 토큰 사용: 사용자는 서버에 요청을 보낼 때 HTTP 헤더에 이 토큰을 포함시켜서 보내게 된다. 일반적으로 ‘Authroization’ 헤더에 ‘Bearer {토큰}’ 으로 포함시키면 된다.
- 토큰 검증: 서버는 각 요청을 받을 때마다 해당 헤더에서 토큰을 추출하고, 이를 검증한다. 토큰이 유효하면 요청을 처리하고, 그렇지 않으면 오류 메시지를 반환한다.
이 방식의 장점은 명확하다. 서버가 사용자의 인증 상태를 아까와 같이 세션 저장소에 저장하는 것처럼 별도의 DB를 마련하지 않아도 되기 때문에 서버 자원 사용을 최소화할 수 있고 그로 인해 서버 확장에도 용이하다는 것이다.
또한 토큰 방식은 여러 대의 서버가 존재하는 경우에도 각 서버가 secret key만 가지고 있다면 모든 서버가 해당 사용자의 토큰을 검증할 수 있기 때문에 로드 밸런서 뒤에 있는 여러 서버들에 요청을 별도의 아무런 처리 없이도 쉽게 분산시킬 수가 있는 것이다.
사용자가 임의로 토큰을 수정한다고 다른 사용자의 정보를 얻을 수 없는 이유는 이 secret key를 가지고 마지막에 새로운 서명을 생성하기 때문에 서버가 토큰을 검증하는 과정에서 유효하지 않은 토큰으로 받기 때문인 것이다.
단점
토큰 방식의 단점은 다음과 같다.
- 토큰 저장의 책임: 클라이언트 측에서 토큰을 안전하게 저장하고 관리해야 하는 책임이 생긴다. 웹 애플리케이션의 경우, 쿠키, 로컬 스토리지, 세션 스토리지 등 여러 저장 옵션이 있지만, 각각 XSS(Cross Site Scripting)나 CSRF(Cross Site Request Forgery) 공격에 대한 취약점을 가질 수 있다.
- 토큰 탈취와 재사용: 만약 토큰이 탈취당하면, 공격자는 토큰이 만료될 때까지 해당 토큰을 사용해 서비스에 접근할 수 있다. 특히, JWT는 만료 시간이 내장되어 있지만, 만료 전까지는 서버 측에서 토큰의 사용을 즉각적으로 막을 수 있는 방법이 제한적이다.(세션의 장점과 대비되는 부분)
- 토큰 기반 인증 시스템은 세션 기반 시스템에 비해 구현과 관리가 다소 복잡할 수가 있다. 인증 흐름, 토큰의 저장 및 관리, 보안 취약점에 대한 고려 등이 포함된다. 이는 시스템의 아키텍처를 설계하고 유지 관리하는 데 있어 추가적인 노력을 요구한다.
4. 토큰에 대해 좀 더 알아보자
나는 토큰 방식의 인증 방식을 주로 사용했었고 앞으로도 자주 애용할 생각이기 때문에 해당 방식에 대해서 조금 더 자세히 알아보도록 하겠다.
4-1. 실생활에서의 JWT
위 토큰에 대한 설명을 통해 JWT를 곧바로 이해하기 쉽지 않을 수 있다. 실제 서비스에서는 JWT 토큰을 하나만 발급하는 것이 아니라 access token과 refresh token 두 형태로 발급하여 클라이언트 쪽으로 보내기도 한다.
그럼 이 JWT가 정확히 어떤 방식이고 왜 사용하는 것인지 실생활 예시를 통해 이해해보자.
주말이 되어 JWT 놀이공원에 놀러간 상황을 가정해보자. JWT 놀이공원의 룰은 간단하다.
코인 형태의 토큰을 제시하면 놀이기구도 탈 수 있고 화장실도 이용할 수 있으며, 매점에 입장할 수도 있다.
- 토큰은 한 사람당 하나씩만 가질 수 있다.
- 토큰에는 내 키, 나이, 이 토큰의 발행 시간 및 만료시간 등이 기재되어 있다.
- 한 토큰의 유효 시간은 30분이다.
처음에 매표소로 가서 돈을 지불하면 위의 토큰 하나와 어떤 이상한 휴대용 기계를 받는다. 이 기계는 토큰을 발급해주는 기계로 버튼을 누르면 토큰이 지이잉하고 나오게 된다.
롤러코스터가 타고 싶어진 나는 롤러코스터 줄을 기다리고 입장의 순간이 되었을 때 발급해두었던 토큰을 제시하면 직원은 해당 토큰을 앞에 비치된 기계에 다시 넣어 해당 토큰이 유효한 토큰인지, 그 토큰에 기재된 이 사람의 키가 해당 놀이기구의 제한조건을 만족하는지, 유효 시간이 아직 지나지 않았는지 등을 확인하면 나를 들여보내 주게 된다.
그런데 놀이공원의 특성상 줄을 기다리는 데 걸리는 시간은 아무리 짧게 잡아야 1시간이다. 그런데 토큰 유효 시간은 30분이니 사실상 놀이기구 하나 탈 때마다 다음 시설 이용을 위해서 내가 가진 기계로부터 토큰을 하나씩 발행해야 하는 것이다.
만약 이 토큰을 길 가다가 잃어버리게 된다면 어떻게 될까? 불법 경로를 통해 JWT 놀이공원에 들어온 사람이 해당 토큰을 주워다가 돈을 지불하지 않았음에도 놀이기구도 타고 매점가서 먹을 것도 사먹게 될 것이다. 그러나 만약 주운 토큰의 유효 시간이 지나 있다면 그 토큰은 무용지물이기 때문에 주운 사람은 그 토큰으로 아무것도 하지 못할 것이다.
그래서 이 토큰의 유효 기간을 짧게 할 수록 좋은 이유가 바로 여기서 나온다. 하지만 또 토큰의 유효 기간이 너무 짧다면 뭐만 하려하면 기계로 토큰을 발행해야 하기 때문에 귀찮을 수 있어 적절한 시간으로 설정해야 할 것이다.
이 비유에서 해당 토큰을 잃어버리게 되는 것은 전적으로 내 책임으로, 놀이공원 쪽에서는 그 토큰을 잃어버렸다고 어떻게 해 줄 수 있는 일이 없다. 그렇기 때문에 토큰을 되도록 잃어버리지 않도록 신경을 써야 한다.
더군다나 그 토큰을 잃어버렸으면 다행이지 토큰을 발급하는 기계를 잃어버렸다면 그 기계를 주운 사람이 내 행세를 하고 다니면서 이곳 저곳에서 놀이기구도 타고 매점도 이용하고 하기 때문에 나중에 나갈 때 비용 처리는 내가 사 먹고 내가 탄 것이 아니더라도 모두 내가 지불해야 할 것이다.
그렇기 때문에 토큰 자체는 주머니와 같이 비교적 잃어버리기 쉬운 곳에 보관해도 되지만 토큰 발행 기계 같은 경우 절대 잃어버리지 않도록 가방이나 목에 걸고 다니도록 해야할 것이다.
4-2. 실제 JWT와 비교했을 때는?
위의 비유에서 각각의 요소는 실제 JWT에서 사용되는 요소와 다음과 같이 매치된다.
- 매표소: 로그인
- 놀이동산 직원: 서버
- 토큰: access token
- 토큰 발행 기계: refresh token(및 JWT 인코더)
- 토큰 검증 기계: JWT 디코더
- 주머니: 브라우저 로컬스토리지
- 가방: 쿠키
이제 토큰의 작동 방식을 다시 보면 이해가 될 것이다.
로그인을 하게 되면 서버에서는 두 개의 토큰을 발행해서 클라이언트로 보내고, 클라이언트는 이 두 토큰을 쿠키 또는 로컬 스토리지에 잘 보관해 두었다가 API 요청을 할 때 access token을 요청 헤더(Authorization)에 담아 같이 보내면 서버에서 이 토큰을 디코딩하여 유효한지 검사하여 승인 혹은 거부된 요청의 결과를 응답으로 보내게 된다.
이때 앞서 놀이공원 비유와 마찬가지로 access token이나 refresh token을 탈취당하는 것은 어쩔 수가 없기 때문에 각 토큰의 유효 시간을 너무 길지 않게 설정해야 하며 쿠키의 다양한 설정을 활용하여 되도록 탈취당하지 않도록 유의해야 한다.
또한 토큰을 재발행하기 위해 토큰 발행 기계의 버튼을 매번 누를 필요없이 유효 시간을 계산하고 있다가 자동으로 토큰이 발행된다면 꽤 편리한 기능일 것이다.
이처럼 사용자 편의성을 위한 기능도 JWT를 활용하여 구현해낼 수 있으며, 일례로 access token 만료 여부를 추적하다가 만료 시에 쿠키에 담긴 refresh token과 함께 재발행 요청을 서버로 날려 새로 발급 받은 토큰을 기반으로 요청을 재개할 수 있기도 하다.(silent refresh token)
참고로 사용자의 상태를 서버에서 관리하지 않는 대신 refresh token을 DB 혹은 서버 메모리에 저장하여 관리하는 형태이기 때문에 refresh token은 세션 관리와도 유사한 느낌을 가지고 있다고 할 수 있다.
마지막으로 JWT의 secret key의 역할은 토큰에 사인을 해서 내 서버에서 발행한 토큰인지를 확인할 수 있도록 하는 키로, 위조의 위험성을 줄이기 위한 수단이라고 볼 수 있다. (JWT 인코더는 그 안의 내용이 달라지면 secret key 때문에 최종적인 JWT 값도 달라진다.)
4-3. JWT의 이해
JWT(Json Web Token)란?
- 유저를 인증하고 식별하기 위한 토큰(Token) 기반 인증에서 사용된다.
- 토큰 자체에 사용자의 권한 정보나 서비스를 사용하기 위한 정보가 포함된다.
- RESTful한 API와 같이 무상태(stateless) 환경에서 사용자 데이터를 주고 받기에 용이하다.
- 세션(Session) 방식에서는 쿠키 등을 통해 사용자를 식별하고 서버에 세션 정보를 저장했다면, JWT 방식의 인증에서는 토큰 자체를 클라이언트에 저장하고 요청 시 HTTP 헤더에 토큰을 첨부하기만 해도 쉽게 데이터 요청에 대한 응답을 받을 수 있다.
JWT 구조
JWT의 구조는 위 그림과 같이 세 부분으로 나뉘며, 각 파트를 점으로 구분하여 하나의 문자열로 표현된다.
JWT는 Header, Payload, Signature의 3부분으로 이루어지며, JSON 형태인 각 부분은 Base64 방식으로 인코딩되어 표현된다.
참고로 Base64로 인코딩되기 때문에 별도의 암호화가 이루어지지 않기 때문에 누구나 이 문자열을 갖게되면 jwt.io 와 같은 사이트에서 쉽게 디코딩하여 해석된 내용을 볼 수 있다.
(1) Header (헤더)
- 토큰의 헤더는 typ과 alg과 같은 두 가지 속성을 가진 객체로 구성된다. alg는 헤더(Header)를 암호화하는 것이 아니고, Signature를 Hashing하기 위한 알고리즘을 지정하는 것이라고 할 수 있다.
- kid: 서명 시 사용하는 키(Public/Private Key)를 식별하는 값
- typ: 토큰의 타입을 지정(ex. JWT)
- alg: 서명 암호화 알고리즘 방식을 지정하며, 서명 및 토큰 검증에 사용 (ex. HS256(HMAC SHA-256), HS512, RS256(RSASSA SHA-256), ES256(ECDSA P-256 curve SHA-256))
(2) Payload (페이로드)
- 토큰의 페이로드에는 토큰에서 사용할 정보의 조각들인 클레임(Claim)이 담겨있다. 이 클레임의 종류는 총 3가지로 나뉘며, JSON(Key/Value) 형태로 다수의 정보를 넣을 수 있다.
- 저장되는 정보에 따라 등록된 클레임(Registered Claims), 공개 클레임(Public Claims), 비공개 클레임(Priavate Claims)으로 구분된다.
- 등록된 클레임(Registered Claim)
- 등록된 클레임은 토큰 정보를 표현하기 위해 이미 정해진 종류의 데이터들로, 모두 선택적으로 작성이 가능하며 사용되는 것이 권장된다.
- 또한 JWT를 간결하게 하기 위해서는 모든 Key값의 길이가 3으로 명시하도록 해야 한다. 여러 key값 중 subject를 의미하는 ‘sub’는 unique한 값을 사용해야하며, 보통 사용자 이메일을 넣는 경우가 일반적이다.
- key
- iss: 토큰 발급자(issuer)
- sub: 토큰 제목(subject)
- aud: 토큰 대상자(audience)
- exp: 토큰 만료 시간(expiration)
- NumericDate 형식으로 되어있어야 함(ex. 1480842318)
- nbf: 토큰 활성 날짜(not before)
- 이 날이 지나기 전의 토큰은 활성화되지 않음
- iat: 토큰 발급 시간(issued at)
- 토큰 발급 이후의 경과 시간을 알 수 있음
- jti: JWT 토큰 식별자 (JWT ID)
- 중복 방지를 위해 사용하며, 일회용 토큰(Access Token) 등에 사용됨
- 공개 클레임(Public Claim)
- 공개 클레임은 사용자 정의 클레임으로, 공개용 정보를 위해 사용된다. 충돌(Collision) 방지를 위해 URI 방식의 포맷을 이용한다.
{ "<https://cdragon.tistory.com>": true }
- 비공개 클레임(Private Claim)
- 비공개 클레임은 사용자 정의 클레임으로, 서버와 클라이언트 사이에 임의로 지정한 정보를 저장한다.
{ "access_token": access }
(3) Signature(서명)
- Header에서 정의한 알고리즘 방식(alg)을 활용한다.
- Header+Payload와 서버가 갖고 있는 유일한 Key 값을 합친 것을 헤더에서 정의한 알고리즘으로 암호화한다.
- Header와 Payload는 단순히 인코딩된 값이기 때문에 제 3자가 복호화 및 조작할 수 있지만, Signature(서명)는 서버 측에서 관리하는 secret key를 사용하기 때문에 이 key가 유출되지 않는 이상 복호화할 수 없다. 때문에 이는 토큰의 위변조 여부를 확인하는데 사용되는 부분이다.
정리하자면 서명(signature)은 토큰을 인코딩하거나 유효성 검증을 할 때 사용하는 고유한 암호화 코드이다. 그래서 위에서 만든 헤더와 페이로드의 값을 각각 Base64로 인코딩하고, 이 인코딩한 값을 secret key를 이용해 Header에서 정의한 알고리즘으로 해싱을 한 다음, 이 값을 다시 Base64로 인코딩하여 생성한다.
생성된 토큰은 HTTP 통신을 할 때 AUthroization이라는 필드의 value로 사용된다. 일반적으로 value에 Bearer라는 문자열이 포함되도록 한다.
{
"Authorization": "Bearer {생성된 토큰 값}",
}
'프로젝트' 카테고리의 다른 글
mysql 커넥션 풀 오류 해결 과정 (1) | 2024.08.26 |
---|---|
[프로젝트] 데이터를 불러오는 데 너무 오래걸리는 문제 해결 및 성능 개선(feat. 데이터베이스 join) (0) | 2024.06.20 |
[Devlog] 개발 시작부터 지금까지의 여정 (1) | 2024.06.02 |
[Develog] 골치아픈 로그인 및 인증 - 2. 적용 (Next-auth@v5/Auth.js) (0) | 2024.04.24 |
[Develog] NestJS와 TypeORM (Entity 클래스 쉽게 만드는 법) (0) | 2024.03.11 |
소중한 공감 감사합니다