Как выйти из системы с помощью BASIC-аутентификации?

Можно ли выйти из системы с веб-сайта, если он использует обычную аутентификацию?

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

Пока единственное решение - закрыть браузер, но это неприемлемо с точки зрения удобства использования.

26 ответов

Решение

Обычная аутентификация не предназначена для управления выходом из системы. Вы можете сделать это, но не полностью автоматически.

Вам нужно сделать так, чтобы пользователь щелкнул ссылку выхода из системы и отправил "401 Unauthorized" в ответ, используя ту же область и тот же уровень URL-папки, что и обычный 401, который вы отправляете, запрашивая логин.

Они должны быть направлены на ввод неправильных учетных данных, например, пустое имя пользователя и пароль, и в ответ вы отправляете обратно страницу "Вы успешно вышли из системы". Неверные / пустые учетные данные затем перезапишут предыдущие правильные учетные данные.

Короче говоря, сценарий выхода из системы инвертирует логику сценария входа в систему, возвращая страницу успеха, только если пользователь не передает правильные учетные данные.

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

Отредактируйте, чтобы добавить в ответ на комментарий: повторный вход - это немного другая проблема (если, конечно, вам не требуется двухэтапный выход / выход из системы). Вы должны отклонить (401) первую попытку доступа к ссылке повторной регистрации, чем принять вторую (которая предположительно имеет другое имя пользователя / пароль). Есть несколько способов сделать это. Можно было бы включить текущее имя пользователя в ссылку выхода из системы (например, / relogin? Username) и отклонить, когда учетные данные соответствуют имени пользователя.

Попросите пользователя перейти по ссылке на https://log:out@example.com/. Это заменит существующие учетные данные недействительными; выходя из них.

Дополнение к ответу от bobince...

С Ajax вы можете привязать ссылку / кнопку "Выход" к функции Javascript. Пусть эта функция отправит XMLHttpRequest с неверным именем пользователя и паролем. Это должно вернуть 401. Затем установите document.location обратно на страницу перед входом в систему. Таким образом, пользователь никогда не увидит дополнительное диалоговое окно входа в систему и не должен будет вводить неверные учетные данные.

Вы можете сделать это полностью в JavaScript:

IE имеет (долгое время) стандартный API для очистки кэша базовой аутентификации:

document.execCommand("ClearAuthenticationCache")

Должен вернуть true, когда это работает. Возвращает либо false, undefined, либо взрывается в других браузерах.

Новые браузеры (по состоянию на декабрь 2012 года: Chrome, FireFox, Safari) имеют "магическое" поведение. Если они видят успешный базовый запрос авторизации с любым поддельным другим именем пользователя (скажем, logout) они очищают кэш учетных данных и, возможно, устанавливают его для этого нового поддельного имени пользователя, которое необходимо убедиться, что оно не является допустимым именем пользователя для просмотра содержимого.

Основной пример этого:

var p = window.location.protocol + '//'
// current location must return 200 OK for this GET
window.location = window.location.href.replace(p, p + 'logout:password@')

"Асинхронный" способ сделать это - сделать AJAX-вызов, используя logout имя пользователя. Пример:

(function(safeLocation){
    var outcome, u, m = "You should be logged out now.";
    // IE has a simple solution for it - API:
    try { outcome = document.execCommand("ClearAuthenticationCache") }catch(e){}
    // Other browsers need a larger solution - AJAX call with special user name - 'logout'.
    if (!outcome) {
        // Let's create an xmlhttp object
        outcome = (function(x){
            if (x) {
                // the reason we use "random" value for password is 
                // that browsers cache requests. changing
                // password effectively behaves like cache-busing.
                x.open("HEAD", safeLocation || location.href, true, "logout", (new Date()).getTime().toString())
                x.send("")
                // x.abort()
                return 1 // this is **speculative** "We are done." 
            } else {
                return
            }
        })(window.XMLHttpRequest ? new window.XMLHttpRequest() : ( window.ActiveXObject ? new ActiveXObject("Microsoft.XMLHTTP") : u ))
    }
    if (!outcome) {
        m = "Your browser is too old or too weird to support log out functionality. Close all windows and restart the browser."
    }
    alert(m)
    // return !!outcome
})(/*if present URI does not return 200 OK for GET, set some other 200 OK location here*/)

