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

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

Суть проблемы в том, что стандартом HTTP Basic Authorization не предусмотрена возможность разлогина. Вообще. При заходе на страницу, защищённую Basic Authorization, браузер сам выводит вам своё собственное окно с запросом на логин/пароль и при успешном логине сохраняет их где-то у себя в глубинных недрах. Затем все последующие запросы к другим защищённым страницам данного сайта автоматически использует эти значения в заголовке Authorization:
Authorization: Basic bm9uZTpub25l
где bm9uZTpub25l — это base64-кодированная связка логин/пароль none:none (у вас логин и пароль, возможно, будут другие)

Для того, чтобы перелогиниться, нужно всего лишь сбросить значения старых логина/пароля в тех самых глубинных недрах браузера и на их место записать новые. Вот тут нас и поджидает сюрприз. Поскольку никакой стандарт не регламентирует это действо, каждый браузер творит это по своему собственному разумению.

Хуже того, поскольку окно запроса логина/пароля является собственным окном браузера, реагирующее уже на одни только HTTP заголовки страницы, это окно всплывает до какой бы то ни было обработки тела страницы. То есть выполнить какой-нибудь javascript при получении ответа сервера со статусом 401 не получится. Перед пользователем снова и снова будет вылезать это окно с повторным запросом на логин/пароль.

На самом деле этот момент критичен, так как почти для всех браузеров единственным способом разлогиниться является отправка на сервер запроса с заведомо неправильными логином/паролем, получение ответа сервера со статусом 401 и последующий сброс браузером своего логин/парольного кэша. Но если при этом браузер не даёт вам обработать ответ 401 и продолжить процесс логина уже для нового пользователя, перехватывая управление ещё на этапе чтения HTTP заголовков и выкидывания вам в лицо ту самую форму авторизации, в которую что ни вводи, работать не будет, то это уже проблема. Причём не имеет значения, обычный ли это запрос по ссылке или XMLHttpRequest или fetch. Браузер всё равно перехватывает управление на этапе разбора заголовков.

Итак…

Исходные данные:


  1. Есть отдельная страница logout.html, на которой в javascript скрипте находится вся логика, части которого приводятся по ходу изложения
  2. Задан url — адрес страницы для перенаправления после успешного логина
  3. Задан url401 — адрес страницы, всегда возвращающей HTTP ошибку 401 (не авторизован)

// file logout.html
const url = new URL("http://mysite.com/");
const url401 = new URL("http://mysite.com/401");

Internet Explorer


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

if (document.execCommand("ClearAuthenticationCache")) {
    window.location.assign(url);
}

В IE существует метод ClearAuthenticationCache, который сбрасывает «те самые глубинные недра». Просто и элегантно. И никаких плясок со вспомогательной страницей 401. Работает ли данный метод в Edge, не знаю. Скорее всего да.

Конструкция document.execCommand возвращает true, если метод существует и «сработал». После чего window.location.assign(url) перенаправляет пользователя для ввода новых логина и пароля

Firefox (72.0.1)


В контексте нашей задачи это самый проблемный браузер. Для него полноценного решения не существует до сих пор. В баг-трекере команды его разработчиков вот уже лет 15-20 висит запрос на указанную проблему. Но «воз и ныне там». Максимум чего можно добиться — это кривой разлогин.

Вводные данные:
Firefox не сбрасывает парольный кэш после получения ответа 401 ни через XMLHttpRequest, ни через fetch запрос. Только через обычный запрос с указанием логина/пароля в самом URL. То есть что-то вроде
http ://none:none@mysite.com/
Код:

else if (/firefox|iceweasel|fxios/i.test(window.navigator.userAgent)) {
    url.username = 'none';
    url.password = 'none';
    window.location.assign(url);
}

После чего пользователь получает форму ввода логина/пароля, в которую что ни вводи, она будет выскакивать вновь и вновь. Дело в том, что введённые значения не будут переопределять значения логина/пароля, заданные в URL-е. То есть вместо введённых значений на сервер всякий раз будет уходить связка none:none. И чтобы перелогиниться под другим именем, пользователь должен нажать отмену, перейти на стартовую страницу (http ://mysite.com/) и уже там ввести новые логин/пароль.

Криво? Но увы, другого решения нет

Google Chrome (79.0.3945.88)


Для Хрома замечательно работает метод fetch. А вот XMLHttpRequest не работает ((( Кэш не сбрасывается, и разлогина не происходит. Причём пробовал логин/пароль задавать и как параметрами к методу open, так и установкой заголовка.

else if (/chrome/i.test(window.navigator.userAgent)) {
    fetch(url401, {
      credentials: 'include',
      headers: {
        'Authorization': 'Basic ' + btoa('none:none'),
        'X-Requested-With': 'XMLHttpRequest'
      }
    }).then(() => {
      window.location.assign(url);
    }).catch(err => console.error(err));
}

Делаем fetch запрос на страницу 401 с заведомо неверными логином/паролем, получаем ответ 401 от сервера, браузер сбрасывает свой кэш.

ВАЖНО! Сервер НЕ должен возвращать заголовок WWW-Authenticate. Иначе браузер перехватит управление, и перенаправления со страницы 401 не произойдёт никогда. По общепринятому соглашению сервер не должен возвращать этот заголовок, если в запросе указано X-Requested-With: XMLHttpRequest. Поэтому в запрос добавлен заголовок X-Requested-With.

Safari (12)


Для Сафари ситуация в точности до наоборот: работает XMLHttpRequest, но не работает fetch

else {
    const xhr = new XMLHttpRequest();
    xhr.open("GET", url401, true, 'none', 'none');
    xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
    xhr.onerror = function(err) {
      console.error(err);
    };
    xhr.onload = function () {
      window.location.assign(url);
    };
    xhr.send()
}

Действия те же, что и в Хроме, только через XMLHttpRequest

Должно работать для версий Сафари 6+. В более ранних версиях были свои баги. В частности, например, в версиях от 5.1 при каждом перенаправлении браузер принудительно перезапрашивал авторизацию, из-за чего авторизация с перенаправлением на конечную страницу не работала в принципе. А в версиях до 5.0 не работал разлогин.