Связь с аутентификацией серверов HTTP

В этой главе описываются, как взаимодействовать с аутентификацией серверов HTTP путем использования в своих интересах API CFHTTPAuthentication. Это объясняет, как найти соответствие объектов аутентификации и учетных данных, применить их к Запросу HTTP и сохранить их для более позднего использования.

В целом, если сервер HTTP возвращает 401 или 407 ответов после Вашего Запроса HTTP, это означает, что сервер аутентифицирует и требует учетных данных. В API CFHTTPAuthentication каждый набор учетных данных сохранен в объекте CFHTTPAuthentication. Поэтому каждый различный сервер аутентификации и каждый различный пользователь, соединяющийся с тем сервером, требуют отдельного объекта CFHTTPAuthentication. Для передачи с сервером необходимо применить объект CFHTTPAuthentication к Запросу HTTP. Эти шаги объяснены более подробно затем.

Обработка аутентификации

Добавление поддержки аутентификации позволит Вашему приложению говорить с аутентификацией серверов HTTP (если сервер возвратит 401 или 407 ответов). Даже при том, что Аутентификация HTTP не является трудным понятием, это - сложный процесс для выполнения. Процедура следующие:

  1. Клиент отправляет Запрос HTTP в сервер.

  2. Сервер возвращает вызов клиенту.

  3. Клиент связывает исходный запрос учетными данными и передает их обратно серверу.

  4. Согласование имеет место между клиентом и сервером.

  5. Когда сервер аутентифицировал клиент, он передает ответ обратно на запрос.

Выполнение этой процедуры требует многих шагов. Схема всей процедуры может быть замечена на рисунке 4-1 и рисунке 4-2.

Рисунок 4-1  , Обрабатывающий
Handling Authentication Flow-chart
authenticationFigure 4-2
  , Находящий объект аутентификации
Finding an Authentication Object Flow-chart

Когда Запрос HTTP возвращает 401 или 407 ответов, первый шаг для клиента для нахождения допустимого объекта CFHTTPAuthentication. Объект аутентификации содержит учетные данные и другую информацию, когда применено к запрос сообщения HTTP, проверяющую Ваши идентификационные данные с сервером. Если Вы уже аутентифицировали один раз с сервером, у Вас будет допустимый объект аутентификации. Однако в большинстве случаев необходимо будет создать этот объект из ответа с CFHTTPAuthenticationCreateFromResponse функция. См. Перечисление 4-1.

Перечисление 4-1  , Создающее объект аутентификации

if (!authentication) {
    CFHTTPMessageRef responseHeader =
        (CFHTTPMessageRef) CFReadStreamCopyProperty(
            readStream,
            kCFStreamPropertyHTTPResponseHeader
        );
 
    // Get the authentication information from the response.
    authentication = CFHTTPAuthenticationCreateFromResponse(NULL, responseHeader);
    CFRelease(responseHeader);
}

Если новый объект аутентификации допустим, то Вы сделаны и можете продолжать к второму шагу рисунка 4-1. Если объект аутентификации не допустим, то выбросьте объект аутентификации и учетные данные и проверку, чтобы видеть, были ли учетные данные плохи. Для получения дополнительной информации об учетных данных, считайте «Учетные данные Безопасности».

Плохие учетные данные означают, что сервер не принимал информацию о входе в систему, и это будет продолжать прислушиваться к новым учетным данным. Однако, если учетные данные были хороши, но сервер все еще отклонил Ваш запрос, то сервер отказывается говорить с Вами, таким образом, необходимо сдаться. Принятие учетных данных было плохо, повторяет это все начало процесса с создания объекта аутентификации, пока Вы не получаете рабочие учетные данные и допустимый объект аутентификации. В коде эта процедура должна быть похожей на тот в Перечислении 4-2.

Перечисление 4-2  , Находящее допустимый объект аутентификации

CFStreamError err;
if (!authentication) {
    // the newly created authentication object is bad, must return
    return;
 
} else if (!CFHTTPAuthenticationIsValid(authentication, &err)) {
 
    // destroy authentication and credentials
    if (credentials) {
        CFRelease(credentials);
        credentials = NULL;
    }
    CFRelease(authentication);
    authentication = NULL;
 
    // check for bad credentials (to be treated separately)
    if (err.domain == kCFStreamErrorDomainHTTP &&
        (err.error == kCFStreamErrorHTTPAuthenticationBadUserName
        || err.error == kCFStreamErrorHTTPAuthenticationBadPassword))
    {
        retryAuthorizationFailure(&authentication);
        return;
    } else {
        errorOccurredLoadingImage(err);
    }
}

