[TAROYAKI] Ep13-2. 로그인 시스템 호스팅(Frontend)

Yihoon·2025년 5월 6일

TAROYAKI

목록 보기
16/20
post-thumbnail

Frontend

전편에 이어서, 프론트 쪽 로그인 흐름을 재설계한 내용을 정리한다.
백엔드 아키텍처에 맞게 흐름을 개선하고, 오류 상황에 대한 강건성을 높이는 데 포커스를 맞추었다.

client.js

설정 및 초기화
먼저 페이지 초기화 시 다음 함수가 실행된다.
우선 이벤트 리스너들을 초기화하고 URL 파라미터에서 인증 코드가 존재하는지 확인한다.

async function initializePage() {
    try {

        // 기본 이벤트 리스너 초기화
        initializeEventListeners();
        disablePreLoginFeatures();

        // URL 파라미터 체크
        const urlParams = new URLSearchParams(window.location.search);
        const authCode = urlParams.get('code');

        if (authCode) {
            await handleAuthenticationFlow();
            return;
        }

인증 코드가 없는 경우 토큰의 유효성을 검증한다.

        const isValid = await TokenManager.validateTokenSet();
        if (!isValid) {
            const beforelogin = document.getElementById('beforelogin');
            // 이하 생략: 토큰 코드가 없을 경우 로그인 화면이 표시됨
            }
            return;
        }

유효한 토큰이 있을 경우 사용자 정보를 추출하고 세션 목록을 불러온다.

        // 5. 유효한 토큰이 있는 경우의 초기화
        const idToken = localStorage.getItem('auth_token');
        if (idToken) {
            const tokenPayload = parseJwt(idToken);
            if (tokenPayload?.sub) {
                userId = tokenPayload.sub;
                localStorage.setItem('userId', userId);
                // 중략: 토큰 존재할 경우 UI 업데이트 및 사용자 이름 표시
                    if (userId) {
                        await fetchSessions(userId);
                    }   
    } catch (error) {
        console.error('Initialization failed:', error);
        // ...
    }
}

인증 흐름 관련
아래 함수는 토큰 발급에 사용된다. authCode를 받아 Cognito 엔드포인트에 요청을 보내고, 반환된 ID token, Access token, Refresh token들을 localstorage에 저장한다.

async function getToken(authCode) {
    const headers = new Headers({
        'Authorization': 'Basic ' + btoa(config.clientId + ':' + config.clientSecret),
        'Content-Type': 'application/x-www-form-urlencoded'
    });

    const body = new URLSearchParams({
        grant_type: 'authorization_code',
        code: authCode,
        redirect_uri: config.redirectUri
    });

    const response = await fetch(`${config.domain}/oauth2/token`, {
        method: 'POST',
        headers,
        body
    });

    const data = await response.json();
    
    // 토큰 저장
    localStorage.setItem('auth_token', data.id_token);
    localStorage.setItem('access_token', data.access_token);
    localStorage.setItem('refresh_token', data.refresh_token);
    console.log('received token successfully');
    return data;
}

인증 흐름은 아래 함수에서 확인할 수 있다.다음 코드들은 URL에서 authCode를 추출해 Token으로 교환하고 사용자 정보 표시, 세션 초기화 등을 담당한다.

먼저 URL 파라미터에서 authCode를 확인하고 Token 개체를 반환받는다.

async function handleAuthenticationFlow() {
    try {
        const urlParams = new URLSearchParams(window.location.search);
        const authCode = urlParams.get('code');

        if (!authCode) return false;

        const tokenData = await getToken(authCode);

다음으로 URL에 남아 있는 authCode를 제거한다.

        window.history.replaceState({}, document.title, window.location.pathname);

        //중략: 로그인 후 UI 표시, 주요 기능 활성화

여기서는 payload에서 userId(sub)를 추출하고 localstorage에 저장한다.

        // userId 설정
        const tokenPayload = parseJwt(tokenData.id_token);
        if (tokenPayload?.sub) {
            userId = tokenPayload.sub;
            localStorage.setItem('userId', userId);
        }

idtoken에서 이메일 정보를 추출해 표시하고, 세션을 불러온다.

        const userInfo = await getUserInfo(tokenData.id_token);
        document.getElementById('userinfo').innerText = userInfo.email;
        updateProfileButton(userInfo);
        initializeEventListeners();
        if (userId) {
            await fetchSessions(userId);
        }
        console.log('handle authentication complete');
        return true;
    } catch (error) {
        console.error('Authentication error:', error);
        document.getElementById('userinfo').innerText = 'Error fetching user info.';
        return false;
    }
}

토큰 관리
API call이 일어나기 전 토큰의 유효성을 확인하고, 만료되었다면 새로 갱신한다. 해당 함수는 모든 API call에 호출되어 유효성 검증에 사용된다.

async function validateTokenBeforeRequest() {
  const token = localStorage.getItem('accessToken');
  const expiry = localStorage.getItem('tokenExpiry');
  
  if (!token || !expiry || Date.now() >= parseInt(expiry)) {
    // 토큰 갱신 대기
    await refreshTokens();
  }
  return localStorage.getItem('accessToken');
}

위에서 다룬 토큰 갱신은 아래 코드들로 이루어진다. API call로 새 토큰을 발급받고 새 토큰을 업데이트한다. 실패할 경우 자동으로 로그아웃된다.

async function refreshTokens() {
  const refreshToken = localStorage.getItem('refreshToken');
  if (!refreshToken) {
    throw new Error('No refresh token available');
  }
  
// refreshtoken으로 새 토큰 발급
  try {
    const response = await fetch(`${FLASK_URL}/auth/refresh`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ refresh_token: refreshToken })
    });

    if (response.ok) {
      const data = await response.json();
      
      // 전역 변수 및 localStorage 모두 새 토큰으로 업데이트
      accessToken = data.access_token;
      tokenExpiryTime = Date.now() + (data.expires_in * 1000);
      
      localStorage.setItem('accessToken', data.access_token);
      localStorage.setItem('tokenExpiry', tokenExpiryTime.toString());
      
      return data.access_token;
    } else {
      throw new Error('Token refresh failed');
    }
  } catch (error) {
  	// 토큰에 문제 있을 경우 로그인 리다이렉션
    localStorage.clear();
    redirectToLogin();
    throw error;
  }
}

