ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Spring Security] Stateless 서버를 위한 JWT 인증 방식
    개발 2021. 7. 28. 20:47

    이번 포스팅을 통해 해소하고 싶은 궁금증들은 다음과 같다.

    - 로그인은 어떻게 이루어지나?
    - JWT 토큰은 왜 쓰는거야?
    - 인증 서버의 구조?
    - OAuth2 인증
    - 브라우저마다 달라지는 로컬 스토리지 저장 방식
    - 자동로그인 처리 방식?


    포스팅을 본격적으로 시작하기 전에 먼저 알아두어야 할 것이 있다.

    Stateless 서버는 모든 요청에 대해 사용자의 상태를 저장하지 않는다. 

    Stateless 란 사용자의 이전 상태, 즉 세션 정보를 기록하지 않는 접속이다.

     

    서버에서는 모든 API 요청에 대해 인가된 사용자에게만 응답을 보내주어야 한다. 서버가 응답하는 데이터는 매우 민감한 개인 정보가 포함 될 수 있기 때문에, 적절한 권한을 가진 사용자의 요청에만 응답을 보내주어야 한다. 

     

    그러나 Stateless 서버는 사용자의 상태 정보를 저장하지 않기 때문에, 모든 클라이언트는 인가 된 사용자라는 사실을 증명할 수 있는 수단을 가지고 있어야 한다.

     


    인증은 어떻게 이루어 질까?

    1. Session / Cookie 이용

    (1) 클라이언트가 로그인 정보를 보낸다.

    (2) 서버에서는 세션을 생성하여 클라이언트에게 세션 ID를 발급.

    (3) 클라이언트는 로컬 저장소에 세션 ID를 저장 해 놓고 인증이 필요한 요청 때 마다 헤더에 담아서 보낸다.

    (4) 서버에서는 세션 ID 의 유효성, 사용자 정보를 확인 한 후 필요한 정보를 응답해 준다.

    ** 용어

    • 세션: 서버에서 가지고 있는 정보.
    • 쿠키: 사용자에게 발급 된 세션을 열기 위한 열쇠(Session ID). 따라서 쿠키 자체는 사용자에 대한 인증 정보를 가지고 있지 않으며, 쿠키는 HTTP request 만 가로체면 언제든지 열어볼 수 있으므로, 쿠키만 사용하는 경우는 자동로그인 설정과 같은 보안과 상관 없는 기능이다.
    • 따라서 세션에 유효기간을 설정하고, HTTPS를 사용하는 이유가 서버가 쿠키 탈취 공격에 대한 저항성을 갖게 하기 위해서이다.

    ** 장점

    • 쿠키 자체가 유의미한 값을 갖고 있지 않아서 탈취 되더라도 괜찮다.

    ** 단점

    • 세션 하이재킹에 취약하다. (A사용자의 HTTP 요청을 해커가 가로채어 HTTP 요청을 보내는 경우에 서버가 A 사용자로 오인해서 정보를 줄 수도 있다.)
    • 서버에 세션 저장소가 필요해서 추가적인 저장 공간을 필요로 한다.

     


    2. 토큰 기반 인증 방식 (JWT)

     

    우선 JWT 토큰 구조를 알아보자.

    Header

    • signature 생성을 위해 사용한 알고리즘을 명시 (ex: RS256)
    • key rolling을 지원하는 경우, 존재하는 여러개의 key 중 어떤 key를 signature를 생성 할 때 사용했는지 알기 위해 kid (key 별로 unique한 값이 정의되어 있음)를 포함하기도 한다.

    Payload

    • JSON key-value 형태로 데이터들이 포함되어있으며, 이곳에 포함된 각 필드를 클레임(Claim)이라고 부른다.
    • 예시) JWT를 유저인증을 위해 사용하는 경우 payload에 토큰을 발급받은 사용자의ID 값을 aud 필드에 포함하면 되고, 다음과 같은 절차를 따른다.
      • JWT를 이용하여 application server에 요청
      • server에서 JWT의 signature 유효성을 확인하고
      • 유효 하다면 payload에서 사용자 ID값을 읽어들여서 요청을 보낸 사람이 어떤 사용자 인지 인증 할 수 있다.

    Signature

    • 설명을 간단히 하기위해 header와 payload를 concat 한 값을 message라고 하자.
    • 해당 message가 변조되지 않았는지 검사하기 위해 서명이 필요하고, 아래 두가지 방식이 대표적이다. 더 보안성을 좋게하기 위해서 비트(bit) 수를 늘려 256 대신 384나 512를 사용하기도 한다.
    • RS256 (RSA Signature with SHA-256)
      • 비대칭키 방식
      • message에 SHA256적용 후 private key 사용해서 암호화
      • JWT를 발급한 서버뿐만아니라, JWT를 받아서 사용하는 어떤 주체라도 signature 유효성 검증이 가능
      • 일반적으로 public key는 JWT를 발급한 서버에서 JWK (JSON Web Key)에 정의된 방식을 통해 공개적으로 제공
    • HS256 (HMAC with SHA-256)
      • 대칭키 방식
      • message에 SHA256 적용 후 대칭키 사용해서 암호화
      • JWT를 발급한 서버 또는 해당 대칭키를 미리 공유해서 알고있는 주체들만 signature 유효성 검증이 가능하다.

    출처 https://www.letmecompile.com/api-auth-jwt-jwk-explained/


    AccessToken + RefreshToken 을 이용한 OAuth2 인증 방식

    (1) 클라이언트가 로그인 정보를 보낸다

     

    (2) 서버에서는 계정 정보를 읽어서 accessToken, refreshToken 을 발급한다.

    -1. 사용자의 고유 ID 값을 함께 부여하여 Payload 에 넣는다.

    -2. 암호화 할 Secret Key 를 이용하여 Access Token 과 refresh Token 을 발급, 암호화 하여 클라이언트에게 보낸다.

     

    (3) 클라이언트는 로컬 저장소에 accessToken, refreshToken을 저장 해놓고 인증이 필요한 요청 때 마다 accessToken 을 헤더에 담아서 보낸다.

     

    (4) 서버에서는 accessToken 을 검증한다.

    -1. 유효한 토큰 인 경우 정상 Payload 에 담긴 사용자 정보로 응답한다.

    -2. 만료가 되었을 경우 만료 신호를 보낸다.

     

    (5) AccessToken 이 만료되었다면 클라이언트는 RefreshToken과 함께 AccessToken 발급 요청을 보낸다.

    -1. Refresh Token 의 유효성을 검증한다.

    -2. 만료되지 않았다면 새로운 Access Token 을 발급한다.

    -3. Refresh Token 도 만료 되었다면 사용자는 다시 로그인해야 한다.

     

    ** 장점

    • 기기나 AccessToken 이 탈취 되더라도 빨리 만료 된다.
    • RefreshToken 은 토큰 만료를 강제로 시킬 수 있다.

     


    여기서 JWT토큰을 별도의 저장소에 저장하지 않는다. JWT 토큰은 그 자체로 사용자의 유효성을 검증한다.

    그런데 만약에 특정 비즈니스 로직에 따라, 토큰 유효기간을 강제로 만료시키고 싶다면 어떻게 해야할까?

     

    토큰은 이미 클라이언트의 로컬 스토리지에 저장이 되어있고, 유효기간을 강제로 만료 시킨 토큰을 응답하더라도 클라이언트가 이전에 사용하던 토큰을 보내면 서버로서는 알 길이 없다. 그렇다면 토큰을 서버 어딘가에 저장해 놓고 토큰 정보를 기존 토큰과 비교하여 보안을 유지해야 하는건 아닐까?

     

    예를 들어, 디코딩 된 토큰의 Payload가 이런 구조라고 해보자.

    암호화된 토큰: eyJ0eXAiOiJKV1Q2xlcyI6WyJV...
    복호화된 토큰:
    {
      "sub": "101",
      "roles": [
        "USER"
      ],
      "userType": "STUDENT",
      "iat": 1618984448,
      "exp": 1618986248
    }

    이때, "sub" 필드는 사용자의 고유 id 값이다.
    우리 서버는 tokenString 자체를 검증하는 것이 아니라, 이를 복호화 한 후에 "sub" 필드로
    사용자를 인증한다.
    따라서 "sub" 필드, 즉 사용자 고유번호가 같지만, 해커가 임의로 만들어 낸 accessToken 으로 요청이
    들어올 경우 서버는 정상 사용자라고 인식하고 중요한 정보를 넘겨줄 수도 있다.

    왜 토큰을 DB 등에 저장하지 않는 걸까?

     

     

    서버는 JWT토큰을 별도의 저장소에 저장하지 않는 이유는, JWT토큰을 이용해 Stateless 한 RESTServer 를 구축하기 위해서이다

    1. 토큰을 db나 서버 캐시에 저장을 하게 되면, 매 RefreshToken 을 갱신해 달라는 요청 마다 DB를 왔다 갔다 해서 데이터를 검증해야 하는데, 이게 상당한 부하이다.
      사용자가 많고, RefreshToken 을 갱신해달라는 트래픽이 많이 발생하면 그 부하를 감당 하기가 매우 힘들 수 있다.
    2. 그럼에도 불구하고 DB에 저장하는 특수한 경우가 있다. 한명의 사용자가 여러개의 분산 서버에 Request 를 보내는 경우에, 한명의 사용자의 인증 정보를 여러 대의 서버가 공유해야 하기 때문이다. 이때 공통의 인증 정보를 저장하는 공간이 별도로 필요하다. 매우 특수한 경우이다.

     

Designed by Tistory.