웹에서 인증이 유지되는 방법들

date
Aug 13, 2022
thumbnail
slug
web-auth
author
status
Published
tags
Web
summary
상태를 갖지 않는 HTTP 프로토콜에서 인증이 유지되는 방법 (쿠키/세션, JWT)
type
Post
updatedAt
Jan 23, 2023 03:13 PM

1. HTTP는 비연결성, 무상태성?

HTTP는 인터넷 상에서 데이터를 주고 받기 위한 서버/클라이언트 모델을 따르는 프로토콜이다.
HTTP는 비연결성, 무상태성이라는 특징을 가지고 있다.
  • 비연결성 : 클라이언트가 요청을 하고, 서버가 해당 요청에 적합한 응답를 하게 되면 바로 연결을 끊는 성질
  • 무상태성 : 서버가 클라이언트의 이전 상태를 보존하지 않는다. 상태를 보관하지 않으므로 클라이언트의 요청에 항상 동일하게 동작한다. 모든 HTTP 요청에 대해 완전히 독립적이라는 의미
서버가 다수의 클라이언트와 연결을 계속해서 유지한다면, 이에 따른 자원 낭비가 심해진다. 따라서 비연결성 및 무상태성의 특징을 통해 불필요한 자원 낭비를 줄일 수 있다.
만약, 수 만명이 이용하는 웹 서버가 모든 클라이언트와 계속해서 connection을 유지하고 있다면 감당이 불가능 할 것이다.

근데 HTTP 헤더에 Keep-alive가 있던데, 이건 뭔가요?

HTTP/1.1에서 사용 가능한 여러 HTTP 요청/응답에 대해 단일 TCP 연결을 사용하는 옵션이다.
비연결성의 문제는 연결이 끊어짐에 따라 새로 연결될 때 TCP/IP 연결을 새로 맺어야 하므로 3-way handshake에 따른 시간이 추가된다. 하지만 이 옵션을 사용하면, 요청에 따라 연결이 된 이후 일정 시간 연결을 유지하므로 다시 TCP 연결을 맺기 위한 3-way handshake을 매 요청마다 할 필요가 없어진다. 즉, 이전 TCP 커넥션을 재활용하여 비연결성에 따른 비효율성 문제를 해결한다. 보통 HTML 하나를 요청하면, 그 안에서 JS, CSS, 이미지 등 수 많은 자원을 또 요청해야 한다. 그 상황에서 매 자원을 요청할 때 이전 TCP연결을 사용하는 것이 합리적이다. 너무 긴 시간을 설정하면 부하가 발생하므로 서버에서 적당히 짧은 시간으로 설정한다. (보통 동시 접속이 너무 많다면 off한다고 한다.)
HTTP/2.0은 이 옵션이 존재하지 않고, 기본적으로 TCP연결을 재활용한다.

HTTP 특성에 따른 문제점

기본적으로 서버가 클라이언트를 식별할 수 없다는 것이 가장 큰 문제이다. 어떠한 처리를 해주지 않으면, 로그인을 하더라도 다음 요청에서는 해당 클라이언트를 기억하지 못해, 다시 또 로그인을 해야하는 문제가 발생할 것이다. (브라우저에서 새로고침을 누를 때마다 로그인을 해야하는 상황을 생각해보면…) 로그인 상태를 유지한다는 것은 인증상태를 유지한다는 것인데, HTTP의 기본적인 특성을 생각해보면 굉장히 난감한 상황이다. 우리가 사용하고 있는 웹 사이트들의 경우, 한 번 로그인 하면 다시 로그인할 필요 없이 여러 페이지를 돌아다니며 다양한 기능들을 이용한다. 이게 어떻게 가능할까?

2. 쿠키와 세션

쿠키

