직관적으로 로그인 기능 구현하기 (Feat. JWT, Access Token, Refresh Token, Cookie, 권한별 라우팅)

1

1 ) 로그인 기능 구현 시 알아야 필수 지식들

로그인 기능을 구현하기 전, 도대체 무엇을 알아야 로그인 기능을 구현할 수 있을지 막막했다. 내가 무엇을 모르는지조차 모르는 상태였기 때문에 한숨만 쉬어지던 때가 있었다. 로그인 기능을 처음 구현하는 사람이라면, 누구나 이런 마음이 들 수 있다는 생각에 로그인 문외한이었던 내가 어떤 과정으로 무엇을 공부했는지 공유해 보려 한다!

1-1. JWT, 너 뭔데?

JWT 를 이용해 로그인을 구현한다는 말을 많이 들어봤지만, 구체적으로 JWT 가 뭐고, 토큰을 어떻게 사용해야할지 모르는 사람들이 많을 것 같다.
먼저 JWT 란 JSON Web Token 의 준말로 클라이언트와 서버가 암호화된 정보를 안전하게 주고 받고 싶을 때 사용하는 특별한 도구이다.
JWT 토큰은 아래와 같이 생겼는데,

여기서 signature 부분에 서버가 중요한 정보를 암호화시켜 클라이언트로 보내주게 된다.

1-2. JWT, 왜 써야 하는데?

우리가 로그인 기능을 구현하는 이유를 생각해 보자. 이는 각각의 회원을 구분하고, 각각의 회원에게 독립된 데이터를 전달하기 위함이다. 즉 타인은 나의 정보를 보면 안 되고, 나는 나의 정보를 볼 수 있어야 한다. 중앙 집중식 서버는 여러 클라이언트로 오는 요청을 동시에 받는다. 그럼 다수의 클라이언트를 구별할 수 있는 일종의 키가 필요하다. 그리고 그 키는 다른 누군가가 보면 안 되는 중요한 정보이다.

즉, 클라이언트를 구분해 주는 중요한 보안 키의 역할을 JWT 가 훌륭하게 해줄 수 있으므로 우리는 JWT 를 사용하는 것이다.

1-3. JWT의 특징, Stateless 와 만료 시간

JWT 의 특징 중 statelss 하다는 것이 있다. 이는 다시 말해 서버가 클라이언트의 인증 상태를 저장하지 않고, JWT 를 요청과 함께 보내면 그때 그때 허용을 해준다는 것이다. 따라서 JWT 는 한 번 만들어지면 제어가 불가능하기 때문에 반드시 토큰의 만료 시간을 지정해 주어야 한다.

1-4. JWT 의 만료 시간으로 인해 생기는 문제점

만약 JWT 토큰의 만료 시간을 필요 이상으로 길게 주면 어떤 일이 생길까? 토큰이 탈취된 경우 탈취자가 토큰을 악용할 가능성이 커진다는 보안상의 이슈가 생길 수 있다. 그렇다고 만료 시간을 짧게 해버리면 사용자는 계속해서 로그인을 해야 하는 불편함을 겪을 수 있다.

보안상의 이슈도 해결하면서 + 사용자가 계속해서 로그인을 하지 않아도 되게 하려면 어떻게 해주어야 할까?

그에 대한 해답으로 나온 것이 access token 과 refresh token 이다.

1-5. Access Token 과 Refresh Token

Access Token : 사용자를 식별하는 데 사용된다. 유지 시간이 상대적으로 짧은 편.
Refresh Token : 새로운 Access Token 을 발급받는 데 사용된다. 유지 시간이 상대적으로 긴 편.

두 개의 기능을 하는 토큰 두 개를 만들어 이 둘을 함께 사용하면 된다. 무슨 말일까?
먼저 상대적으로 지속 시간이 짧은 Access Token 을 만들어 클라이언트를 식별한다. 그럼 토큰이 탈취되었을 때 토큰이 악용될 가능성이 낮아진다.
그리고 이 토큰을 재발급하는 것을 클라이언트가 하는 대신에 Refresh Token 을 이용해 서버 측에서 자동으로 해준다. 그럼 클라이언트는 Refresh Token 이 만료되었을 때만 재요청을 하면 된다. 이때 Refresh Token 의 지속 시간을 적당히 길게 잡아주면 유지 기간이 길면서도 보안성은 강화된 로그인 기능을 구현할 수 있게 된다.

