Улучшение обработки ошибок при проверке личности в Android WebView

Улучшение обработки ошибок при проверке личности в Android WebView

1. Предпосылки работы

Эта работа не была случаем разработки функции проверки личности с нуля, а представляла собой задачу сопровождения, включавшую анализ и исправление ошибки, возникшей в уже существующей функции. В рамках проекта веб-экраны, реализованные на Vue, отображались через общий WebView в приложении React Native, а проверка личности N*** CheckPlus уже была интегрирована в этот экран.

Функция нормально работала в веб-браузерах и приложении iOS, но аутентификация завершалась ошибкой только в Android WebView приложения React Native. Задача заключалась не в полном изменении существующей структуры интеграции, а в локализации причины с учётом различий между платформами и восстановлении процесса аутентификации при минимальном влиянии на другие функции общего WebView.

2. Возникновение ошибки и подсказки, полученные в ходе анализа

2.1 Подтверждённая ошибка

При выполнении проверки личности N*** в WebView Android-приложения аутентификация не завершалась, и пользователь перенаправлялся на страницу ошибки. В этот момент была зафиксирована следующая ошибка.

SecurityError: Failed to read a named property 'checkSuccess' from 'Window':
Blocked a frame with origin "https://n***.checkplus.co.kr"
from accessing a cross-origin frame.

Это сообщение означает, что попытка напрямую обратиться к свойству другого окна между окнами или фреймами из разных источников была заблокирована политикой одного источника. Однако, checkSuccessне была функцией, написанной непосредственно в проекте, а представляла собой имя, используемое внутри экрана аутентификации N***, поэтому подойти к проблеме через прямое изменение этой функции в коде Vue проекта было невозможно.

Кроме того, я не исходил из предположения, что исключение, отображённое на экране последним, было исходной причиной сбоя. Процесс аутентификации мог завершиться ошибкой на более раннем этапе, после чего во время выполнения последующего скрипта на странице ошибки могло возникнуть исключение Cross-Origin.

2.2 Сравнение сред выполнения и процессов навигации по URL

При сравнении одной и той же функции в разных средах выполнения она нормально работала в веб-браузерах, в обычном браузере на том же устройстве Android и в приложении iOS, но завершалась ошибкой только в Android WebView приложения React Native. Исходя из этого различия, я сначала изучил процесс обработки URL в Android WebView, а не сервис N*** в целом или бизнес-логику Vue.

В записях непосредственно перед сбоем был подтверждён следующий процесс навигации по URL.

/cert/mobileCert/main
> /cert/mobileCert/method
> /cert/mobileCert/fail/applink
> SecurityError 발생

SecurityErrorКлючевой подсказкой стало то, что перед этим этапом процесс проходил через путь /fail/applink. Одной этой записи было недостаточно, чтобы точно определить причину сбоя внутри N***, но необходимо было проверить, не завершился ли сначала ошибкой этап вызова внешнего приложения аутентификации, предшествовавший финальному исключению Cross-Origin.

3. Проверка существующего кода и сужение области поиска причины

3.1 Определение фактической точки внесения изменений

Поскольку проблема возникала только на Android, сначала я рассмотрел возможность изменения Java- или Kotlin-реализации WebViewClient. Однако Activity Android в проекте не создавала WebView напрямую; фактический экран был построен в общем компоненте TypeScript, который обёртывал react-native-webview.

Таким образом, точкой внесения изменений был общий компонент React Native WebView, а не Activity Android. Даже если ошибка возникает на Android, сначала необходимо определить, какой уровень проекта управляет нативной функциональностью, чтобы сократить область ненужных изменений.

3.2 Разделение существующих настроек и внутреннего процесса обратного вызова

В общем WebView уже были настроены originWhitelist={['*']}, JavaScript, DOM Storage, поддержка нескольких окон и параметры cookie, зависящие от платформы. Поскольку для Android уже присутствовали такие настройки, как thirdPartyCookiesEnabled, domStorageEnabled, и setSupportMultipleWindows, было сложно считать простой пропуск настройки cookie или хранилища основной причиной.

originWhitelist задаёт диапазон целей навигации, разрешённых WebView; он не отключает политику одного источника и не гарантирует запуск внешнего приложения. Поэтому вместо определения причины исключительно на основании настроек я сузил область поиска, учитывая, что в общем компоненте, который проверялся в тот момент, отсутствовал обработчик, явно выделяющий URL внешних приложений.

В проекте также существовал отдельный процесс, в котором страница результата всплывающего окна на ПК вызывала window.opener.callbackEncodeData(). В отличие от этого мобильное приложение отправляло форму с помощью _self и обрабатывало EncodeData на странице, на которую выполнялся возврат, тогда как имя функции, указанное в этой ошибке, было checkSuccess. Поэтому я не объединял проблему обратного вызова всплывающего окна на ПК и проблему вызова внешнего приложения в Android WebView, считая их имеющими одну и ту же причину.