Теперь, когда у Вас есть допустимый объект аутентификации, продолжайте после блок-схемы рисунок 4-1. Во-первых, определите, нужны ли Вам учетные данные. Если Вы не делаете, то применяете объект аутентификации к Запросу HTTP. Объект аутентификации применяется к Запросу HTTP в Перечислении 4-4 (resumeWithCredentials).

Не храня учетные данные (как объяснено в Хранении Учетных данных в Памяти и Хранении Учетных данных в Персистентном Хранилище), единственный способ получить допустимые учетные данные путем запроса пользователя. Большую часть времени, имя пользователя и пароль необходимы для учетных данных. Путем передачи аутентификации возражают против CFHTTPAuthenticationRequiresUserNameAndPassword функция, которую Вы видите, необходимо ли имя пользователя и пароль. Если учетным данным действительно нужно имя пользователя и пароль, предложите пользователю их и сохраните их в словаре учетных данных. Для сервера NTLM учетные данные также требуют домена. После того, как у Вас будут новые учетные данные, можно применить объект аутентификации к Запросу HTTP с помощью resumeWithCredentials функция от Перечисления 4-4. Этот целый процесс показан в Перечислении 4-3.

Перечисление 4-3  , Находящее учетные данные (если необходимый) и применение их

// ...continued from Listing 4-2
else {
    cancelLoad();
    if (credentials) {
        resumeWithCredentials();
    }
    // are a user name & password needed?
    else if (CFHTTPAuthenticationRequiresUserNameAndPassword(authentication))
        {
        CFStringRef realm = NULL;
        CFURLRef url = CFHTTPMessageCopyRequestURL(request);
 
         // check if you need an account domain so you can display it if necessary
        if (!CFHTTPAuthenticationRequiresAccountDomain(authentication)) {
            realm = CFHTTPAuthenticationCopyRealm(authentication);
        }
        // ...prompt user for user name (user), password (pass)
        // and if necessary domain (domain) to give to the server...
 
        // Guarantee values
        if (!user) user = CFSTR("");
        if (!pass) pass = CFSTR("");
 
        CFDictionarySetValue(credentials, kCFHTTPAuthenticationUsername, user);
        CFDictionarySetValue(credentials, kCFHTTPAuthenticationPassword, pass);
 
        // Is an account domain needed? (used currently for NTLM only)
        if (CFHTTPAuthenticationRequiresAccountDomain(authentication)) {
            if (!domain) domain = CFSTR("");
            CFDictionarySetValue(credentials,
                                 kCFHTTPAuthenticationAccountDomain, domain);
        }
        if (realm) CFRelease(realm);
        CFRelease(url);
    }
    else {
        resumeWithCredentials();
    }
}

Перечисление 4-4  , Применяющее аутентификацию, возражает против запроса

void resumeWithCredentials() {
    // Apply whatever credentials we've built up to the old request
    if (!CFHTTPMessageApplyCredentialDictionary(request, authentication,
                                                credentials, NULL)) {
        errorOccurredLoadingImage();
    } else {
        // Now that we've updated our request, retry the load
        loadRequest();
    }
}

Хранение учетных данных в памяти

Если Вы часто планируете связь с сервером аутентификации, может стоить снова использовать учетные данные, чтобы избежать предлагать пользователю имя пользователя и пароль сервера многократно. Этот раздел объясняет изменения, которые должны быть внесены в разовый код аутентификации использования (такой как в Обработке Аутентификации) для хранения учетных данных в памяти для повторного использования позже.

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

  1. Создайте непостоянный массив для содержания всех объектов аутентификации.

    CFMutableArrayRef authArray;

    вместо:

    CFHTTPAuthenticationRef authentication;
  2. Создайте отображение от объектов аутентификации до учетных данных с помощью словаря.

    CFMutableDictionaryRef credentialsDict;

    вместо:

    CFMutableDictionaryRef credentials;
  3. Поддержите эти структуры везде, Вы раньше изменяли текущий объект аутентификации и текущие учетные данные.

    CFDictionaryRemoveValue(credentialsDict, authentication);

    вместо:

    CFRelease(credentials);

Теперь, после создания Запроса HTTP, ищите соответствующий объект аутентификации перед каждой загрузкой. Простой, неоптимизированный метод для нахождения надлежащего объекта может быть замечен в Перечислении 4-5.

