티스토리 뷰

반응형

어플리케이션에서 로그인을 구현하는 방법은 인증을 서버의 세션에서 할지 토큰을 통해서 할지에 따라서 달라지게 됩니다.

이번 글에서는 JWT를 사용하여 로그인을 구현하는 방법에 대해서 작성합니다.

세션 방식과 토큰 방식의 차이점은 아래 글에서 확인 할 수 있습니다.

 

쿠키, 세션(cookie, session)과 토큰 (token, JWT)의 차이점

웹 어플리케이션에서 로그인을 구현하는 방법은 크게 쿠키, 세션을 이용하는 방법과 토큰을 이용하는 방법 2가지가 있습니다. 간단히 말하면 쿠키, 세션 방식은 서버에서 세션아이디를 기록하

jcon.tistory.com

Access token & Refresh token

JWT 토큰을 사용하여 로그인을 구현할 때에는 보통 access token과 refresh token을 함께 사용합니다. access token과 refresh token에 대해 간단히 알아보자면,

 

access token: 인증에 필요한 토큰

refresh token: access token을 다시 발급 받기 위한 토큰

 

access token만 사용하는 것이 아니라 refresh 토큰을 함께 사용하는 이유는 JWT토큰 방식의 특성 상 JWT토큰이 유효할 경우 인증된 유저로 판단하기 때문에 acess token이 탈취 되었을 때의 피해를 최소화 하기 위해서 access token을 다시 발급 받을 수 있는 refresh token과 함께 사용합니다.

 

구현

작성되어 있는 코드는 의사코드로 동작을 보장하지 않습니다. 상황에 맞게 응용하세요.

 

우선 로그인 시에 동작하는 flow를 정리하자면,

1. 새로고침 시에도 로그인 상태가 유지되어야 한다.

2. access token이 만료되면 refresh token을 통해서 재발행 받아야 한다.

3. refresh token이 만료되었다면 로그아웃 되어야 한다.

입니다.

 

세부 사항을 구현하기 전에 access token은 api를 요청할 때 자주 사용하게 됨으로 여러 컴포넌트에서 공유되는 것이 좋을 것 같아 access token을 관리하는 context api를 통해서 공유되도록 합니다.

// SessionProvider.js
import React from "react";

const SessionContext = React.createContext<SessionContextValue>();

export default const SessionProvider = ({
  children,
}) => {
  const [session, setSession] = React.useState();

  return (
    <SessionContext.Provider
      value={{
        session,
        setSession,
      }}
    >
      {children}
    </SessionContext.Provider>
  );
};
//App.js
import SessionProvider from 'SessionProvider.js'

export default const App = () => {
	return (
    	<SessionProvider>
        	<Component />
        </SessionProvider>
    )
}

그 후에 새로고침시에도 로그인을 유지시키기 위해서는 로컬스토리지에 access token과 refresh token을 저장해 둘 필요가 있습니다.

// SessionProvider.js
import React from "react";

const SessionContext = React.createContext<SessionContextValue>();

export default const SessionProvider = ({
  children,
}) => {
  const [session, setSession] = React.useState();
  
  // 로그인 시에 로컬스토리지에 저장
  React.useEffect(() => {
    if (session) {
      localStorage.setItem("authToken", session.authToken);
      localStorage.setItem("refreshToken", session.refreshToken);
    }
  }, [session]);
  
  // 새로고침, 재접속 시 로그인 유지를 담당
  React.useEffect(() => {
    const localStorageAuthToken = localStorage.getItem("authToken");
    const localStorageRefreshToken = localStorage.getItem("refreshToken");

    if (localStorageAuthToken && localStorageRefreshToken) {
      setSession({
        authToken: localStorageAuthToken,
        refreshToken: localStorageRefreshToken,
      });
    }
  }, []);


  return (
    <SessionContext.Provider
      value={{
        session,
        setSession,
      }}
    >
      {children}
    </SessionContext.Provider>
  );
};

로그인 시에 access token과 refresh token을 setSession을 통해 session state에 업데이트 하면, useEffect를 통해서 로컬스토리지에 저장됩니다. 새로 고침 시에는 로컬 스토리지에서 가져와 session state에 업데이트 합니다.

 