Вы также можете сделать это букмарклетом:

javascript:(function(c){var a,b="You should be logged out now.";try{a=document.execCommand("ClearAuthenticationCache")}catch(d){}a||((a=window.XMLHttpRequest?new window.XMLHttpRequest:window.ActiveXObject?new ActiveXObject("Microsoft.XMLHTTP"):void 0)?(a.open("HEAD",c||location.href,!0,"logout",(new Date).getTime().toString()),a.send(""),a=1):a=void 0);a||(b="Your browser is too old or too weird to support log out functionality. Close all windows and restart the browser.");alert(b)})(/*pass safeLocation here if you need*/);

Следующая функция подтверждена для Firefox 40, Chrome 44, Opera 31 и IE 11.
Bowser используется для обнаружения в браузере, также используется jQuery.

- secUrl - это URL защищенной паролем области, из которой можно выйти.
- redirUrl - это URL для незащищенной паролем области (страница успешного выхода из системы).
- Вы можете увеличить таймер перенаправления (в настоящее время 200 мс).

function logout(secUrl, redirUrl) {
    if (bowser.msie) {
        document.execCommand('ClearAuthenticationCache', 'false');
    } else if (bowser.gecko) {
        $.ajax({
            async: false,
            url: secUrl,
            type: 'GET',
            username: 'logout'
        });
    } else if (bowser.webkit) {
        var xmlhttp = new XMLHttpRequest();
        xmlhttp.open("GET", secUrl, true);
        xmlhttp.setRequestHeader("Authorization", "Basic logout");
        xmlhttp.send();
    } else {
        alert("Logging out automatically is unsupported for " + bowser.name
            + "\nYou must close the browser to log out.");
    }
    setTimeout(function () {
        window.location.href = redirUrl;
    }, 200);
}

Вот очень простой пример Javascript с использованием jQuery:

