좋은 로그아웃 경험의 조건

Kenji Baheux
Kenji Baheux

로그아웃

사용자가 웹사이트에서 로그아웃하는 것은 맞춤설정된 사용자 환경에서 완전히 벗어나고 싶다는 의사를 표현한 것입니다. 따라서 사용자의 정신 모델을 최대한 준수하는 것이 중요합니다. 예를 들어 적절한 로그아웃 환경에서는 사용자가 로그아웃하기 전에 열었던 모든 탭을 고려해야 합니다.

우수한 로그아웃 경험의 핵심은 사용자 환경의 시각적 측면과 상태 측면에서 일관성을 유지하는 것으로 요약할 수 있습니다. 이 가이드에서는 주의해야 할 사항과 좋은 로그아웃 환경을 조성하는 방법에 대한 구체적인 조언을 제공합니다.

주요 고려사항

웹사이트에서 로그아웃 기능을 구현할 때는 다음과 같은 사항을 주의하여 원활하고 안전하며 직관적인 로그아웃 프로세스를 보장합니다.

  • 명확하고 일관된 로그아웃 UX: 명확하고 일관되게 보이는 로그아웃 버튼 또는 링크를 웹사이트 전체에서 쉽게 식별하고 액세스할 수 있어야 합니다. 모호한 라벨을 사용하거나 잘 보이지 않는 메뉴, 하위 페이지 또는 기타 직관적이지 않은 위치에서 로그아웃 기능을 숨기지 마세요.
  • 확인 메시지: 로그아웃 프로세스를 완료하기 전에 확인 메시지를 구현합니다. 이렇게 하면 사용자가 실수로 로그아웃하는 것을 방지할 수 있고, 사용자가 정말로 로그아웃해야 하는지(예: 안전한 비밀번호나 기타 인증 메커니즘으로 기기를 부지런히 잠가야 하는 경우) 다시 고려할 수 있습니다.
  • 여러 탭 처리: 사용자가 동일한 웹사이트의 여러 페이지를 서로 다른 탭에서 연 경우 한 탭에서 로그아웃하면 해당 웹사이트의 열려 있는 다른 탭도 모두 업데이트되어야 합니다.
  • 보안 방문 페이지로 리디렉션: 로그아웃이 완료되면 더 이상 로그인 상태가 아님을 알 수 있는 보안 방문 페이지로 사용자를 리디렉션합니다. 맞춤설정된 정보가 있는 페이지로 사용자를 리디렉션하지 마세요. 마찬가지로 다른 탭에 더 이상 로그인 상태가 반영되지 않도록 하세요. 또한 공격자가 활용할 수 있는 열린 리디렉션을 빌드하지 않도록 해야 합니다.
  • 세션 정리: 사용자가 로그아웃하면 사용자의 세션과 관련된 민감한 사용자 세션 데이터, 쿠키 또는 임시 파일을 완전히 삭제합니다. 이렇게 하면 사용자 정보나 계정 활동에 대한 무단 액세스를 방지하고 브라우저가 다양한 캐시, 특히 뒤로-앞으로 캐시에서 민감한 정보가 포함된 페이지를 복원하는 것을 방지할 수 있습니다.
  • 오류 처리 및 의견: 사용자가 로그아웃할 때 문제가 발생하면 사용자에게 명확한 오류 메시지나 의견을 제공합니다. 로그아웃 프로세스가 실패할 경우 보안 위험이나 데이터 유출 가능성을 알려줍니다.
  • 접근성 고려사항: 스크린 리더나 키보드 탐색과 같은 보조 기술을 사용하는 장애인 등 장애가 있는 사용자도 로그아웃 메커니즘에 액세스할 수 있는지 확인합니다.
  • 교차 브라우저 호환성: 여러 브라우저와 기기에서 로그아웃 기능을 테스트하여 일관되고 안정적으로 작동하는지 확인합니다.
  • 지속적인 모니터링 및 업데이트: 로그아웃 프로세스에 잠재적인 취약점이나 보안 허점이 있는지 정기적으로 모니터링합니다. 시기적절한 업데이트와 패치를 구현하여 식별된 문제를 해결합니다.
  • ID 제휴: 사용자가 제휴 ID를 사용하여 로그인한 경우 ID 공급업체의 로그아웃도 지원되고 필요한지 확인합니다. 또한 ID 공급업체에서 자동 로그인을 지원하는 경우 자동 로그인을 차단해야 합니다.