쿠키는 웹 브라우저에 저장되는 데이터이다. 쿠키는 key-value 형태로 웹 브라우저의 쿠키 저장소에 저장된다.
서버는 HTTP 응답 메시지 헤더에 Set-Cookie를 설정하여 응답과 함께 전송하고, 이 정보는 브라우저가 받아서 저장하게 된다.
네이버에 로그인 요청을 한 후 응답으로 온 HTTP 메시지
네이버에 로그인 요청을 한 후 응답으로 온 HTTP 메시지
네이버 로그인 요청 후, HTTP Response 헤더브라우저에 저장된 쿠키들서버로부터 쿠키를 전달받아 저장한 웹 브라우저는 이후 웹 서버에 요청을 보낼 때 HTTP Request 헤더에 cookie를 설정하여 함께 전송한다.
브라우저 → 서버 요청 시 헤더에 설정되는 cookie쿠키는 네트워크를 통해 전달되기 때문에 중간에 쿠키를 탈취할 수 있다는 취약점이 있다. 따라서 매 요청마다 쿠키에 ID, PWD같은 민감한 정보를 실어서 보내는 것은 많이 위험해보인다. 이처럼 쿠키만으로 로그인을 구현하는 것은 한계가 있고 이를 보완하기위해 세션을 함께 사용한다.

세션

세션은 쿠키를 기반하고 있지만, 사용자 정보 파일을 브라우저에 저장하는 쿠키와 달리 세션은 서버 측에서 관리한다.
웹 브라우저는 각각 별도의 세션을 갖게 된다. 각 세션을 구분하기 위해 고유 ID 를 할당하고, 서버는 각 브라우저에 세션 ID 를 전송한다. (즉 1 클라이언트, 1 세션ID)
  • 웹 서버와 웹 브라우저가 세션 ID 를 주고받기 위해서 사용하는 것이 쿠키이다.
  • 쿠키에 민감한 개인정보를 담는 게 아니라, 세션 ID를 담는다.
물론 이 상황에서도 쿠키가 탈취당해서 SESSION ID가 유출되면(Session hijacking) 해커가 로그인한 사용자처럼 행동이 가능하다는 한계가 있다. 이를 위해 아래와 같은 해결책을 둔다.
  • 로그인 이후에도 중요한 서비스 이용시에는 사용자 재인증을 통하여 인가된 사용자만이 해당 서비스를 이용할 수 있도록 통제한다.
  • Session Timeout기능과 Session ID 재생성 기능을 사용한다.
쿠키-세션 인증 흐름
쿠키-세션 인증 흐름

3. JWT (JSON Web Token)

인증에 필요한 정보들을 암호화시킨 토큰
쿠키/세션 방식과 유사하게 JWT 토큰(Access Token)을 HTTP 헤더에 실어 서버가 클라이언트를 식별한다.
HTTP 요청 메시지의 authorization 헤더
HTTP 요청 메시지의 authorization 헤더
  • bearer는 JWT와 OAuth를 타나내는 인증 타입이다. (임의의 문자열을 나타내는 의미긴하지만…) 일반적으로 요청의 Authorization필드에 담아서 보내지게 된다.

JWT 구조

인코딩 전 JWT 구조
인코딩 전 JWT 구조
  • Header : 암호화할 해싱 알고리즘 및 토큰의 타입을 지정
  • Payload : 토큰에 담을 정보. 주로 클라이언트의 고유 ID 값 및 유효 기간 등이 포함되는 영역
  • Signature : Header와 Payload 정보들을 합친 후, 서버의 SECRET KEY로 암호화
Payload에 민감한 개인 정보는 담지 않는다. BASE64로 인코딩되었으므로, 탈취당하면 디코딩하여 정보를 볼 수 있기 때문이다. 따라서 인증이 필요한 최소한의 정보(사용자ID, 권한의 범위, 토큰의 발급일자와 만료일자 등)를 담는다.
인코딩 된 JWT 문자열
인코딩 된 JWT 문자열
JWT를 이용한 인증 흐름
JWT를 이용한 인증 흐름
  1. 사용자가 로그인을 한다.
  1. 서버에서는 계정정보를 읽어 사용자를 확인 후, Payload에 정보를 담는다.암호화할 SECRET KEY를 이용해 Signature를 생성하고, Access Token을 발급한다.
  1. 사용자는 Access Token을 받아 저장한 후, 인증이 필요한 요청마다 토큰을 헤더에 실어 보낸다.서버에서는 해당 토큰의 조작 여부, 유효기간 등을 확인한다.
  1. 검증이 완료된다면, Payload를 디코딩하여 사용자의 ID에 맞는 데이터를 가져옵니다.

JWT의 장점