Я также рассматривал изменение настроек нескольких окон, но не применял его в фактическом решении. В итоговое решение вошли только меры, нормальная работа которых была проверена.

3.3 Проверка обработки пользовательских схем внешних приложений

При запуске внешнего приложения аутентификации, такого как P***, в процессе проверки личности N*** URL не является обычным адресом http:// или https://адресом, а скорееintent: URI или tauthlink: можно использовать пользовательские схемы. В рабочих заметках того времени также сохранялся такой формат: tauthlink://sktauth?... за исключением конфиденциальных параметров.

Обычный мобильный браузер может передать такие URL операционной системе для запуска приложения, однако во встроенном WebView может потребоваться перехватывать запросы навигации по URL и передавать их в слой React Native или нативный слой.

В распространённом компоненте WebView, который проверялся в то время, имелись настройки, связанные с файлами cookie и окнами, но не было обработчика, явно отличающего URL приложения аутентификации от HTTP-URL и передающего их внешнему приложению. На основании результатов воспроизведения для конкретных платформ, записи навигации /fail/applink и пропусков, выявленных в существующем коде, мы сначала дополнили обработку URL внешнего приложения аутентификации на Android.

4. Применение решения

4.1 Критерии обработки

onShouldStartLoadWithRequestиспользовался для проверки запросов навигации по URL, возникающих во время аутентификации, и разделения запросов, которые должен продолжать обрабатывать WebView, и запросов внешних приложений, которые следует передавать операционной системе. Поскольку URL в этом случае был запросом навигации, возникающим в процессе аутентификации, а не первоначальной загрузкой, этот подход позволил решить проблему.

Критерии обработки были организованы следующим образом.

  • http://, https://, about:blankпродолжают обрабатываться WebView.

  • В качестве целей запуска внешнего приложения разрешены только проверенные на Android URL приложений аутентификации (intent:, tauthlink:).

  • Для URL, передаваемых внешнему приложению, возвращается falseчтобы WebView не загружал их повторно.

  • Схемы, не включённые в список разрешённых, не выполняются, а загрузка в WebView также прекращается.

  • На iOS этот обработчик не выполняет отдельный вызов Linking.

4.2 Основной код

Приведённый ниже код представляет собой упрощённый пример, призванный облегчить понимание основной структуры обработки, применявшейся в то время.