권장사항

  • 로그아웃 과정 (또는 기타 액세스 취소 흐름) 중에 서버의 쿠키를 무효화하는 경우 사용자 기기에서도 쿠키를 삭제해야 합니다.
  • 사용자 기기에 저장했을 수 있는 쿠키, localStorage, sessionStorage, indexedDB, CacheStorage, 기타 로컬 데이터 스토어 등 민감한 정보를 모두 정리합니다.
  • 브라우저가 영구 저장소(예: 디스크)에 이러한 리소스를 저장하지 않도록 민감한 정보(특정 HTML 문서)가 포함된 모든 리소스가 Cache-control: no-store HTTP 헤더와 함께 반환되도록 합니다. 마찬가지로 민감한 정보를 반환하는 XHR/fetch 호출도 캐싱을 방지하기 위해 Cache-Control: no-store HTTP 헤더를 설정해야 합니다.
  • 서버 측 액세스 취소를 포함하여 사용자 기기에서 열려 있는 모든 탭이 최신 상태인지 확인합니다.

로그아웃 시 민감한 정보 삭제

로그아웃할 때 임시 및 로컬에 저장된 민감한 정보를 삭제하는 것이 좋습니다. 민감한 정보에 중점을 두는 이유는 모든 것을 지우면 사용자가 다시 돌아올 수 있기 때문에 사용자 환경이 크게 악화될 것이라는 사실에서 비롯됩니다. 예를 들어 로컬에 저장된 모든 데이터를 삭제하려는 경우, 사용자는 쿠키 사용 동의 메시지를 재확인하고 애초에 웹사이트를 방문한 적이 없는 것처럼 다른 절차를 거쳐야 합니다.

쿠키를 삭제하는 방법

로그아웃 상태를 확인하는 페이지의 응답에 Set-Cookie HTTP 헤더를 첨부하여 민감한 정보와 관련이 있거나 민감한 정보가 포함된 모든 쿠키를 지웁니다. expires 값은 먼 과거의 날짜로 설정하고 쿠키 값을 빈 문자열로 설정합니다.

<ph type="x-smartling-placeholder">
Set-Cookie: sensitivecookie1=; expires=Thu, 01 Jan 1970 00:00:00 GMT; secure
Set-Cookie: sensitivecookie2=; expires=Thu, 01 Jan 1970 00:00:00 GMT; secure
...

오프라인 시나리오

위에서 설명한 접근 방식은 일반적인 사용 사례에 충분하지만 사용자가 오프라인으로 작업하는 경우에는 작동하지 않습니다. 로그인 상태를 추적하기 위해 두 개의 쿠키(안전한 HTTPS 전용 쿠키 및 JavaScript를 통해 액세스할 수 있는 일반 쿠키)를 요구하는 것이 좋습니다. 사용자가 오프라인 상태에서 로그아웃하려는 경우 JavaScript 쿠키를 삭제하고 가능한 경우 다른 정리 작업을 진행할 수 있습니다. 서비스 워커가 있는 경우 Background Fetch API를 활용하여 사용자가 나중에 온라인 상태가 되면 서버의 상태를 지우기 위한 요청을 다시 시도할 수도 있습니다.

스토리지를 정리하는 방법

