🎉 berenickt 블로그에 온 걸 환영합니다. 🎉
Back
로그인
로그인의 역사(feat. JWT)

1. 로그인의 역사 : 쿠키/세션

1.1 메모리 세션(session)을 이용한 로그인

브라우저에서 특정 email과 password를 가지고 로그인을 하게되면 백엔드로 loginAPI 요청이 날라가게 되고, 백엔드에서는 해당 유저가 있는지 DB에 확인 후 있으면 session에 저장해두게 됩니다. 그 후 특정한 id를 부여해서 브라우저로 보내줍니다. 이를 인증(Authentication)이라고 합니다. 이렇게 보내진 id는 해당 유저가 뭔가를 요청할 때, 본인이 누군지 식별할 수 있도록 id를 함께 넣어서 보내줍니다. 그렇다면 브라우저는 받아온 세션 ID를 저장해야 합니다.

💡 브라우저에는 크게 4가지 저장공간이 존재합니다.

  • 변수
  • localStorage
  • sessionStorage
  • Cookie

그리고 브라우저에서는 다음 API(e.g. fetchUser)를 요청할 때, 누구의 정보를 요청할 것인지 알아야 하기 때문에, 세션 ID를 보냅니다. 그리고 메모리세션에 세션ID가 있는지 확인(인가, Authorization)합니다. 그런 다음 메모리제이션에 세션ID가 있다면, DB에서 해당 요청 정보를 가지고 옵니다.

💡 인증 vs 인가

  • 인증(Authentication)
    • 로그인을 해서, ID, PW를 확인하고, 그걸 메모리세션에 저장하고, 세션 ID를 만들어 내는
    • 반드시 DB에 접근해야 함
    • 즉, 백엔드로 부터 토큰을 받아오는 것
  • 인가(Authorization)
    • 로그인이 된 상태에서, 로그인 정보가 필요한 API를 요청할 때, (e.g. fetchUser, 결제 등)
    • 내가 로그인한 ID가 맞는지 메모리세션에 요청을 보낸 세션ID가 있는지 확인하는 것
    • 즉, 토큰을 확인한 다음 API 요청

이렇게 유저의 정보(Id)를 백엔드 서버로 받다보니 한번에 여러명의 정보를 받기엔 한계가 있었습니다. 이를 보완하기 위해서 백엔드 컴퓨터를 scale-up을 해주었습니다.

💡 scale-up : 컴퓨터의 성능(cpu,memory 등)을 올려주는 것

  • 디스크 : 엑셀같은 파일 저장
  • 메모리 : 변수 저장

1.2 컴퓨터 여러 대로 접속을 분산

이렇게 백엔드 컴퓨터의 성능을 올려주었음에도 불구하고, 더 많은 유저의 접속이 동시 다발적으로 일어나면, 여전히 서버의 부하를 초래했습니다. 그래서 나온 방법으로 백엔드 컴퓨터를 복사하는 방법이었습니다. 이 방법은 유저의 정보가 담기는 백엔드 컴퓨터를 복사해 여러대의 컴퓨터로 백엔드 서버의 부하를 분산해주었습니다.

그렇다면 트래픽을 분산하는 알고리즘에는 2가지가 있습니다.

  • least connection 알고리즘 : 가장 적은 컴퓨터에 접속을 몰아주는 것
  • Round Robin 알고리즘 : 컴퓨터들의 접속순서를 동그랗게 만들고 1번씩 원 방향으로 돌리는 것
    • cf. 위 2개는 대표적인 알고리즘으로 더 많은 알고리즘이 존재하긴 합니다.

하지만 이 방법 또한 문제가 있습니다. 백엔드 컴퓨터를 복사할때는 세션까지 scale out이 안되기 때문에, 기존의 로그인 정보를 가지고 있던 백엔드 컴퓨터가 아니면, 로그인 정보가 없습니다. 또한 백엔드 컴퓨터를 복사해도 DB는 하나이기 때문에 결국 DB로 부하가 몰리는 병목현상이 일어납니다. 이 문제를 해결하기 위해 세번째 로그인 방법이 사용됩니다.

💡 scale-out : 똑같은 성능(cpu,memory 등)의 또 다른 컴퓨터를 추가하는 것

💡 stateful과 stateless

그러나 메모리 세션은 scale-out을 할 수 없어서, 해당 세션 ID를 가진 컴퓨터에 몰리는 현상때문에 한계가 있습니다. 이렇게 컴퓨터가 특정 세션ID를 가진 상태stateful이라고 합니다.