const handleShouldStartLoadWithRequest = (
  {url}: {url: string},
): boolean => {
  if (/^(https?:\/\/|about:blank(?:#.*)?$)/i.test(url)) return true;
  if (Platform.OS !== 'android') return true;

  const isIntentUrl = /^intent:/i.test(url);
  const isTAuthUrl = /^tauthlink:/i.test(url);
  if (!isIntentUrl && !isTAuthUrl) return false;

  const targetUrl = isIntentUrl ? parseIntentUrl(url) : url;
  if (!targetUrl) {
    showAuthAppError();
    return false;
  }

  void Linking.openURL(targetUrl).catch(showAuthAppError);
  return false;
};

Ключ заключается в том, чтобы отделить обычные веб-URL от разрешённых URL приложений аутентификации на Android, запросить запуск внешнего приложения с помощью Linking.openURL(), а затем остановить выполнение этой навигации WebView.

parseIntentUrl()— вспомогательная функция, которая формирует фактический URL схемы приложения из формата intent:, рассмотренного в то время. В заметках того времени также была отдельная ветка, проверявшая getFallbackUrl() для получения S.browser_fallback_url в случае сбоя запуска приложения. В приведённом примере сохранена только основная структура разделения навигации по URL; эти две функции не являются универсальными анализаторами всех URI Android Intent, поэтому область их применения следует ограничить форматами, проверенными в фактическом сервисе.

Для чистой пользовательской схемы tauthlink: может отсутствовать информация о пакете приложения или резервном URL. Поэтому решение о том, следует ли при сбое запуска показывать только уведомление либо управлять сопоставлениями схем и пакетов и перенаправлять пользователей в магазин, должно приниматься в рамках отдельной политики.

Если общий компонент передаёт внешние свойства WebView через ...rest, следует проверить, существует ли уже onShouldStartLoadWithRequest. Вместо простого перезаписывания одного обработчика другим в зависимости от порядка свойств безопаснее объединить результаты возврата существующего обработчика и общего обработчика.

5. Результаты повторного тестирования

После внесения изменений в то время мы повторно выполнили проверку личности N*** в среде Android, где ранее возникала ошибка. Обычная страница аутентификации https:// по-прежнему открывалась в WebView, тогда как пользовательская схема приложения аутентификации не загружалась WebView напрямую, а передавалась как запрос на запуск внешнего приложения. В результате вызовы внешних приложений аутентификации, таких как P***, и процесс проверки личности N*** продолжились, а существующий /fail/applink и SecurityErrorПоток сбоев, в котором они следовали один за другим, в том же сценарии больше не воспроизводился.

Этот результат не означает, что логика обработки пользовательских схем изменила политику одного источника. Поскольку вызов внешнего приложения выполнялся по обычному пути, процесс больше не переходил в ветку сбоя, где выполнялся последующий скрипт для /fail/applink, и поэтому последующее исключение Cross-Origin также не возникало.

Однако я не сделал вывод, что отсутствие обработки пользовательских схем было единственной внутренней причиной SecurityError. Причина заключалась в том, что я напрямую не проверил всю структуру, в которой внутри N*** вызывался checkSuccess. На тот момент из записей можно было подтвердить следующие факты.

  • Аутентификация завершалась с ошибкой только в WebView Android-приложения.

  • При сбое выполнялся доступ к /fail/applink а затем возникал SecurityError.

  • В использовавшемся на тот момент общем WebView отсутствовала логика, явно обрабатывающая URL внешнего приложения аутентификации.

  • После добавления этой обработки тот же сценарий аутентификации снова стал работать нормально.

Поэтому точнее резюмировать этот случай не как «ошибка была устранена за счёт обхода политики Cross-Origin», а как «после дополнения логики обработки URL внешнего приложения аутентификации в Android WebView предшествующий путь сбоя и последующее исключение Cross-Origin больше не воспроизводились в том же сценарии».

6. Соображения по реализации

Добавление обработчика навигации по URL в общий WebView может привести к тому, что через ту же логику будут проходить экраны, отличные от N***. Перенаправление каждого URL, не использующего HTTP, во внешнее приложение может вызвать непреднамеренный запуск приложений или изменить существующую функциональность, поэтому безопаснее ограничить область действия экранами аутентификации или состояниями выполнения аутентификации и разрешать только проверенные схемы.

intent: Если поддержку URI необходимо предоставить в общем виде, фактические внутренние схемы, пакеты и домены резервных URL также следует проверять по списку разрешённых значений. Если полный URL аутентификации записывается в журналы, он может содержать идентификаторы или токены, поэтому следует записывать только необходимую информацию, например схему и хост, а конфиденциальные параметры маскировать.

7. Полученный в ходе работы опыт

7.1 Необходимо изучать поток, предшествующий итоговой ошибке

Первым обнаруженным мной сообщением было Cross-Origin SecurityError, однако история навигации по URL показала, что до этого приложение перешло по пути /fail/applink. Если бы я попытался исправить непосредственно только итоговое исключение, то мог бы упустить признак того, что вызов внешнего приложения сначала завершился с ошибкой. При анализе инцидента необходимо также изучать навигацию по URL и изменения состояния до возникновения исключения.

7.2 Сравнение платформ может сузить область анализа

Тот факт, что проблема не возникала в веб-браузерах и приложении iOS, но проявлялась только в React Native Android WebView, позволил мне быстро сузить объект анализа. Сравнение обычного браузера и WebView приложения на одном и том же устройстве Android также помогло отделить различия самой операционной системы от различий в способе интеграции WebView.

7.3 Необходимо отличать реализованную меру от рассмотренных альтернатив

В ходе анализа я рассмотрел несколько вариантов, включая методы обработки окон и настройки нескольких окон. Однако ключевым изменением, которое, как было подтверждено, восстановило нормальную работу, стала обработка, отличавшая разрешённые URL внешних приложений в onShouldStartLoadWithRequest и передававшая их в Linking. При документировании технического случая разграничение мер, которые действительно были реализованы и проверены, и альтернатив, которые лишь рассматривались как возможные варианты, помогает не преувеличивать результаты.

8. Заключение

В этой работе анализировалась и исправлялась ошибка проверки личности N***, возникавшая только в Android WebView существующего гибридного приложения React Native. Хотя расследование началось с ошибки Cross-Origin, я последовательно изучил результаты воспроизведения на разных платформах, поток навигации по URL непосредственно перед сбоем, существующие настройки общего WebView и пользовательскую структуру обратного вызова, что позволило сузить анализ до обработки URL внешнего приложения аутентификации.

На основании записей того времени я дополнил реализацию, чтобы отличать обычные веб-URL от URL Android-приложений аутентификации и передавать разрешённые схемы в React Native Linking. После исправления вызов внешнего приложения аутентификации и проверка личности N*** в том же сценарии сбоя выполнялись нормально, а /fail/applink и последующий SecurityError больше не воспроизводились.

Этот случай научил меня, что вместо непосредственного исправления только последней ошибки, отображаемой на экране, важно проверить навигацию по URL перед возникновением ошибки и выяснить, какая обработка фактически отсутствовала на границе взаимодействия веба с нативным кодом. Также важно отделять подтверждённые факты от предположений, чтобы не преувеличивать выводы технического анализа.

зелёный

Site footer