좋은 로그아웃 경험의 조건

켄지 바후스
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 값을 먼 과거의 날짜로 설정하고 쿠키 값을 빈 문자열로 설정합니다.

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 전용 쿠키 1개와 자바스크립트를 통해 액세스할 수 있는 일반 쿠키 1개)를 요구할 수도 있습니다. 사용자가 오프라인 상태에서 로그아웃하려는 경우 자바스크립트 쿠키를 지우고 가능한 경우 다른 정리 작업을 진행할 수 있습니다. 서비스 워커가 있는 경우 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 헤더로 제공된 동일 출처 페이지를 제거합니다.
    • 하나 이상의 보안 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.
        // ...
      }
    }
    

결론

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