이제 새로고침 시에도 로그인이 풀리지 않고, 유지 할 수 있게 되었습니다. 다음은 access token이 만료되었을 경우 refresh token을 재발급하고 refresh token이 만료되었다면 로그아웃 시키도록 하겠습니다. access token을 재발급 받는 여러가지 방법이 있겠지만, 지금은 setInterval을 통해 일정 시간 마다 access token이 만료되었는지 체크하고 만료 되었으면 토큰을 재발행하겠습니다.

// SessionProvider.js
import React from "react";
import { decodeJwt } from "jose";

const SessionContext = React.createContext<SessionContextValue>();
const DEFAULT_REFRESH_INTERVAL_MS = 1000;

export default const SessionProvider = ({
  children,
}) => {
  const [session, setSession] = React.useState();
  
  // 로그인 시에 로컬스토리지에 저장
  React.useEffect(() => {
    if (session) {
      localStorage.setItem("authToken", session.authToken);
      localStorage.setItem("refreshToken", session.refreshToken);
    }
  }, [session]);
  
  // 새로고침, 재접속 시 로그인 유지를 담당
  React.useEffect(() => {
    const localStorageAuthToken = localStorage.getItem("authToken");
    const localStorageRefreshToken = localStorage.getItem("refreshToken");

    if (localStorageAuthToken && localStorageRefreshToken) {
      setSession({
        authToken: localStorageAuthToken,
        refreshToken: localStorageRefreshToken,
      });
    }
  }, []);
  
  // 토큰 재발급을 담당
  React.useEffect(() => {
    regenerateExpiredToken();

    const intervalId = setInterval(
      regenerateExpiredToken,
      DEFAULT_REFRESH_INTERVAL_MS
    );

    return () => clearInterval(intervalId);
  }, [regenerateExpiredToken]);
  
  const regenerateExpiredToken = async () => {
    try {
      if (!session) return;

      const authTokenExpiredTime = decodeJwt(session.authToken).exp;
      const refreshTokenExpiredTime = decodeJwt(session.refreshToken).exp;
      const nowUnixTime = dayjs().unix();

      // refresh token이 만료되었다면 로그아웃
      if (refreshTokenExpiredTime < nowUnixTime) {
        throw new Error("Expired refresh token");
      }

      // access token이 만료되었다면 토큰 재발급 후 업데이트
      if (authTokenExpiredTime < nowUnixTime) {
        const { accessToken, refreshToken } = await getRegeneratedToken({ refreshToken });
        setSession({ accessToken, refreshToken });
    } catch (error) {
      removeSession();
    }
  };
  
  // 로그아웃 로직
  const removeSession = () => {
    setSession(undefined);
    localStorage.removeItem("authToken");
    localStorage.removeItem("refreshToken");
  };

  return (
    <SessionContext.Provider
      value={{
        session,
        setSession,
      }}
    >
      {children}
    </SessionContext.Provider>
  );
};

getRegeneratedToken은 refresh token을 전달하면 유효한 새로운 access token과 refresh token을 reponse하는 api라고 생각하시면 됩니다. 로그아웃 시에는 로컬스토리지에 있는 토큰들을 삭제하고, session state도 초기화 하여 구현합니다.

또 다른 방법

access token을 로컬스토리지에 저장하는 것은 xss 공격에 탈취당할 위험성이 있습니다.

 

이를 해결하는 또 다른 방법은 로그인 시에 로컬스토리지에 refresh token만을 저장하고 access token은 state에만 저장합니다. 그리고 setInterval로 일정시간마다 체크하는 것이 아니라, setTimeout을 이용하여 access token의 유효시간 또는 유효시간 보다 조금 작은 시간에 토큰을 다시 재발급합니다.

 

새로고침 시에는 저장된 refresh token을 이용해서 access token을 항상 새로 받도록하는 방법으로 구현을 한다면 xss 공격에서 access token을 탈취당할 위험이 줄어듭니다.

 

 

쿠키, 세션(cookie, session)과 토큰 (token, JWT)의 차이점

웹 어플리케이션에서 로그인을 구현하는 방법은 크게 쿠키, 세션을 이용하는 방법과 토큰을 이용하는 방법 2가지가 있습니다. 간단히 말하면 쿠키, 세션 방식은 서버에서 세션아이디를 기록하

jcon.tistory.com

반응형
댓글