토큰이 만료되었거나 만료가 임박한 경우에도 토큰 갱신을 수행해야 한다. 갱신에 실패하더라도 현재 토큰이 만료되지 않았다면 해당 토큰을 계속 사용할 수 있도록 한다.

async function ensureValidToken() {
	//localstorage에서 현재 토큰 호출
    const idToken = localStorage.getItem('auth_token');
    const refreshToken = localStorage.getItem('refresh_token');

    if (!idToken || !refreshToken) {
        throw new Error('No tokens available');
    }

    if (isTokenExpired(idToken)) {
        // 토큰이 만료된 경우 갱신 시도
        await refreshTokens(refreshToken);
    } else if (needsRefresh(idToken)) {
        // 만료가 임박한 경우 갱신 시도
        try {
            await refreshTokens(refreshToken);
        } catch (error) {
            // 갱신 실패했지만 현재 토큰이 아직 유효한 경우 계속 진행
            if (!isTokenExpired(idToken)) {
                console.warn('Token refresh failed but current token still valid');
            } else {
                throw error;
            }
        }
    }
}

사용자 정보 및 Session
아래 함수로는 사용자 정보(sub, 이메일, 닉네임)을 호출한다.
위에서 다룬 ensureValidToekn()이 여기서도 사용된다.

async function getUserInfo(token) {
    // API 호출 전 토큰 유효성 확인 및 갱신
    await ensureValidToken();
    
    // 갱신된 토큰으로 API 호출
    const currentToken = localStorage.getItem('auth_token');
    const response = await fetch(config.authEndpoint, {
        headers: { Authorization: currentToken }
    });
    const userData = await response.json();
    console.log('userdata received');
    return JSON.parse(userData.body);
}

최종적으로는 사용자 경험을 해친다는 이유로 제거하였지만 사용자가 장기간 비활성화되어있을 경우 자동으로 로그아웃을 수행하는 함수도 구축했었다.

function initializeSessionCheck() {
    let lastActivity = Date.now();
    
    // 사용자 활동 가지
    ['click', 'keypress', 'scroll', 'mousemove'].forEach(event => {
        document.addEventListener(event, () => {
            lastActivity = Date.now();
        });
    });

    // 주기적으로 세션 상태 체크
    setInterval(async () => {
        const inactiveTime = (Date.now() - lastActivity) / 1000;
        if (inactiveTime >= config.sessionDuration) {
            await handleLogout();
        }
    }, 60000); // 1분마다 확인
}

로그아웃
로그아웃은 아래 함수가 담당한다. 모든 토큰과 사용자 정보를 localstorage에서 제거하고, 사용자를 리다이렉션시킨다.

function handleLogout() {
    // 모든 토큰 제거
    localStorage.removeItem('auth_token');
    localStorage.removeItem('access_token');
    localStorage.removeItem('refresh_token');
    localStorage.removeItem('userId');
    
    const userinfoElement = document.getElementById('userinfo');
    if (userinfoElement) {
        userinfoElement.innerText = '';
    }
    
    const logoutUrl = `${config.domain}/logout?client_id=${config.clientId}&logout_uri=${encodeURIComponent(config.logoutRedirectUri)}`;
    window.location.href = logoutUrl;
    console.log('handlelogout');
}
profile
딴짓 좋아하는 데이터쟁이

0개의 댓글