function logout(to_url) {
    var out = window.location.href.replace(/:\/\//, '://log:out@');

    jQuery.get(out).error(function() {
        window.location = to_url;
    });
}

Этот пользователь выходит из системы, не показывая ему окно входа в браузер, а затем перенаправляет его на страницу выхода

Это невозможно напрямую с Basic-Authentication.

В спецификации HTTP нет механизма для сервера, который бы указывал браузеру прекратить отправку учетных данных, которые пользователь уже представил.

Существуют "хаки" (см. Другие ответы), обычно включающие использование XMLHttpRequest для отправки HTTP-запроса с неверными учетными данными, чтобы перезаписать первоначально предоставленные.

Это на самом деле довольно просто.

Просто зайдите в браузер и используйте неверные учетные данные: http://username:password@yourdomain.com/

Это должно "выйти из системы".

Для записи есть новый заголовок HTTP-ответа, который называется Clear-Site-Data. Если ваш ответ сервера включаетClear-Site-Data: "cookies"заголовок, тогда необходимо удалить учетные данные для аутентификации (не только файлы cookie). Я тестировал его в Chrome 77, но на консоли отображается это предупреждение:

Clear-Site-Data header on 'https://localhost:9443/clear': Cleared data types:
"cookies". Clearing channel IDs and HTTP authentication cache is currently not
supported, as it breaks active network connections.

И учетные данные для авторизации не удаляются, поэтому (на данный момент) это не работает для реализации базового выхода из авторизации, но, возможно, в будущем будет. Не тестировал в других браузерах.

Ссылки:

https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Clear-Site-Data

https://www.w3.org/TR/clear-site-data/

https://github.com/w3c/webappsec-clear-site-data

https://caniuse.com/#feat=mdn-http_headers_clear-site-data_cookies

Это работает для IE/Netscape/Chrome:

      function ClearAuthentication(LogOffPage) 
  {
     var IsInternetExplorer = false;    

     try
     {
         var agt=navigator.userAgent.toLowerCase();
         if (agt.indexOf("msie") != -1) { IsInternetExplorer = true; }
     }
     catch(e)
     {
         IsInternetExplorer = false;    
     };

     if (IsInternetExplorer) 
     {
        // Logoff Internet Explorer
        document.execCommand("ClearAuthenticationCache");
        window.location = LogOffPage;
     }
     else 
     {
        // Logoff every other browsers
    $.ajax({
         username: 'unknown',
         password: 'WrongPassword',
             url: './cgi-bin/PrimoCgi',
         type: 'GET',
         beforeSend: function(xhr)
                 {
            xhr.setRequestHeader("Authorization", "Basic AAAAAAAAAAAAAAAAAAA=");
         },

                 error: function(err)
                 {
                    window.location = LogOffPage;
             }
    });
     }
  }


  $(document).ready(function () 
  {
      $('#Btn1').click(function () 
      {
         // Call Clear Authentication 
         ClearAuthentication("force_logout.html"); 
      });
  });          

Я только что протестировал следующее в Chrome (79), Firefox (71) и Edge (44), и он отлично работает. Он применяет решение сценария, как указано выше.

Просто добавьте ссылку "Выйти" и при нажатии верните следующий HTML-код.

    <div>You have been logged out. Redirecting to home...</div>    

<script>
    var XHR = new XMLHttpRequest();
    XHR.open("GET", "/Home/MyProtectedPage", true, "no user", "no password");
    XHR.send();

    setTimeout(function () {
        window.location.href = "/";
    }, 3000);
</script>

Все, что вам нужно, это перенаправить пользователя на некоторый URL-адрес выхода и вернуться 401 Unauthorized ошибка на нем. На странице ошибок (которая должна быть доступна без базовой аутентификации) вам нужно предоставить полную ссылку на вашу домашнюю страницу (включая схему и имя хоста). Пользователь перейдет по этой ссылке, и браузер снова запросит учетные данные.

Пример для Nginx:

location /logout {
    return 401;
}

error_page 401 /errors/401.html;

location /errors {
    auth_basic off;
    ssi        on;
    ssi_types  text/html;
    alias /home/user/errors;
}

Страница ошибки /home/user/errors/401.html:

<!DOCTYPE html>
<p>You're not authorised. <a href="<!--# echo var="scheme" -->://<!--# echo var="host" -->/">Login</a>.</p>

Добавьте это в ваше приложение:

@app.route('/logout')
def logout():
    return ('Logout', 401, {'WWW-Authenticate': 'Basic realm="Login required"'})

Отправка https://invalid_login@hostname отлично работает везде, кроме Safari на Mac (ну, Edge не проверял, но и там должен работать).

Выход из системы не работает в Safari, когда пользователь выбирает "запомнить пароль" во всплывающем окне базовой аутентификации HTTP. В этом случае пароль хранится в Keychain Access (Finder > Applications > Utilities > Keychain Access (или CMD+SPACE и введите "Keychain Access")). Отправкаhttps://invalid_login@hostnameне влияет на доступ к связке ключей, поэтому с этим флажком невозможно выйти из Safari на Mac. По крайней мере, у меня так работает.

MacOS Mojave (10.14.6), Safari 12.1.2.

Приведенный ниже код отлично работает в Firefox (73), Chrome (80) и Safari (12). Когда пользователь переходит на страницу выхода, код выполняется и удаляет учетные данные.

    //It should return 401, necessary for Safari only
    const logoutUrl = 'https://example.com/logout'; 
    const xmlHttp = new XMLHttpRequest();
    xmlHttp.open('POST', logoutUrl, true, 'logout');
    xmlHttp.send();

Также по какой-то причине Safari не сохраняет учетные данные во всплывающем окне базовой аутентификации HTTP, даже если выбран параметр "запомнить пароль". Остальные браузеры делают это правильно.

Вот как мой выход из системы работает с использованием формы:

  1. создать базовый выход пользователя с авторизацией с выходом из системы по паролю
  2. создайте папку logout/ и добавьте .htaccess: со строкой «требовать выхода пользователя из системы»
      RewriteEngine On
AuthType Basic
AuthName "Login"
AuthUserFile /mypath/.htpasswd
require user logout
  1. добавить кнопку выхода на сайт в виде формы, например:
      <form action="https://logout:logout@example.com/logout/" method="post">
   <button type="submit">Logout</button>
</form>
  1. logout/index.php может выглядеть примерно так:
      <?php
echo "LOGOUT SUCCESS";
header( "refresh:2; url=https://example.com" );
?>

5.9.2022 подтверждена работа в браузерах Chrome, Edge и Samsung Android.

 function logout(url){
    var str = url.replace("http://", "http://" + new Date().getTime() + "@");
    var xmlhttp;
    if (window.XMLHttpRequest) xmlhttp=new XMLHttpRequest();
    else xmlhttp=new ActiveXObject("Microsoft.XMLHTTP");
    xmlhttp.onreadystatechange=function()
    {
        if (xmlhttp.readyState==4) location.reload();
    }
    xmlhttp.open("GET",str,true);
    xmlhttp.setRequestHeader("Authorization","Basic xxxxxxxxxx")
    xmlhttp.send();
    return false;
}
function logout() {
  var userAgent = navigator.userAgent.toLowerCase();

  if (userAgent.indexOf("msie") != -1) {
    document.execCommand("ClearAuthenticationCache", false);
  }

  xhr_objectCarte = null;

  if(window.XMLHttpRequest)
    xhr_object = new XMLHttpRequest();
  else if(window.ActiveXObject)
    xhr_object = new ActiveXObject("Microsoft.XMLHTTP");
  else
    alert ("Your browser doesn't support XMLHTTPREQUEST");

  xhr_object.open ('GET', 'http://yourserver.com/rep/index.php', false, 'username', 'password');
  xhr_object.send ("");
  xhr_object = null;

  document.location = 'http://yourserver.com'; 
  return false;
}

Исходя из того, что я прочитал выше, я получил простое решение, которое работает в любом браузере:

1) на своей странице выхода вы вызываете ajax для своей учетной записи. Ваш бэкэнд должен принять пользователя из системы. Как только бэкэнд примет, браузер очищает текущего пользователя и принимает пользователя "выхода из системы".