인코딩된 Header와 Payload를 가지고 서버의 SECRET KEY를 통해 Signature를 생성하므로 데이터 위변조를 막을 수 있다. 사용자 인증에 필요한 모든 정보를 토큰 자체에 담고 있기 때문에 별도의 인증 저장소가 필요없다. 모바일 어플리케이션 환경에서도 잘 동작한다. 이전에는 Client로 웹 브라우저만 존재했지만, 현재는 다양한 모바일 앱들도 존재한다. 웹과 앱의 쿠키 처리 방식이 다르므로, 쿠키/세션 방식에서는 이를 처리하기 위한 상이한 로직이 필요해진다.

JWT의 치명적인 단점

JWT를 발급해서 전달하고, 그 이후의 상황은 클라이언트가 관리하는 형태로 체계가 잡혀있기 때문에 토큰에 대한 어떠한 정보도 갖고 있지 않다. 즉 토큰을 임의로 삭제하는 것이 불가능하므로 탈취당하면 대처가 불가능하다. 이를 해결하기 위해서 토큰의 Expiration Time을 최대한 짧게 가져 가면 되지만, 그렇다면 사용자가 인증을 주기적으로 해줘야하는 불편함이 생긴다.

Refresh Token의 도입

Access Token의 만료기간이 짧다는 것은, 사용자 입장에서는 30분, 1시간만 유효하기 때문에 1시간 뒤에는 ID, PW를 또 입력해야 하는 불편한 상황이 발생한다는 것이다. 이를 해결하기 위해서 가장 많이 사용하는 방법으로 JWT를 처음 발급할 때 Access Token과 함께 Refresh Token이라는 토큰을 함께 발급하여 짧은 만료시간을 보완할 수 있다. 짧은 시간이 아닌 비교적 긴 시간 (7일, 30일 등)의 만료시간을 가진 Refresh Token은 말 그대로 Access Token을 Refresh 해주는 것을 보장하는 토큰이다. 만약 클라이언트가 Access Token이 만료됨을 본인이 인지하거나, 서버로부터 만료됨을 확인받았다면 Refresh Token을 서버에게 보내서 새로운 Access Token을 발급하도록 요청한다.
access token과 refresh token을 이용한 인증 과정
Access Token과 Refresh Token을 이용한 인증 흐름
Access Token과 Refresh Token을 이용한 인증 흐름
  1. 클라이언트가 ID, PW로 서버에게 인증을 요청하고 서버는 이를 확인하여 Access Token과 Refresh Token을 발급한다.
  1. 클라이언트는 이를 받아 Refresh Token를 본인이 잘 저장하고 Access Token을 가지고 서버에 자유롭게 요청한다.
  1. 요청을 하던 도중 Access Token이 만료되어 더 이상 사용할 수 없다는 오류를 서버로부터 전달받는다.
  1. 클라이언트는 본인이 사용한 Access Token이 만료되었다는 사실을 인지하고 본인이 가지고 있던 Refresh Token를 서버로 전달하여 새로운 Access Token의 발급을 요청한다.
  1. 서버는 Refresh Token을 받아 서버의 Refresh Token Storage에 해당 토큰이 있는지 확인하고, 있다면 Access Token을 생성하여 전달한다.
  1. 클라이언트는 새로운 Access Token을 받아서 다시 요청한다.

근데 결국, Refresh Token을 위한 저장소가 필요하다면 ‘쿠키/세션’ 방식의 단점을 다시 가져오는 것이 아닌가?

Refresh Token을 검증하기 위해선 다시 서버측에 별도의 저장소가 필요한 것이므로, JWT의 장점(I/O 작업이 필요 없는 빠른 인증 처리)이 사라지고, 쿠키/세션 방식에서 나타난 단점(저장 및 요청마다 검증은 위한 I/O작업)이 다시 나타나게 된다. 즉 쿠키/세션 방식의 I/O 작업에 대한 단점이 다시 나타나므로 어차피 쿠키/세션 방식이랑 큰 차이가 없지 않나? 라는 의문이 들었다.
하지만 쿠키/세션 방식은 항상 인증 요청을 할 때마다 클라이언트로부터 넘어온 세션 ID를 세션 저장소에 있는 세션 ID와 비교한다. 즉 매 요청마다 저장소에 접근해야 한다. 하지만 Refresh Token은 Refresh가 필요한 그 순간만 ,즉 요청을 했는데 access token이 만료되었을 때만 I/O 작업이 발생한다. (access token이 만료가 되지 않았다면, 검증 과정 후 payload부분만 확인하면 되기 때문이다.) 또한 토큰 방식은 모바일 환경에서도 잘 동작한다.

참고