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

Mustaq Ahmed
Joe Medley
Joe Medley

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

오늘날 주요 브라우저는 사용자 활성화가 activation-gated API를 제어하는 방식에 관해 매우 다른 동작을 보여줍니다. Chrome에서는 구현이 토큰 기반 모델을 기반으로 했지만, 이 모델은 모든 활성화 게이트 API에서 일관된 동작을 정의하기에는 너무 복잡했습니다. 예를 들어 Chrome은 postMessage()setTimeout() 호출을 통해 활성화 게이트 API에 대한 불완전한 액세스를 허용하고 있었습니다. 또한 Promises, XHR, Gamepad 상호작용 등으로 사용자 활성화가 지원되지 않았습니다. 이러한 버그 중 일부는 널리 알려져 있지만 오래된 버그입니다.

Chrome은 버전 72에 모든 활성화 게이트 API에 대한 사용자 활성화 가용성을 완료하는 User Activation 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의 일관성 예시

다음은 window.open()를 사용하여 열리는 팝업 창이 포함된 두 가지 예시로, User Activation v2가 활성화 게이트 API의 동작을 일관되게 만드는 방법을 보여줍니다.

체이닝된 setTimeout() 호출

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

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

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

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

User Activation 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);
});

User Activation v2가 없으면 상위 프레임은 두 번째 메시지를 수신할 때 팝업을 열 수 없습니다. 첫 번째 메시지가 다른 교차 출처 프레임에 '연쇄'되면 (즉, 첫 번째 수신자가 메시지를 다른 수신자에게 전달하는 경우) 첫 번째 메시지조차 실패합니다.

이는 원래 양식과 체이닝 모두에서 User Activation v2와 함께 작동합니다.