JWT

GUK·2023년 12월 15일
0

기타

목록 보기
2/2
post-thumbnail

들어가며

JSON 웹 토큰이란?
JWT(JSON Web Token)는 웹 표준으로 정의된 토큰 기반의 인증 및 권한 부여 방식 중 하나입니다.
주로 사용자 인증에 활용되며, 특히 웹 및 모바일 애플리케이션에서 널리 사용됩니다.


본론으로 들어가기 전 인증(Authentication)과 인가(Authorization)에 대해 먼저 알아보겠습니다.

인증(Authentication)

  • 어떤 사이트에 가입한 사용자임을 아이디와 비밀번호 등을 통해서 인증을 받는 것
  • 사용자가 자신의 계정을 사용하려 할 때 로그인 시키는 것

즉, 어떤 사이트나 서비스에 등록한 사용자가 자신의 신원을 확인하고 서비스에 접근하는 것을 의미합니다. 이를 위해 사용자는 아이디와 패스워드 등의 자격 증명을 제공하게 되고, 시스템은 이를 확인해 사용자의 신원을 검증합니다.

인가(Authorization)

  • 인증 받은 사용자가 서비스를 이용할 때 서버에서 인증 받은 것을 알아보고 허가해주는 것

즉, 인가는 인증된 사용자에게 특정 리소스 또는 서비스에 대한 접근 권한을 부여하는 과정을 의미합니다. 아는 사용자가 로그인되어 있는 상태에서 자신의 계정으로만 할 수 있는 특정 활동이나 리소스에 접근할 때 발생합니다.


인증이 사용자의 신원을 확인하고 시스템에 로그인하는 것이라면, 인가는 로그인된 사용자가 어떤 특정한 권한을 가지고 있는지 확인하고, 해당 권한에 따라 특정 리소스에 접근을 허용 또는 거부하는 것입니다.

JWT는 인증보단 인가에 연관된 기술


JWT는 JSON 형식으로 인코딩된 토큰으로서, 세 부분으로 구성됩니다.

  1. Header(헤더) :
  • 토큰의 타입과 해싱 알고리즘 등의 메타 데이터를 포함합니다. 일반적으로는 {"alg": "HS256", "typ": "JWT"}와 같은 형태를 가집니다.

  1. Payload(페이로드) :
  • 클레임(Claim)이라 불린느 정보를 포함합니다. 클레임은 3가지 타입으로 나뉩니다.
    • Registered Claims : 토큰에 대한 정보를 담고 있는 일반적인 클레임들입니다.
    • Public Claims : 충돌을 피하기 위해 명명된 클레임들입니다.
    • Private Claims : 사용자 정의 클레임들로, 서버와 클라이언트 간의 정보를 공유할 때 사용됩니다.

  1. Signature(서명) :
  • 헤더와 페이로드를 인코딩하고, 비밀 키를 사용하여 서명된 부분입니다. 서명은 토큰이 변조되지 않았음을 확인하는 역할을 합니다.

클라이언트가 서버에 로그인 요청을 보내면, 서버는 해당 사용자에 대한 정보를 기반으로 JWT를 생성하고, 이를 클라이언트에게 반환합니다. 클라이언트는 이 JWT를 저장하고, 이후의 요청에서는 이 토큰을 함께 서버에 전달하여 인증을 수행합니다.


JWT의 주요 특징

  • 자가 수용적(Self-contained) : 팔요한 모든 정보를 자체적으로 포함하고 있어 별도의 저장소에 의존하지 않습니다.
  • 암호화(Encrypted) : 서명을 통해 토큰이 변조되지 않았음을 확인하며, 필요에 따라 암호화하여 정보를 안전하게 전송할 수 있습니다.
  • 유연성(Flexibility) : 클레임을 통해 사용자 정의 데이터를 토큰에 포함할 수 있습니다.

JWT를 사용함으로써 서버는 세션 저장소를 관리할 필요 없이 클라이언트에서 토큰을 관리하므로, 분산된 서비스 아키텍처에서도 유용하게 사용될 수 있습니다.



JWT방식과 세션(session)방식

세션 방식

사용자가 로그인에 성공하면 세션이라는 티켓을 출력해줍니다.

그럼 이 티켓을 반으로 나누어 반은 사용자, 반은 메모리(경우에 따라 하드디스크나 DB 등에 담아두기도 함)에 보관합니다.

브라우저는 메모리에 보관한 티켓을 session id란 이름의 쿠키로 저장하고, 이 브라우저는 사이트에 요청을 보낼 때마다 이 티켓을 실어서 보냅니다.

session id를 사용해 사용자가 서버에 로그인 되어 있음이 지속되는 상태를 세션이라고 합니다.

즉, 사용자가 현재 로그인되어 있는 지를 갖고서 서버가 사용자가 로그인에 성공했을 때 출력한 티켓의 반쪽을 어디에 보관했는지(메모리, DB 등) 기억을 해두고 있는 것이 세션입니다.

