React&Next.js

로그인 & 회원가입 기능에 JWT 도입하기

jjin502 2023. 3. 17. 00:07

도입 배경

현재 진행하기로 한 프로젝트는 STAY MATE라는 프로젝트로, 기숙사 입소 인증을 돕기 위한 플랫폼이다. 해당 프로젝트에서 학생들의 정보를 관리하기 위해 로그인 및 회원가입 기능이 필요하다. 그래서 정보를 관리하고 저장할 브라우저 저장소가 필요했고, 쿠키와 세션 그리고 토큰(JWT) 중 이번에 토큰(JWT)을 활용하기로 하였다.

왜 JWT 여야만 할까?

이전에 쿠키를 사용해서 로그인과 회원가입 기능을 개발한 적 있다. 그 경험을 토대로 이번에도 똑같이 적용하려고 했으나, 쿠키를 사용하기엔 큰 문제점이 있었다. 

 

바로, 정보 탈취의 위험성 이 있다는 것이다. 쿠키에 대해 공부하던 중 아래와 같은 단점에 대해 알게 되었다.

  • 보안에 취약하다. (요청 시 쿠키의 값을 그대로 보내어, 유출 및 조작당할 위험이 존재한다.

해당 프로젝트는 기숙사에 본인이 들어왔다는 것(입소)을 직접 인증해야한다. 즉, 다른 누군가가 계정을 탈취해서 대신 인증하는 그런 불상사는 막아야한다는 의미다. 쿠키로 사용할 경우 이와 같은 일이 너무 쉽게 일어날 가능성이 높다. 게다가 학교 특성상 학생들이 곧 개발자이기 때문에 더 보안에 신경써야했다.(누군가가 정보보안 시간에 배운걸 써먹기라도 하면..)

 

그래서 세션과 JWT 방식 중 고민했다. 각 방법의 장단점을 확인했고, 인증 정보에 대한 별도의 저장소가 필요없는 그리고 확장성이 우수한 JWT 방식을 선택하기로 했다. 이번 프로젝트로 인해서 기숙사 서비스가 많이 생기면, 여기에서 만든 로그인을 OAuth로 만들어 사용할 수도 있지 않을까? 그 가능성을 염두에 두기로 했다.

 

그래서, JWT가 뭔데?

JWT(Json Web Token)은 인증에 필요한 정보들을 암호화시킨 토큰이다. 

구조

Header, Paload, Signature로 이뤄졌고, 각각 Base 64로 인코딩 되어 "."으로 구분한다. 세 구조의 역할은 아래와 같다.

 

  • Header
    • 시그니처 생성을 위해 어떤 알고리즘을 사용할지, 이 토큰은 어떤 토큰인지 기록하는 부분으로 alg와 typ으로 이루어져 있다. typ은 JWT로 사용하기 때문에 JWT로 사용한다.
    • alg는 어떤 알고리즘을 가지고 시그니처 부분을 암호화할지 정한다.
{
  alg : "HS256",
  typ : "JWT"
}

 

  • Paload
    • 실질적인 정보가 저장되는 구간이다. 
    • 저장되어 있는 정보의 각 부분을 클레임(Claim)이라고 한다.
{
 email : jjj8823@gmail.com
 password: 13572468
}

 

  • Signature
    • 이 토큰이 올바른 토큰인지 확인하는 부분으로 이 부분이 존재하지 않는 다면 그저 문자열일 뿐이다.
    • Header와 Paload를 Base64로 인코딩한 뒤 " . "으로 연결한다.
    • 인코딩한 값을 비밀 키를 사용해서 암호화를 진행, 그 암호화된 값을 다시 Base64로 인코딩한다.

사용법

사용자의 email과 password를 입력 받고 "로그인" 버튼을 클릭했을 때 정보를 인증하고, localStorage에 저장해주기로 했다. 흐름을 정리하자면 아래와 같다.

  1. 사용자로 부터 email과 password를 입력받는다.
  2. "로그인" 버튼을 클릭 할 때
  3. 서버가 인증 정보를 보내준다. 이때 암호화가 가능한 데이터 패키지 안에 인증 정보를 보내주는데, 이 패키지가 JWT다.
  4. 담긴 정보 중 accessToken은 만료 기간이 굉장히 짧기 때문에, refreshToken이 이후 유저 인증에 사용된다.
  5. 이 정보를 클라이언트(localStorage)에 저장한다. (refreshToken이 만료되지 않는 한 로그인은 계속 유지된다.)
  6.  accessToken을 사용자에게만 보여줄 수 있는 정보에 접근할 때 서버에 보내면
  7. 서버는 그 토큰이 유효한지 확인하는 방식으로 인증한다.

이를 코드를 통해서 나타낼 수 있다.

const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    
    // 로그인 요청 로직
    const response = await fetch("/api/login", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ email, password }),
    });

    const data = await response.json();

    if (data.success) {
      // JWT 토큰을 클라이언트에 저장하는 코드 작성
      axios.defaults.headers["x-access-token"] = data.token;
      localStorage.setItem("token", data.token);
      localStorage.setItem("admin", data.isAdmin);
      localStorage.setItem("userId", data.userId);
      
      // Sweet Alert 라이브러리를 사용해 로그인 성공 여부 알려주기
      Swal.fire({
        icon: "success",
        title: "로그인 성공",
      });
      
      // 로그인에 성공했으므로 메인 페이지로 이동
      router.push("/main");
    } else {
      // 로그인 실패 처리
      Swal.fire({
        icon: "error",
        title: "로그인 실패",
      });
    }
  };

 

코드에서 약간 아쉬운점이 있다면, response 받은 내용을 localStorage에 저장할 때 한 줄씩 저렇게 저장하는게 효율적인 코드일까? 고민하게 된다. 객체 형태로 저장할 수 있는 방식은 없을까? 이 방법은 추후에 고민하거나, 혹시 아시는 분이 계시다면 알려주시면 감사하겠습니다. 🙇‍♀️

결론

요즘 프로젝트나 코드를 보면 JWT를 사용해서 암호화하는 방식을 사용한 부분들이 많이 보인다. 그런데 막상 왜 사용했는가에 대해 이야기하면 "그냥 많이 쓰잖아요."라는 답변을 꽤 들었던것 같다. 나도 아직까지는 쿠키와의 엄청 큰 차이점은 잘 모르겠지만, 이 프로젝트를 서비스화 하고 나면 조금은 알 수 있을 것이라고 생각한다. 

 

때에 맞게, 상황에 맞게 적절하게 장단점을 고려해서 기술을 사용하는 사람이 되고싶다. 

 

참고자료