$.ajax({
    async: false,
    url: 'http://your_login_backend',
    type: 'GET',
    username: 'logout'
});      

setTimeout(function () {
    window.location.href = 'http://normal_index';
}, 200);

2) Теперь, когда пользователь вернулся к обычному индексному файлу, он попытается автоматически войти в систему с пользователем "logout", в этот второй раз вы должны заблокировать его, ответив 401, чтобы вызвать диалог логина / пароля.

3) Есть много способов сделать это, я создал два бэкэнда входа в систему, один из которых принимает пользователя, а другой нет. Моя обычная страница входа использует ту, которая не принимает, моя страница выхода использует ту, которая ее принимает.

Тип chrome://restart в адресной строке Chrome со всеми приложениями, работающими в фоновом режиме, перезапустится и очистит кэш пароля Auth.

  • использовать идентификатор сессии (cookie)
  • сделать недействительным идентификатор сеанса на сервере
  • Не принимать пользователей с неверными идентификаторами сеансов

Этот JavaScript должен работать для всех браузеров последней версии:

//Detect Browser
var isOpera = !!window.opera || navigator.userAgent.indexOf(' OPR/') >= 0;
    // Opera 8.0+ (UA detection to detect Blink/v8-powered Opera)
var isFirefox = typeof InstallTrigger !== 'undefined';   // Firefox 1.0+
var isSafari = Object.prototype.toString.call(window.HTMLElement).indexOf('Constructor') > 0;
    // At least Safari 3+: "[object HTMLElementConstructor]"
var isChrome = !!window.chrome && !isOpera;              // Chrome 1+
var isIE = /*@cc_on!@*/false || !!document.documentMode; // At least IE6
var Host = window.location.host;


