iOS: аутентификация с использованием XMLHttpRequest - обработка ответа 401

Я пишу приложение для iOS с использованием PhoneGap (он же Cordova), у меня есть простая html-страница входа в систему, которая регистрирует пользователя с помощью XMLHttpRequest с базовой аутентификацией по SSL. Все работает великолепно, когда вы правильно вводите имя пользователя и пароль. Однако, если вы введете неправильное имя пользователя / пароль, ни один из моих обратных вызовов не будет вызван.

Если вы запускаете тот же код в Chrome, например, с неверным именем пользователя и паролем, chrome ведет себя аналогичным образом, за исключением того, что он вызывает диалоговое окно проверки подлинности. Нажатие кнопки "Отмена" в диалоговом окне Chrome возвращает управление моему JavaScript-коду. К сожалению, на iOS UIWebView даже не открывает диалоговое окно авторизации, оно просто зависает. Мне нужен способ сообщить пользователю, что он ввел неправильное имя пользователя или пароль, чтобы они могли повторить попытку.

Самым близким к ответу, который я смог найти, был этот http://www.freelock.com/2008/06/technical-note-http-auth-with-ajax но изменение статуса ответа с сервера не похоже на правильная вещь.

Вот как выглядит мой код запроса, но когда отправляется неверное имя пользователя или пароль, он никогда не достигает моего обратного вызова onload (фактически обратный вызов onreadystatechange вызывается только один раз, и это для readyState 1, иначе OPEN).

var req = new XMLHttpRequest();
req.onload = function(ev) {
    if (req.status == 401) {
        alert("Invalid Username/Password");
        document.getElementById('password').focus();
    } else if (req.status == 200) {
        window.location.href = some_secure_site;
    } else {
        // edit //
        alert("Some other status");
    }
}
req.onerror = function (ev) { alert('Error'); };
req.ontimeout = function(ev) { alert('Timeout'); };
req.open('GET', uri, true, userValue, passValue);
req.withCredentials = true;
req.send();

5 ответов

Решение

Несколько вещей стали очевидными для меня при попытке сделать это на iOS. Во-первых, в iOS есть ошибка, связанная с базовой аутентификацией, поэтому, если в вашем пароле есть определенные специальные символы, вы никогда не получите ответ от вашего сервера, потому что ваш сервер никогда не получит вызов аутентификации. То есть, если вы используете поле имени пользователя и пароля в методе "open".

Я предполагаю, что они делают что-то глупое, например отправляют это через http://username:password@myorigin.com/etc, когда им нужно использовать заголовки http и base64, кодирующие титры, вот так

req.setRequestHeader("Authorization", "Basic " + base64(username) + ':' + base64(password));

Другая вещь, которую я узнал, состоит в том, что Basic Auth не очень безопасен и подвержен миллиону и одной проблеме. Одна из причин, которая вас раздражает, заключается в том, что клиент будет кэшировать имя пользователя и пароль, которые будут переопределять любые новые значения, которые вы отправляете через "req.open(...)". Удачи вам в этом, используя только javascript, вы должны сделать что-то магическое в ObjC, чтобы очистить кеш.

Если у вас есть контроль над вашим сервером, я бы предложил использовать токен аутентификации. Соединитесь через SSL и затем отправьте POST с данными JSON, содержащими имя пользователя и пароль. Затем сервер может отправить обратно данные JSON с токеном аутентификации (по сути, набором случайных символов, достаточно длинных, чтобы его невозможно было угадать, UUID работает хорошо. Он генерируется сервером и может быть известен только клиенту и сервер). Затем сохраните токен и имя пользователя в цепочке для ключей, чтобы пользователю не нужно было вводить свои кредиты при каждом запуске приложения.

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

try {
    var req = new XMLHttpRequest();
    req.onload = function(ev) {
        var response = JSON.parse(this.responseText);
        if (response.success === true) {
            // The server will respond with a token that will allow us to login
            storeCredentials(userValue, response.token);
            // redirect with token
        else if (req.status == 401) {
            alert("Invalid Username/Password");
            document.getElementById('password').focus();
        } else {
            alert("Some other status");
        }
    }
    req.ontimeout = setTimeout(function(ev) { navigator.notification.alert('Timeout trying to contact the server'); }, 10000);
    req.onerror = function(ev) { clearTimeout(this.ontimeout); navigator.notification.alert('Error connecting to the server during authentication.'); };

    var uri = myWebOrigin + '/authenticate';
    req.open('POST', uri, true);
    req.setRequestHeader('Cache-Control', 'no-cache');
    req.setRequestHeader('Content-Type', 'application/json');
    json_data = {username : Base64.encode(userValue), password : Base64.encode(passValue)};
    req.send(JSON.stringify(json_data));
} catch(error) {
    navigator.notification.alert('Uh oh, an error occurred trying to login! ' + error);
    return;
}

Чтобы решить эту проблему, удалите заголовок WWW-Authenticate из ответа сервера.

У меня просто были те же проблемы, когда ни один из обратных вызовов не вызывался при использовании iOS + PhoneGap + jQuery. Если я передам неверные учетные данные и использовать

$.ajax({
    ...
    timeout: 5000, // Some timeout value that makes sense
    ...
});

тогда обратный вызов ошибки вызывается с {"readyState":0,"status":0,"statusText":"timeout"}, В этом случае вам придется угадывать, что настоящая ошибка - HTTP 401.

В качестве альтернативы вы можете использовать

$.ajax({
    ...
    async: false, // :-(
    ...
});

и ваша ошибка обратного вызова получит что-то вроде {"readyState":4,"responseText":"<html>...</html>","status":401,"statusText":"Unauthorized"} назад.

Возможно, есть другой код состояния HTTP рядом с 401 а также 200 коды получены! Убедитесь, что на самом деле не получен другой код состояния:

if (req.status == 401) {
    alert("Invalid Username/Password");
    document.getElementById('password').focus();
} else if (req.status == 200) {
    window.location.href = some_secure_site;
} else {
    alert('Unfetched status code '+req.status+' captured!');
}

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

Я предполагаю, что внутренне UIWebView получает запрос на ввод учетных данных, но это приглашение подавляется, что приводит к этой ошибке. Это основано на том, что все другие браузеры, которые я пробовал (Safari, Chrome, Firefox), не вызывают обратные вызовы в этом случае до тех пор, пока приглашение не будет отклонено.

Another solution (the one I'm using) would be to not use Javascript to do the authentication, and instead do it on the iOS side - you can use the code on How to display the Authentication Challenge in UIWebView? as a rough template for doing this.

Другие вопросы по тегам