단점 : 만약 메모리에 티켓을 보관했을 경우, 많은 사용자가 접속했을 경우 메모리가 부족해질 수 있습니다. 또 메모리는 휘발성이기 때문에 서버에 문제가 생겨 메모리가 꺼진다면 보관해둔 티켓은 사라집니다.
이렇게 된다면 로그인이 풀리며, 사용자는 다시 로그인해야 되는 상황이 발생하는데,
그럼 하드디스크나 DB에 저장하면 안되나요?
이 둘은 메모리에 비해 속도가 느려진다는 단점이 있습니다.


이러한 부담 없이 인가를 구현하기 위해 고안된 것이 바로 토큰 방식, JWT 입니다.

JWT 방식

토큰이란?
jwt 방식을 사용하면 사용자가 로그인 했을 때 세션과 같이 토큰이라는 티켓을 출력해주는데, 세션과 다른 점은 이 티켓을 온전히 사용자에게 전달합니다. (서버가 기억하지 않음)

사용자가 받은 토큰은 인코딩 또는 암호화된 3가지 데이터를 이어붙인 것입니다.

토큰을 확인하면 중간 중간에 마침표(.)가 있는데, 이 마침표를 기준으로 3부분으로 나뉩니다.
예시) XXXX.YYYY.ZZZZ

위 토큰은 각각 header(헤더), payload(페이로드), verify signature(서명)로 구분됩니다.

  • header : 헤더를 디코딩해보면 두 가지 정보가 담겨있습니다.
    1. type : 토큰의 타입인데, 여기에는 언제나 jwt가 들어갑니다.
    2. alg : 알고리즘의 약자, 이것은 서명 값을 만드는데 사용될 알고리즘이 지정됩니다.
      헤더와 페이로드, 서버에 감춰놓은 비밀 값을 이 암호화 알고리즘에 넣고 돌리면 서명 값이 나옵니다.
      이와 같은 방법으로 특별한 암호화가 되어있지 않은 페이로드의 문제점을 방지할 수 있습니다.

  • payload : 페이로드를 디코딩해보면 json 형식의 여러 정보들이 들어있습니다.
    이 토큰을 누가 누구에게 발급했는지, 이 토큰이 언제까지 유효한지 등등, 이 정보들은 서비스 측에서 원하는대로 담을 수 있습니다.
    이렇게 토큰에 담긴 사용자의 정보 등의 데이터를 클레임(Claim)이라고 합니다.
    간단하게 말하자면 사용자가 로그인을 하고 나서 받는 토큰에 여러 정보들이 담긴 클레임이라는 것이 실려오고, 그 이후에 요청들마다 사용자로부터 서버한테 보내지고, 사용자가 받아서 갖고 있는 토큰 자체에 정보가 들어있으면 서버가 요청마다 일일이 DB에서 뒤져봐야 할 것들이 줄어듭니다.

그럼 세션을 사용하지 않고 JWT만 사용하면 되지 않나요?


JWT의 단점


세션처럼 stateful(상태 유지)해서 모든 사용자들의 상태를 기억하고 있다는 건 구현하기 부담되고 고려사항도 많습니다.
하지만, 이게 가능하기만 하다면 기억하는 대상의 상태들을 언제든 제어할 수 있다는 장점이 있는데,
예를 들어 만약 한 기기에서만 로그인 가능한 서비스를 만든다고 한다면,
pc에서 로그인한 상태의 사용자가 모바일에서 또 다시 로그인을 시도 한다면 pc에서는 로그아웃 되도록 기존 세션을 종료할 수 있습니다.
하지만 JWT는 사용자에게 토큰을 쥐어주는 방식이기 때문에 제어하기가 힘듭니다.


다른 문제로는 만약 토큰을 해커에게 탈취 당했을 경우 해당 토큰을 무효화할 방법도 없기 때문에 실제 서비스를 운영하는 곳에서는 JWT만으로 인가를 구현하는 곳은 많지 않습니다.

이런 문제점을 보완하기 위해 만료 시간을 짧게 잡아 토큰의 수명을 줄이는 방법이 있습니다.

그럼 얼마 지나지 않아서 또 로그인해야 되지 않나요?

수명이 짧은 access 토큰과 수명이 긴 refresh 토큰이 있습니다.
사용자는 로그인을 하면 이 access 토큰과 refresh 토큰을 받습니다.

서버는 access 토큰과 refresh 토큰을 발급하고 클라이언트에게 보내고 나서 refresh 토큰은 상응값을 DB에도 저장합니다.
사용자는 access 토큰의 수명이 다 하면 refresh 토큰을 보내는데, 서버는 이걸 DB에 저장된 값과 대조해보고 맞다면 새 access 토큰을 발급해줍니다.

즉, 매번 인가를 받을 때 사용하는 수명이 짧은 토큰이 access 토큰이고, access 토큰을 재발급 받을 때 사용하는 것이 refresh 토큰입니다.

이렇게 하면 access 토큰이 탈취 당했을 경우 수명이 짧은 access 토큰의 특성상 오래 사용하지 못하며, 탈취 당한 사용자의 로그아웃을 이행하기 위해 refresh 토큰을 이용해 DB에서 상응값을 지워버려 토큰 갱신이 실패하게 만들어 로그아웃 시킬 수 있습니다.