Перечисление 4-5  Ища соответствующий объект аутентификации

CFHTTPAuthenticationRef findAuthenticationForRequest {
    int i, c = CFArrayGetCount(authArray);
    for (i = 0; i < c; i ++) {
        CFHTTPAuthenticationRef auth = (CFHTTPAuthenticationRef)
                CFArrayGetValueAtIndex(authArray, i);
        if (CFHTTPAuthenticationAppliesToRequest(auth, request)) {
            return auth;
        }
    }
    return NULL;
}

Если массив аутентификации имеет соответствующий объект аутентификации, то проверьте хранилище учетных данных, чтобы видеть, доступны ли корректные учетные данные также. Выполнение так препятствует тому, чтобы Вы имели для запроса пользователя имя пользователя и пароль снова. Ищите учетные данные с помощью CFDictionaryGetValue функционируйте как показано в Перечислении 4-6.

Перечисление 4-6  , Ищущее хранилище учетных данных

credentials = CFDictionaryGetValue(credentialsDict, authentication);

Тогда примените свое соответствие объекта аутентификации и учетных данных к Вашему исходному Запросу HTTP и снова пошлите его.

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

Хранение учетных данных в персистентном хранилище

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

Хранение и получение учетных данных в цепочке для ключей требуют двух функций: один для нахождения словаря учетных данных для аутентификации и один для сохранения учетных данных нового запроса. Эти функции будут объявлены в этом документе как:

CFMutableDictionaryRef findCredentialsForAuthentication(
        CFHTTPAuthenticationRef auth);
 
void saveCredentialsForRequest(void);

Функция findCredentialsForAuthentication первые проверки, которые словарь учетных данных сохранил в памяти, чтобы видеть, кэшируются ли учетные данные локально. См. Перечисление 4-6 для того, как реализовать это.

Если учетные данные не кэшируются в памяти, то ищут цепочку для ключей. Для поиска цепочки для ключей используйте функцию SecKeychainFindInternetPassword. Эта функция требует большого количества параметров. Параметры и краткое описание того, как они используются с учетными данными Аутентификации HTTP:

keychainOrArray

NULL указать список цепочки для ключей пользователя по умолчанию.

serverNameLength

Длина serverName, обычно strlen(serverName).

serverName

Имя сервера проанализировано из Запроса HTTP.

securityDomainLength

Длина домена защиты, или 0 если нет никакого домена. В примере кода, realm ? strlen(realm) : 0 передается для учета обеих ситуаций.

securityDomain

Область объекта аутентификации, полученного из CFHTTPAuthenticationCopyRealm функция.

accountNameLength

Длина accountName. Начиная с accountName NULL, это значение 0.

accountName

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

pathLength

Длина path, или 0 если нет никакого пути. В примере кода, path ? strlen(path) : 0 передается для учета обеих ситуаций.

path

Путь от объекта аутентификации, полученного из CFURLCopyPath функция.

port

Номер порта, полученный из функции CFURLGetPortNumber.

protocol

Строка, представляющая тип протокола, такой как HTTP или HTTPS. Тип протокола получен путем вызова CFURLCopyScheme функция.

authenticationType

Тип аутентификации, полученный из функции CFHTTPAuthenticationCopyMethod.

passwordLength

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

passwordData

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

itemRef

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

Когда вызвано должным образом, код должен быть похожим на это в Перечислении 4-7.

Перечисление 4-7  , Ищущее цепочку для ключей

didFind =
    SecKeychainFindInternetPassword(NULL,
                                    strlen(host), host,
                                    realm ? strlen(realm) : 0, realm,
                                    0, NULL,
                                    path ? strlen(path) : 0, path,
                                    port,
                                    protocolType,
                                    authenticationType,
                                    0, NULL,
                                    &itemRef);

Принятие этого SecKeychainFindInternetPassword возвраты успешно, создайте список атрибутов цепочки для ключей (SecKeychainAttributeList) содержа единственный атрибут цепочки для ключей (SecKeychainAttribute). Список атрибутов цепочки для ключей будет содержать имя пользователя и пароль. Для загрузки списка атрибутов цепочки для ключей вызовите функцию SecKeychainItemCopyContent и передайте его ссылочный объект элемента цепочки для ключей (itemRef) это было возвращено SecKeychainFindInternetPassword. Эта функция заполнит атрибут цепочки для ключей именем пользователя учетной записи и a void ** как его пароль.