반대로 세션ID 상태를 없애줘야 scale-out을 해서 특정 컴퓨터에 로그인 요청 API가 몰리는 것을 막을 수 있습니다. 이렇게 메모리세션에 세션정보가 없는 상태stateless라고 합니다.

즉, stateless : 백엔드 컴퓨터에 상태를 가지고 있지 않음

그렇다면 stateful 상태를 stateless로 바꿔줘야 합니다.

💡 병목현상(bottle neck) : 콜라병 입구같이 병의 입구처럼 가능 길목이 좁아 정체되는 현상


1.3 Redis(임시 DB)에 로그인 정보 저장

이 방법은 위의 session을 scale-out해오지 못하는 문제점을 보완한 방법이자, DB에 부하가 몰리는 병목현상을 해소한 방법으로, 현재 많이 쓰이고 있는 방법입니다. session을 복사해오지 못하자, 사람들은 로그인 정보를 DB에 저장하기 시작했습니다.

하지만 결국 이것도 백엔드 서버의 부하가 DB로 옮겨진 것 이기 때문에 DB의 부하를 초래합니다. 보완을 위해 “DB를 복사하면 안되나?” 라고 생각하셨을 수 있지만, DB를 복사하는 방법은 비용문제가 발생하기 때문에 비효율적입니다. 따라서 위의 문제점은 데이터를 쪼개면서 해결하게 됩니다.

💡 데이터베이스를 쪼개는데는 2가지의 방법이 있습니다

  • 수직으로 쪼개는 수직파티셔닝
  • 수평으로 쪼개는 수평파티셔닝(=샤딩, Sharding)
    • cf. 쪼개는 것을 파티셔닝(partitioning)이라고 합니다.
    • cf. 샤딩(Sharding) : 대규모 데이터를 처리하기 위해 데이터를 여러 조각으로 나눠 저장하는 기술
      • ‘조각(Shard)’이라는 말에서 유래

그러나 여기에도 문제점은 있습니다. DB는 컴퓨터를 껏다 켜도 날아가지 않기 때문에 데이터들이 disk에 저장됩니다. 따라서 안전하지만 느립니다. 즉, 로그인 관련 API를 요청할 떄마다, DB에 왔다갔다하면 느립니다. 이렇게 disk에 저장된 데이터를 추출해 오는 현상DB를 긁는다고(scrapping) 표현합니다.

💡 참고

  • 디스크 : 영구적, 느림
  • 메모리 : 영구적X, 빠름
  • cf. 메모리에 저장하는 데이터베이스 종류 : memchached, redis

이를 해결하기위한 방법으로 Redis라는 메모리에 저장하는 임시 데이터베이스에 저장해둡니다. redis메모리에 저장하기 때문에 디스크 보다 빠릅니다. 따라서 위의 문제점을 해결합니다.

이렇게 저장된 특정 ID(토큰)을 다시 브라우저로 돌려주게 됩니다. 돌려받은 토큰브라우저 저장공간에 토큰을 저장해두고, 어떤 행동을 할때 토큰을 같이 보내주어 사용자가 누구인지 식별합니다.


2. 로그인의 역사 : JWT 로그인

똑똑한 사람들은 “로그인 정보를 굳이 서버나 DB에 저장해야 할까?”라고 생각을 했습니다. 그렇게 탄생한 것이 JWT 토큰입니다.

2.1 암호화, 복호화

  • 암호화는 1:1이나 그런 방식으로 특정 규칙(암호화 알고리즘)의 특수문자, 문자 등으로 변환한 문자열
  • 복호화는 암호화를 다시 규칙에 맞게 해석해서 원래 문장으로 바꾸는 것

그렇다면 객체를 암호화한다면 어떨까요? 바로 암호화된 문장을 세션ID로 사용하는 겁니다.


2.2 JWT 토큰 (JSON Web Token)

JWT 토큰은 유저 정보를 담은 객체를 문자열로 만들어 암호화한 후 암호화된 키(accessToken)를 브라우저에 줍니다. 받아온 암호화된 키는 브라우저 저장소에 저장해두었다가, 유저의 정보가 필요한 API를 사용할 때 보내주게 되면, 해당 키를 백엔드에서 복호화해서 사용자를 식별한 후 접근이 가능하도록 합니다.

JWT 토큰에는 해당 토큰이 발급받아온 서버에서 정상적으로 발급을 받았다는 증명을하는 signature(서명)를 가지고 있습니다. 따라서 사용자의 정보를 DB를 열어보지 않고도 식별할 수 있게 되었습니다.

