좋은 로그아웃 경험의 조건

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 값을 오래된 날짜로 설정하고 쿠키 값을 빈 문자열로 설정합니다.

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개, JavaScript를 통해 액세스할 수 있는 일반 쿠키 1개)가 필요할 수 있습니다. 사용자가 오프라인 상태에서 로그아웃하려고 하는 경우 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 헤더로 제공되는 동일한 출처 페이지를 삭제합니다.
    • 하나 이상의 보안 HTTPS 전용 쿠키가 수정 또는 삭제되었습니다.
    • 페이지에서 실행한 XHR/fetch 호출에 대한 응답 1개 이상에 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.
        // ...
      }
    }
    

결론

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