//Clear Basic Realm Authentication
if(isIE){
//IE
    document.execCommand("ClearAuthenticationCache");
    window.location = '/';
}
else if(isSafari)
{//Safari. but this works mostly on all browser except chrome
    (function(safeLocation){
        var outcome, u, m = "You should be logged out now.";
        // IE has a simple solution for it - API:
        try { outcome = document.execCommand("ClearAuthenticationCache") }catch(e){}
        // Other browsers need a larger solution - AJAX call with special user name - 'logout'.
        if (!outcome) {
            // Let's create an xmlhttp object
            outcome = (function(x){
                if (x) {
                    // the reason we use "random" value for password is 
                    // that browsers cache requests. changing
                    // password effectively behaves like cache-busing.
                    x.open("HEAD", safeLocation || location.href, true, "logout", (new Date()).getTime().toString())
                    x.send("");
                    // x.abort()
                    return 1 // this is **speculative** "We are done." 
                } else {
                    return
                }
            })(window.XMLHttpRequest ? new window.XMLHttpRequest() : ( window.ActiveXObject ? new ActiveXObject("Microsoft.XMLHTTP") : u )) 
        }
        if (!outcome) {
            m = "Your browser is too old or too weird to support log out functionality. Close all windows and restart the browser."
        }
        alert(m);
        window.location = '/';
        // return !!outcome
    })(/*if present URI does not return 200 OK for GET, set some other 200 OK location here*/)
}
else{
//Firefox,Chrome
    window.location = 'http://log:out@'+Host+'/';
}

Для всех, кто использует проверку подлинности Windows (также известную как проверка подлинности Negotiate, Kerberos или NTLM), я использую ASP.NET Core с Angular.

Я нашел эффективный способ смены пользователей!

Я изменяю свой метод входа в систему на стороне javascript следующим образом:

      protected login(changeUser: boolean = false): Observable<AuthInfo> {
  let params = new HttpParams();
  if(changeUser) {
    let dateNow = this.datePipe.transform(new Date(), 'yyyy-MM-dd HH:mm:ss');
    params = params.set('changeUser', dateNow!);
  }
  const url: string = `${environment.yourAppsApiUrl}/Auth/login`;
  return this.http.get<AuthInfo>(url, { params: params });
}

Вот мой метод на бэкэнде:

      [Route("api/[controller]")]
[ApiController]
[Produces("application/json")]
[Authorize(AuthenticationSchemes = NegotiateDefaults.AuthenticationScheme)]
public class AuthController : Controller
{
  [HttpGet("login")]
  public async Task<IActionResult> Login(DateTime? changeUser = null)
  {
      if (changeUser > DateTime.Now.AddSeconds(-3))
          return Unauthorized();

      ...
      ... (login process)
      ...

      return Ok(await _authService.GetToken());
    }
}

return Unauthorized()верните код 401, который вызывает появление всплывающего окна идентификации браузера, вот процесс:

  • Я передаю дату сейчас как параметр, если я хочу изменить пользователя.
  • Я возвращаю код 401, если с этого момента прошло не более 3 секунд Сейчас.
  • Я заполняю свои учетные данные, и тот же запрос с тем же параметром отправляется на серверную часть.
  • Поскольку прошло более 3 секунд, я продолжаю процесс входа в систему, но на этот раз с новыми учетными данными!

Как говорили другие, нам нужно получить тот же URL-адрес и отправить ошибку (например, 401: StatusUnauthorized что-то в этом роде), и все.

И я использую метод Get, чтобы сообщить ему, что мне нужно выйти,

Вот полный пример записи с помощью golang.

      package main

import (
    "crypto/subtle"
    "fmt"
    "log"
    "net/http"
)

func BasicAuth(username, password, realm string, handlerFunc http.HandlerFunc) http.HandlerFunc {

    return func(w http.ResponseWriter, r *http.Request) {
        queryMap := r.URL.Query()
        if _, ok := queryMap["logout"]; ok { // localhost:8080/public/?logout
            w.WriteHeader(http.StatusUnauthorized) // 401
            _, _ = w.Write([]byte("Success logout!\n"))
            return
        }

        user, pass, ok := r.BasicAuth()

        if !ok ||
            subtle.ConstantTimeCompare([]byte(user), []byte(username)) != 1 ||
            subtle.ConstantTimeCompare([]byte(pass), []byte(password)) != 1 {
            // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/WWW-Authenticate
            w.Header().Set("WWW-Authenticate", `Basic realm="`+realm+`", charset="UTF-8"`)
            w.WriteHeader(http.StatusUnauthorized)
            _, _ = w.Write([]byte("Unauthorised.\n"))
            return
        }

        handlerFunc(w, r)
    }
}

type UserInfo struct {
    name string
    psw  string
}

