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

Mustaq Ahmed
조 메들리
조 메들리

악성 스크립트가 팝업, 전체 화면 등의 민감한 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에서 팝업을 엽니다 (100밀리초 지연).

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와 함께 작동합니다.