로그아웃 상태를 확인하는 페이지에 대한 응답에서 다음과 같이 다양한 데이터 저장소에서 민감한 정보를 정리해야 합니다.

  • sessionStorage: 사용자가 웹사이트에서 세션을 종료할 때 삭제되지만 웹사이트에서 열린 모든 탭을 닫는 것을 잊은 경우를 대비하여 사용자가 로그아웃할 때 민감한 정보를 사전에 삭제하는 것이 좋습니다.

    // Remove sensitive data from sessionStorage
    sessionStorage.removeItem('sensitiveSessionData1');
    // ...
    
    // Or if everything in sessionStorage is sensitive, clear it all
    sessionStorage.clear();
    
  • localStorage, indexedDB, Cache/Service Worker API: 사용자가 로그아웃하면 세션에서 민감한 정보가 유지된다는 점을 감안하여 이러한 API를 사용해 저장한 민감한 정보를 모두 정리합니다.

    // Remove sensitive data from localStorage:
    localStorage.removeItem('sensitiveData1');
    // ...
    
    // Or if everything in localStorage is sensitive, clear it all:
    localStorage.clear();
    
    // Delete sensitive object stores in indexedDB:
    const name = 'exampleDB';
    const version = 1;
    const request = indexedDB.open(name, version);
    
    request.onsuccess = (event) => {
      const db = request.result;
      db.deleteObjectStore('sensitiveStore1');
      db.deleteObjectStore('sensitiveStore2');
    
      // ...
    
      db.close();
    }
    
    // Delete sensitive resources stored via the Cache API:
    caches.open('cacheV1').then((cache) => {
      await cache.delete("/personal/profile.png");
    
      // ...
    }
    
    // Or better yet, clear a cache bucket that contains sensitive resources:
    caches.delete('personalizedV1');
    

캐시를 정리하는 방법

  • HTTP 캐시: 민감한 정보가 있는 리소스에 Cache-control: no-store를 설정하는 한 HTTP 캐시는 민감한 정보를 유지하지 않습니다.
  • 뒤로-앞으로 캐시: 마찬가지로 Cache-control: no-store에 관한 권장사항과 사용자가 로그아웃할 때 민감한 쿠키 (예: 인증 관련 보안 HTTPS 전용 쿠키)를 삭제하는 방법에 관한 권장사항을 따랐다면 민감한 정보가 뒤로-앞으로 캐시에 보관되는 것에 대해 걱정할 필요가 없습니다. 실제로 뒤로-앞으로 캐시 기능은 다음 신호 중 하나 이상을 발견하는 경우 Cache-control: no-store HTTP 헤더와 함께 제공되는 동일한 출처 페이지를 제거합니다. <ph type="x-smartling-placeholder">
      </ph>
    • 하나 이상의 보안 HTTPS 전용 쿠키가 수정 또는 삭제되었습니다.
    • 페이지에서 실행한 XHR/fetch 호출의 하나 이상의 응답에 Cache-control: no-store HTTP 헤더가 포함되어 있습니다.

여러 탭에서 일관적인 사용자 환경

사용자가 로그아웃하기 전에 웹사이트에서 여러 개의 탭을 열었을 수 있습니다. 그때쯤이면 다른 탭이나 다른 브라우저 창에 대해 잊었을지도 모릅니다. 사용자가 모든 관련 탭과 창을 닫지 않도록 하는 것이 가장 좋습니다. 대신 사용자의 로그인 상태가 여러 탭에서 일관되도록 하여 선제적인 조치를 취합니다.

방법

여러 탭에서 일관된 로그인 상태를 유지하려면 pageshow/pagehide 이벤트와 Broadcast Channel API를 함께 사용해 보세요.

  • pageshow 이벤트: pageshow가 유지되면 사용자의 로그인 상태를 확인하고 사용자가 더 이상 로그인하지 않은 경우 민감한 정보(또는 전체 페이지)를 삭제합니다. pageshow 이벤트는 뒤로-앞으로 탐색에서 복원된 후 페이지가 처음으로 렌더링되기 에 트리거됩니다. 따라서 로그인 상태 확인을 통해 페이지를 민감하지 않은 상태로 재설정할 수 있습니다.

    window.addEventListener('pageshow', (event) => {
      if (event.persisted && !document.cookie.match(/my-cookie)) {
        // The user has logged out.
        // Force a reload, or otherwise clear sensitive information right away.
        body.innerHTML = '';
        location.reload();
      }
    });
    
  • Broadcast Channel API: 이 API를 사용하여 탭과 창에서 로그인 상태 변경사항을 전달합니다. 사용자가 로그아웃한 경우 모든 민감한 정보를 삭제하거나 민감한 정보가 포함된 모든 탭과 창에서 로그아웃 페이지로 리디렉션합니다.

    // Upon logout, broadcast new login state so that other tabs can clean up too:
    const bc = new BroadcastChannel('login-state');
    bc.postMessage('logged out');
    
    // [...]
    const bc = new BroadcastChannel('login-state');
    bc.onMessage = (msgevt) => {
      if (msgevt.data === 'logged out') {
        // Clean up, reload or navigate to the sign-out page.
        // ...
      }
    }
    

결론

이 문서의 안내를 따르면 의도치 않은 로그아웃을 방지하고 사용자의 개인 정보를 보호하는 우수한 로그아웃 사용자 환경을 설계할 수 있습니다.