func main() {

    portNumber := "8080"
    guest := UserInfo{"guest", "123"}

    // localhost:8080/public/  -> ./public/everyone
    publicHandler := http.StripPrefix(
        "/public/", http.FileServer(http.Dir("./public/everyone")),
    )

    publicHandlerFunc := func(w http.ResponseWriter, r *http.Request) {
        switch r.Method {
        case http.MethodGet:
            publicHandler.ServeHTTP(w, r)
        /*
            case http.MethodPost:
            case http.MethodPut:
            case http.MethodDelete:
        */
        default:
            return
        }
    }

    http.HandleFunc("/public/",
        BasicAuth(guest.name, guest.psw, "Please enter your username and password for this site",
            publicHandlerFunc),
    )

    log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", portNumber), nil))
}

Когда вы уже вышли из системы, вам необходимо обновить страницу (F5). В противном случае вы можете увидеть старое содержимое.

Я обновил решение mthoring для современных версий Chrome:

function logout(secUrl, redirUrl) {
    if (bowser.msie) {
        document.execCommand('ClearAuthenticationCache', 'false');
    } else if (bowser.gecko) {
        $.ajax({
            async: false,
            url: secUrl,
            type: 'GET',
            username: 'logout'
        });
    } else if (bowser.webkit || bowser.chrome) {
        var xmlhttp = new XMLHttpRequest();
        xmlhttp.open(\"GET\", secUrl, true);
        xmlhttp.setRequestHeader(\"Authorization\", \"Basic logout\");\
        xmlhttp.send();
    } else {
// http://stackru.com/questions/5957822/how-to-clear-basic-authentication-details-in-chrome
        redirUrl = url.replace('http://', 'http://' + new Date().getTime() + '@');
    }
    setTimeout(function () {
        window.location.href = redirUrl;
    }, 200);
}

На самом деле я думаю, что базовая аутентификация предназначалась для использования со статическими страницами, а не для какого-либо сложного управления сеансами или страниц CGI.

Таким образом, если вы хотите управлять сеансом, вы должны разработать классическую «форму входа» для запроса пользователя и пароля (возможно, также 2-й фактор). Обработчик формы CGI должен преобразовывать успешную аутентификацию в сеанс (ID), который запоминается на сервере и (в файле cookie или как часть URI).

Затем выход из системы можно реализовать, просто заставив сервер (и клиент) «забыть» сеанс. Другое преимущество заключается в том, что (даже при шифровании) пользователь и пароль не отправляются с каждым запросом на сервер (вместо этого будет отправлен идентификатор сеанса).

Если идентификатор сеанса на сервере сочетается с меткой времени для «последнего выполненного действия», то тайм-аут сеанса может быть реализован путем сравнения этой метки времени с текущим временем: если промежуток времени слишком велик, «таймаут» сеанса, забыв идентификатор сеанса.

Любой запрос к недействительному сеансу приведет к перенаправлению на страницу входа (или, может быть, если вы хотите сделать ее более удобной, вы можете иметь «форму повторной проверки», которая также снова запрашивает пароль).

В качестве доказательства концепции я реализовал полное управление сеансом без файлов cookie, основанное исключительно на URI (идентификатор сеанса всегда является частью URI). Однако полный код будет слишком длинным для этого ответа.

Особое внимание следует уделить производительности, если вы хотите обрабатывать несколько тысяч одновременных сеансов.

    function logout(secUrl, redirUrl) {
        if (bowser.msie) {
            document.execCommand('ClearAuthenticationCache', 'false');
        } else if (bowser.gecko) {
            $.ajax({
                async: false,
                url: secUrl,
                type: 'GET',
                username: 'logout'
            });
        } else if (bowser.webkit) {
            var xmlhttp = new XMLHttpRequest();
            xmlhttp.open("GET", secUrl, true);
            xmlhttp.setRequestHeader("Authorization", "Basic logout");
            xmlhttp.send();
        } else {
            alert("Logging out automatically is unsupported for " + bowser.name
                + "\nYou must close the browser to log out.");
        }
        setTimeout(function () {
            window.location.href = redirUrl;
        }, 200);
    }
Другие вопросы по тегам