Имя пользователя и пароль может тогда использоваться для создания нового набора учетных данных. Перечисление 4-8 показывает эту процедуру.

Перечисление 4-8  , Загружающее учетные данные сервера из цепочки для ключей

if (didFind == noErr) {
 
    SecKeychainAttribute     attr;
    SecKeychainAttributeList attrList;
    UInt32                   length;
    void                     *outData;
 
    // To set the account name attribute
    attr.tag = kSecAccountItemAttr;
    attr.length = 0;
    attr.data = NULL;
 
    attrList.count = 1;
    attrList.attr = &attr;
 
    if (SecKeychainItemCopyContent(itemRef, NULL, &attrList, &length, &outData)
        == noErr) {
 
        // attr.data is the account (username) and outdata is the password
        CFStringRef username =
            CFStringCreateWithBytes(kCFAllocatorDefault, attr.data,
                                    attr.length, kCFStringEncodingUTF8, false);
        CFStringRef password =
            CFStringCreateWithBytes(kCFAllocatorDefault, outData, length,
                                    kCFStringEncodingUTF8, false);
        SecKeychainItemFreeContent(&attrList, outData);
 
        // create credentials dictionary and fill it with the user name & password
        credentials =
            CFDictionaryCreateMutable(NULL, 0,
                                      &kCFTypeDictionaryKeyCallBacks,
                                      &kCFTypeDictionaryValueCallBacks);
        CFDictionarySetValue(credentials, kCFHTTPAuthenticationUsername,
                             username);
        CFDictionarySetValue(credentials, kCFHTTPAuthenticationPassword,
                             password);
 
        CFRelease(username);
        CFRelease(password);
    }
    CFRelease(itemRef);
}

Если можно сохранить учетные данные в цепочке для ключей сначала, получение учетных данных от цепочки для ключей только полезно. Шаги очень подобны загружающимся учетным данным. Во-первых, посмотрите, сохранены ли учетные данные уже в цепочке для ключей. Вызвать SecKeychainFindInternetPassword, но передайте имя пользователя для accountName и длина accountName для accountNameLength.

Если запись существует, измените ее для изменения пароля. Установите data поле атрибута цепочки для ключей для содержания имени пользователя, так, чтобы Вы изменили корректный атрибут. Тогда вызовите функцию SecKeychainItemModifyContent и передайте ссылочный объект элемента цепочки для ключей (itemRef), список атрибутов цепочки для ключей и новый пароль. Путем изменения записи цепочки для ключей вместо того, чтобы перезаписать его, будет должным образом обновлена запись цепочки для ключей, и любые связанные метаданные будут все еще сохранены. Запись должна быть похожей на тот в Перечислении 4-9.

Перечисление 4-9  , Изменяющее запись цепочки для ключей

// Set the attribute to the account name
attr.tag = kSecAccountItemAttr;
attr.length = strlen(username);
attr.data = (void*)username;
 
// Modify the keychain entry
SecKeychainItemModifyContent(itemRef, &attrList, strlen(password),
                             (void *)password);

Если запись не будет существовать, то необходимо будет создать ее с нуля. Функция SecKeychainAddInternetPassword выполняет эту задачу. Его параметры совпадают с SecKeychainFindInternetPassword, но в отличие от вызова к SecKeychainFindInternetPassword, Вы предоставляете SecKeychainAddInternetPassword и имя пользователя и пароль. Выпустите ссылочный объект элемента цепочки для ключей после успешного вызова к SecKeychainAddInternetPassword если Вы не должны использовать его для чего-то еще. Посмотрите вызов функции в Перечислении 4-10.

Перечисление 4-10  , Хранящее новую запись цепочки для ключей

SecKeychainAddInternetPassword(NULL,
                               strlen(host), host,
                               realm ? strlen(realm) : 0, realm,
                               strlen(username), username,
                               path ? strlen(path) : 0, path,
                               port,
                               protocolType,
                               authenticationType,
                               strlen(password), password,
                               &itemRef);

Аутентификация брандмауэров

Аутентификация брандмауэров очень подобна серверам аутентификации за исключением того, что каждый неработающий Запрос HTTP должен быть проверен и на аутентификацию прокси и на аутентификацию сервера. Это означает, что Вы должны разделить хранилища (и локальный и персистентный) для серверов источника и прокси-серверов. Таким образом процедура для неработающего ответа HTTP теперь будет:

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

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