토큰을 받아오면 다수의 페이지에서 해당 토큰으로 유저 인증을 진행하게 된다. 이럴 때 전역적으로 쉽게 토큰에 접근하기 위해 크게 세 가지 저장소가 쓰인다.

세 가지 모두 장단점이 있지만 한 가지 눈 여겨 볼 점은 local storage 는 스크립트 실행으로 값을 가져올 수 있어 XSS 공격에 취약한 반면 쿠키는 httpOnly 와 secure 속성을 활용해 이를 막을 수 있다는 점이다. 이 셋의 차이에 대해서는 아래 블로그 글의 설명으로 갈음하려 한다.

https://youngman12.tistory.com/15

2 ) 로그인 기능을 간단하게 구현해 보자!

위의 필수 지식들의 flow 를 바탕으로 하나씩 차근차근 구현해 보도록 하자.

2-1. 서버로 토큰 요청을 보내고, 토큰을 쿠키에 저장한다.

const handleLogin = async () => {
    try {
      const response = await axios.post(
        `${process.env.BASE_URL}api/test/auth/login`,
        {
          id: 'test',
          password: 'testtest',
          accessTokenExpiredTime: accessTokenExpiredTime,
          refreshTokenExpiredTime: refreshTokenExpiredTime,
        },
      );

      const { accessToken, refreshToken } = response.data;

      Cookies.set('accessToken', accessToken, {
        expires: accessTokenExpiration,
      });
      Cookies.set('refreshToken', refreshToken, {
        expires: refreshTokenExpiration,
      });

      router.push('/home');
    } catch (e) {
      console.log(e);
    }
  };

이때 한 가지 주의할 점. 쿠키의 값에서부터 토큰 값을 읽어온다고 한다면, 쿠키에서의 토큰 지속 시간을 정해 주어야 한다. 이는 Cookiew.set 의 세 번째 인자로 전달하는 expires 객체를 통해 구현할 수 있다.

2-2. 페이지마다 쿠키에 있는 토큰값으로 권한 여부를 확인한다.

  useEffect(() => {
    if (!refreshToken) {
      router.push('auth/login');
    }
  }, [refreshToken]);

 useEffect(() => {
  if (!accessToken) {
    axios
     .post(`${process.env.BASE_URL}/api/test/auth/refresh`, {
        refreshToken:
          'refresh 토큰 값',
        accessTokenExpiredTime: 600,
        refreshTokenExpiredTime: 3600,
      })
      .then((response) => {
        console.log(response);
        const newAccessToken = response.data.accessToken;
        Cookies.set('accessToken', newAccessToken);
        router.push('/home');
      })
      .catch((e) => {
        console.log(e);
        router.push('auth/login');
      });
    }
  }, [accessToken]);

refresh 토큰의 값에 변화가 생겼을 때, refreshToken 이 만료되었다면 로그인 페이지로 라우팅 시키고, accessToken 이 만료된 경우엔 refresh 토큰을 이용해 accessToken 재발급을 요청하는 비동기 코드를 작성하였다.

2-3. 로그아웃 기능 구현

const handleLogout = () => {
    Cookies.remove('accessToken');
    Cookies.remove('refreshToken');
    router.push('auth/login');
  };

로그아웃 기능을 구현할 때에는 쿠키에서 access 와 refresh 토큰을 지워주기만 하면 깔끔하게 구현할 수 있다.

3) 마무리

이렇게 로그인 기능 구현에 필요한 전반적인 지식들을 알아보고, 이를 바탕으로 간단하게 로그인, 로그아웃 기능을 구현해 보았다. 이 글이 로그인 기능을 처음 구현하는 사람들에게 좋은 이정표로 영향을 끼칠 수 있길 바라며 글을 마친다.

4) Ref

https://velog.io/@hahan/JWT%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80
https://mygumi.tistory.com/375
https://youngman12.tistory.com/15

profile
매일 1%씩 성장하는 개발자 보리몽입니다 :D

0개의 댓글