JWT 예시

구체적인 상황 및 서버의 구현에 따라 조금씩 다르겠지만 주로 사용하는 로직의 예시를 알아보겠습니다.

1. 로그인 :

  • 사용자가 로그인할 때 서버에 아이디와 비밀번호를 전송하여 인증을 받고, 서버에서는 유요한 경우 Access Token과 Refresh Token을 발급합니다.

2. Access Token 저장 :

  • 발급받은 Access Token을 안전한 곳에 저장합니다. 주로 브라우저의 localStorage, sessionStorage, 또는 쿠키 등이 사용됩니다.
// 로그인 후 토큰 저장
const setAccessToken = (token) => {
  localStorage.setItem("accessToken", token);
};

3. API 요청 :

  • 클라이언트에서 서버에 보낼 API 요청에는 발급받은 Access Token을 헤더에 추가합니다.
const axiosWithAuth = axios.create({
  baseURL: // api 주소,
  headers: {
    Authorization: `Bearer ${localStorage.getItem('accessToken')}`,
  }
})

3-1. 모듈화하기 :


위 로직에서 처럼 직접 localStorage.getItem('accessToken')을 사용하는 방식은 해당 로직을 여러 곳에서 중복해서 사용해야 하며, 나중에 변경이 필요할 때 모든 사용처를 찾아 수정해야 하는 불상사가 생깁니다. 이는 유지보수에 어려움을 줄 수 있기 때문에 재사용성을 위해 분리합니다.

토큰 관리하기 :

  • 토큰을 관리할 로직을 따로 생성해줍니다.
    ex)
const TOKEN = "accessToken";
const TokenRepository = {
  // token 저장
  setToken(token) {
    localStorage.setItem(TOKEN, token);
  },
  // token 반환()
  getToken() {
    return localStorage.getItem(TOKEN);
  },
  // token tkrwp
  removeToken() {
    localStorage.removeToken(TOKEN);
  },
};

export default TokenRepository;
  • setToken(token) : 발급 받은 tokenlocalStorageaccessToken이라는 키로 저장합니다. 이렇게 저장된 토큰은 브라우저 세션 간에 유지되며, 나중에 필요할 때 다시 사용될 수 있습니다.
  • getToken() : localStorage에서 accessToken 키에 저장된 토큰을 가져와 반환합니다. 다른 부분에서 이 함수를 호출하여 현재 사용 중인 토큰을 확인할 수 있습니다.
  • removeToken() : localStorage에서 accessToken 키에 저장된 토큰을 제거 합니다. 주로 사용자가 로그아웃할 때나 토큰이 더 이상 유효하지 않을 때 호출됩니다.

3-2. axios 분리 :


export const axiosInstance = axios.create({
  baseURL: // api 주소,
  // ... 추가 로직
})

export const axiosWithAuth = config = > {
  const access_token = TokenRepository.getToken()
  if(access_token) {
    config.headers.Authorization = `Bearer ${access_token}`
  }
  return config
}

axiosInstance.interceptors.request.use(axiosWithAuth)
  • axiosWithAuth는 axios 요청의 설정(config)을 받아와서 해당 설정의 헤더에 Authorization 필드를 추가하고, 이때 헤더에는 TokenRepository.getToken()을 통해 가져온 accessToken이 포함됩니다.
  • axiosInstance.interceptors.request.use(axiosWithAuth) 를 사용해 axiosInstance로 api 요청이 발생하기 전 axiosWithAuth 함수가 실행되도록 설정합니다.

즉, 모든 api 요청이 보내지기 전에 인증 헤더가 추가되도록 중앙에서 처리합니다.


4. Access Token 만료 :

  • Access Token이 만료되면 서버는 새로운 Access Token과 함께 Refresh Token을 발급합니다. 만약 Refresh Token도 만료되었다면 사용자는 다시 로그인해야 합니다.

5. Refresh Token 저장 :

  • 발급받은 Refresh Token을 안전한 곳에 저장합니다.
// 로그인 후 Refresh Token 저장
const setRefreshToken = (token) => {
  localStorage.setItem("refreshToken", token);
};

6. Access Token 갱신 :

  • Access Token이 만료되면 클라이언트에서는 Refresh Token을 사용하여 새로운 Access Token을 요청합니다.
const refreshAccessToken = async () => {
  const refreshToken = localStorage.getItem("refreshToken");

  try {
    const res = await axios.post("api 주소", {
      refreshToken,
    });

    const newAccessToken = res.data.accessToken;
    setAccessToken(newAccessToken);

    // 갱신된 토큰을 사용하여 이후 API 요청 수행
    axiosWithAuth.get("/api/endpoint");
  } catch (err) {
    // Refresh token 이 만료되었을 경우, 로그인 페이지로 리다이렉트 등의 처리
    console.error("Failed to refresh access token: ", err);
  }
};
profile
Hello, World !

0개의 댓글