따라서 현재 가장 많이 사용되는 로그인 방법은 크게 2가지입니다.

  • Redis(메모리 기반 DB)에 저장하는 법
  • JWT으로 로그인
    • JWT(JSON Web Toekn)
    • 토큰(Token)문장에서 뭔가 잘라낸 단위를 의미합니다.
  • cf. 2023년 기준 현업에서 많이 사용되는 방법은 Redis이지만, 더 최신기술은 JWT입니다.
    • 아직까지는 보안 상 JWT와 Redis를 같이 사용하는데,
    • 암호화 알고리즘이 고도화되면, 나중에는 궁극적으로 JWT만 사용하는 날들이 올 수도 있습니다.

3. 암호화와 JWT 토큰

3.1 단방향 암호화(해싱) 양방향 암호화

우리가 로그인을하고, 로그인 정보를 fetch해왔을 때 브라우저에 비밀번호를 fetch 할 수 없어야 합니다. 즉, 비밀번호를 알아내는게 불가능해야 합니다.

DB에 있는 비밀번호를 알아낼 수 있게되면, 해커가 DB를 해킹해왔을때 유저의 비밀번호를 알아낼 수 있게 되면 민감한 정보에 접근이 가능하게 됩니다. 또한 유저가 해당 사이트에서 사용하는 비밀번호를 다른 사이트에서도 사용하는 경우엔 문제가 더 심각해지게 됩니다.

따라서 비밀번호나 계좌번호같은 민감한 정보는 백엔드에 저장할때 그대로 저장하지 않습니다. 그럼 어떻게 저장하는지 알아보도록 하겠습니다. 어떻게 저장하는지 알아보려면 우리는 2가지 타입의 암호화를 알아야 할 필요가 있습니다.

3.2 양방향 암호화

양방향 암호화는 JWT같은 복호화가 되는 암호화를 말합니다. 즉, 암호화와 복호화 모두할 수 있는 암호화 입니다. (cf. 암호화 알고리즘이 일대일 방식)


3.3 단방향 암호화(hash)

단방향 암호화암호화는 되지만 복호화는 안되는 것을 의미합니다. (cf. 암호화 알고리즘이 다대일 방식)

e.g. 앞에서 부터 2개씩 끊어서, 10으로 나눈 나머지를 적어놓은 단방향 암호화 알고리즘이 있다고 하면,

275719 —암호화—> 779

  • 27을 10으로 나누면 7
  • 57을 10으로 나누면 7
  • 19를 10으로 나누면 9

반대로 복호화를 하려고 하면, 10으로 나눴을 때 나머지가 7이 되는 숫자는 27,37,47 등등 많기 때문에, 원래 정보가 뭔지 모르게 만드는 것 입니다. 이를 정보들을 뭉개다(hash)라고 표현합니다. 이를 다대일 이라고 하는데, 이는 레인보우 테이블(Rainbow Table)무작정 다 대입해서 복호화하는 경우도 있습니다. 레인보우 테이블로 무차별 대입하는 것무차별 대입공격(brute force, 무차별 공격)이라고 부릅니다.

💡 Rainbow Table

레인보우 테이블은 해시 함수(MD5, SHA-1, SHA-2 등)을 사용하여 만들어낼 수 있는 값들을 왕창 저장한 표

그래서 또 이 해시 알고리즘을 알아내기 힘들게 하기 위해 소금(salt, 임의의 문자열)을 추가합니다. 이렇게 하면 레인보우 테이블의 정보가 엄청나게 많아지겠죠? 현재는 보안 방법이 여기까지 나와있지만, 이 방식 또한 뚫릴 것입니다. 그래서 보안과 해킹은 창과 방패같은 관계입니다.

