API 간에 사용자 활성화를 일관되게 만들기

Mustaq Ahmed
Joe Medley
Joe Medley

악성 스크립트가 팝업, 전체 화면 등 민감한 API를 악용하는 것을 방지하기 위해 브라우저는 사용자 활성화를 통해 이러한 API에 대한 액세스를 제어합니다. 사용자 활성화는 사용자 작업과 관련된 탐색 세션의 상태입니다. '활성' 상태는 일반적으로 사용자가 현재 페이지와 상호작용 중이거나 페이지 로드 이후 상호작용을 완료했음을 의미합니다. 사용자 동작은 같은 개념에 흔히 사용되지만 오해의 소지가 있는 용어입니다. 예를 들어 사용자의 스와이프나 획 돌리기 동작은 페이지를 활성화하지 않으므로 스크립트의 관점에서 사용자 활성화가 아닙니다.

오늘날 주요 브라우저는 사용자 활성화가 활성화 게이트 API를 제어하는 방법과 관련하여 매우 다른 동작을 보여줍니다. Chrome에서는 토큰 기반 모델을 기반으로 구현이 진행되었지만, 활성화가 제한된 모든 API에서 일관된 동작을 정의하기에는 너무 복잡한 것으로 판명되었습니다. 예를 들어 Chrome은 postMessage()setTimeout() 호출을 통해 활성화 제한 API에 대한 불완전한 액세스를 허용해 왔으며, 사용자 활성화는 프로미스, XHR, 게임패드 상호작용 등으로 지원되지 않았습니다. 이 중 일부는 인기 있지만 오래된 버그입니다.

버전 72에서 Chrome은 모든 활성화 제한이 적용된 API에 대해 사용자 활성화 가용성을 완성하는 사용자 활성화 v2를 제공합니다. 이렇게 하면 위에서 언급한 비일관성 (MessageChannels와 같은 몇 가지 기타 문제 포함)이 해결되어 사용자 활성화와 관련된 웹 개발을 쉽게 할 수 있습니다. 또한 새로운 구현은 장기적으로 모든 브라우저를 하나로 모으는 것을 목표로 하는 제안된 새 사양의 참조 구현을 제공합니다.

사용자 활성화 v2는 어떻게 작동하나요?

새 API는 프레임 계층 구조의 모든 window 객체에서 2비트 사용자 활성화 상태를 유지합니다. 이전 사용자 활성화 상태의 고정 비트 (프레임에서 사용자 활성화가 발생한 적이 있는 경우)와 현재 상태에 관한 일시적 비트(프레임에서 약 1초 이내에 사용자의 활성화가 확인된 경우)가 있습니다. 고정 비트는 설정된 후 프레임의 전체 기간 동안 재설정되지 않습니다. 일시적인 비트는 모든 사용자 상호작용에서 설정되며 만료 간격 (약 1초) 후에 또는 활성화를 사용하는 API 호출(예: window.open())을 통해 재설정됩니다.

활성화 제한 API마다 각기 다른 방식으로 사용자 활성화에 의존합니다. 새 API는 이러한 API별 동작을 변경하지 않습니다. 예를 들어 window.open()는 예전과 같이 사용자 활성화를 사용하고, 프레임 (또는 그 하위 프레임)에 사용자 작업이 표시된 적이 있는 경우 Navigator.prototype.vibrate()는 계속 유효하므로 사용자 활성화당 하나의 팝업만 허용됩니다.

변경되는 사항

  • 사용자 활성화 v2는 프레임 경계에 걸쳐 사용자 활성화 가시성 개념을 형식화합니다. 특정 프레임과의 사용자 상호작용은 이제 출처와 관계없이 포함된 모든 프레임 (및 이러한 프레임만)을 활성화합니다. (Chrome 72에는 모든 동일한 출처 프레임으로 가시성을 확장할 수 있는 임시 해결 방법이 있습니다. 사용자 활성화를 하위 프레임에 명시적으로 전달할 방법이 확보되면 이 해결 방법을 삭제할 예정입니다.)
  • 활성화 제한 API가 활성화된 프레임에서 호출되지만 이벤트 핸들러 코드 외부에서 호출되면 사용자 활성화 상태가 '활성'인 경우(예: 만료되거나 소비되지 않은 상태) 작동합니다. 사용자 활성화 v2 이전에는 무조건 실패했습니다.
  • 만료 시간 간격 내에 사용되지 않는 여러 사용자 상호작용은 마지막 상호작용에 해당하는 단일 활성화로 통합됩니다.

활성화 제한 API의 일관성 예시

다음은 사용자 활성화 v2가 활성화 제한 API의 동작을 일관되게 만드는 방식을 보여주는 팝업 창 (window.open()를 사용하여 열림)이 있는 두 가지 예입니다.

연결된 setTimeout() 호출

이 예는 setTimeout() 데모에서 가져온 것입니다. click 핸들러가 1초 이내에 팝업을 열려고 하면 코드가 지연을 '구성'하는 방식에 관계없이 성공할 것으로 예상됩니다. 사용자 활성화 v2는 이러한 기대치를 충족하므로 다음 이벤트 핸들러는 각각 click에서 팝업을 엽니다 (100ms 지연).

function popupAfter100ms() {
  setTimeout(callWindowOpen, 100);
}

function asyncPopupAfter100ms() {
  setTimeout(popupAfter100ms, 0);
}

someButton.addEventListener('click', popupAfter100ms);
someButton.addEventListener('click', asyncPopupAfter100ms);

사용자 활성화 v2가 없으면 테스트한 모든 브라우저에서 두 번째 이벤트 핸들러가 실패합니다. (첫 번째 항목도 경우에 따라 실패할 수 있습니다.)

교차 도메인 postMessage() 호출

다음은 postMessage() 데모의 예입니다. 교차 출처 하위 프레임의 click 핸들러가 두 개의 메시지를 상위 프레임으로 직접 전송한다고 가정해 보겠습니다. 다음 메시지 중 하나를 수신하면 상위 프레임에서 팝업을 열 수 있어야 합니다 (둘 다는 불가).

// Parent frame code
window.addEventListener('message', e => {
  if (e.data === 'open_popup' && e.origin === child_origin)
    window.open('about:blank');
});

// Child frame code:
someButton.addEventListener('click', () => {
  parent.postMessage('hi_there', parent_origin);
  parent.postMessage('open_popup', parent_origin);
});

사용자 활성화 v2가 없으면 두 번째 메시지를 수신할 때 상위 프레임이 팝업을 열 수 없습니다. 첫 번째 메시지가 다른 교차 출처 프레임에 '체이닝'되면 (즉, 첫 번째 수신자가 메시지를 다른 수신자에게 전달하는 경우) 실패합니다.

이는 사용자 활성화 v2에서 원래 형식과 체이닝 모두에서 작동합니다.