💡 해시 기반 알고리즘

  • MD5 (Message-Digest algorithm 5)
    • 임의의 길이의 값을 입력받아서 128비트 길이의 해시값을 출력하는 Hash 알고리즘이다. 1991년 설계
    • 단방향 암호화
    • MD5는 속도가 너무 빠르기때문에 salt같은 수단을 붙이더라도 무차별 대입이나 사전 공격에 너무 취약
  • SHA (Secure Hash Algorithm ) 0과 SHA-1
    • 1993년부터 미국 NSA가 제작하고 미국 국립표준기술연구소(NIST)에서 표준으로 채택한 암호학적 해시 함수
    • 1993년 SHA의 표준으로 정의되어 발표되었으며 160 비트의 해시값을 사용한다.
    • 그러나 2년 만에 바로 취약점이 발견되어 이를 개선한 SHA-1이 새로 발표되었고, 널리 사용되었다.
    • SHA-1 역시 160 비트 해시값을 사용한다. 처음 발표된 SHA는 편의상 SHA-0로 표기하여 구분
    • e.g. SHA-1을 활용한 프로그램으로는 비트토렌트, Git의 파일이나 커밋 등의 오브젝트 정보
  • SHA-2
    • SHA-1 역시 해시 충돌을 이용한 위험성이 발견되어 차세대 버전
    • cf. 해시충돌 : 해시함수가 서로 다른 2개의 입력값에 대해 동일한 출력값을 내는 상황
    • 8개의 32비트 상태를 업데이트하는 압축 함수를 기반으로 하고,
    • 해시 길이에 따라서 224, 256, 384, 512 비트를 선택해서 사용
    • 당연히 해시 길이가 길 수록 더 안전하다.
    • 편의상 해시 길이에 따라 SHA-224, SHA-256, SHA-384, SHA-512 등으로 부른다.
    • 일반적으로 SHA-256을 사용
    • 해시 길이가 길어졌기에 그만큼 안전하긴 하지만,
    • 알고리즘의 기본 동작이 SHA-1과 큰 차이가 없기 때문에 완전히 안전하다고 장담하긴 어렵다.
    • 해시 함수 특성상 기본 동작(round)가 많아질수록 안정성이 높아져
    • 현재까지 SHA-2에 대해 알려진 충돌 등은 없지만,
    • 만약을 위해 SHA-1하고 기본적인 원리 자체가 아예 다른 SHA-3이 개발
    • e.g. SHA-256 활용한 프로그램으로는 대한민국 인터넷뱅킹, 비트코인, Window 업데이트 등
  • SHA-3
    • SHA-2 도 위험성 문제가 제기되자,
    • 충돌 가능성을 피하기 위해서 SHA-1과 2와 전혀 다른 알고리즘인 SHA-3의 개발
    • 2012년 10월 2일자로 Keccak이 SHA-3로 확정되었고, 더욱 더 안정성이 높은 방식으로 설계함
    • e.g. 이더리움

이 부분을 보완하기위해서 조금더 어려운 알고리즘을 추가하기도 합니다. 따라서 민감한 정보를 저장할때는 해킹을 당해도 알아볼 수 없도록 단방향 암호화를 사용하여 저장하게 됩니다.

💡 토큰 관점에서 authentication과 authorization

  • authentication(인증) : 로그인을해서 토큰을 받아오는 과정
  • authorization(인가) : 리소스에 접근할 수 있도록 토큰을 확인하는 과정

3.4 JWT의 이상한 점 : 내용을 확인 가능

그런데, JWT에는 한가지 이상한점이 있습니다. 우선 다음 사이트를 브라우저에 띄워주세요.( jwt.io ) 그럼 Encoded부분과 decoded 부분을 볼 수 있습니다. 여기서 Encoded 부분에 accessToken을 넣어보세요.

우리가 받아온 토큰을 넣어보니 decoded부분에 토큰에대한 정보가 모두 보이고 있습니다. 이처럼 누군가 우리의 토큰을 탈취해 해당 사이트에 넣어보면 토큰의 정보를 알 수 있습니다. 즉, JWT토큰은 암호화는 했지만 누구든 열어볼 수 있다는 것 입니다. 따라서 중요한 데이터는 JWT 토큰에 저장해서는 안됩니다.

💡 JWT 토큰의 구성

  1. header : 토큰의 타입, 암호화시 사용한 알고리즘 정보
  2. payload : 토큰 발행정보(누구인지, 언제 발행되었는지, 언제 만료될 것 인지)
  3. verify signature : 토큰의 비밀번호

3.5 그럼 JWT는 사용하면 안됨?

누구든지 복호화가 가능하기 때문에 보안을 위해 토큰의 만료시간을 짧게 주었습니다. (cf. 만료시간은 보통 30분 ~ 2시간) 그러나 복호화된 정보에는 토큰의 만료시간이 명시되어있었습니다. 즉, 조작을 할수도 있다는 것 이지요.

하지만 이런 조작을 미연에 방지하기 위해 JWT는 signature(토큰의 비밀번호)를 사용합니다. 토큰의 내용을 조작하기 위해선 토큰의 비밀번호를 알아야 한다는 것 입니다. 해당 비밀번호는 백엔드에서 생성하며, 알 수 없습니다.


3.6 브라우저 Application 저장소

  • Local Storage : 로컬 컴퓨터에만 사용
  • Session Storage : 로컬 컴퓨터에만 사용
  • Cookies : 백엔드랑 데이터 주고받기 가능,
    • Network Header에 Cookie이 붙어서 요청(request)이 들어감