CFNetwork(四)

Communicating with Authenticating HTTP Servers

@官方文档翻译-李冰

@译文

本章介绍如何通过利用CFHTTPAuthentication API与验证HTTP服务器进行交互。它阐述了如何找到一个相匹配的认证对象和证书,将它们应用于一个HTTP请求,并保存它们供以后使用。

一般,如果一个HTTP服务器响应返回401或407响应你的请求后,这意味着服务器正在认证并需要证书。在CFHTTPAuthentication API中,任一证书集保存在CFHTTPAuthentication对象中。因此,每一个不同的认证服务器和每一个不同的用户与服务器的连接都要求单独的CFHTTPAuthentication对象。要与服务器的通信,你需要将你的CFHTTPAuthentication对象应用HTTP请求。接下来更详细的解释这些步骤。

Handling Authentication

添加对身份验证的支持将允许您的应用程序与身份验证的HTTP服务器通信(如果服务器返回401或407响应)。尽管HTTP认证不是一个很难的概念,但是它是一个复杂的过程。程序如下:

1.客户端发送HTTP请求到服务器
2.服务器返回一个质询
3.客户端使用证书捆绑原始请求,并将其发送回给服务器
4.在客户端和服务器之间进行协商。
5.当服务器验证了客户端,它将返回响应给这一次请求。

执行这个过程需要多个步骤。整个过程的示意图如图4-1和图4-2所示。

图 4-1 处理认证

apply_2x.png

Figure 4-2 查找认证对象

authentication_2x.png

当一个HTTP请求返回401或407响应时,首先在客户端上查找一个有效的CFHTTPAuthentication对象。一个认证对象包含了证书和其他信息,当应用于一个HTTP消息请求时,将验证你与服务器的身份。如果你已经通过服务器认证一次,你将拥有一个有效的身份认证对象。但是,在大多是情况下,你需要使用CFHTTPAuthenticationCreateFromResponse函数从响应中创建这个对象。见表4-1。

注意:有关身份验证的所有示例代码都从ImageClient应用程序改写的

表 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的第二步。如果认证对象无效,则丢弃证书对象和证书并检查证书是否损坏。
更多有关证书的详细信息, 请阅读 "Security Credentials".

错误证书意味着服务器不接受登录信息并且将继续监听新的证书。
但是,如果证书正确但是服务器仍拒绝你的请求,则服务器拒绝与你通信,所以你必须放弃。

假定证书错误,请从开始创建证书对象重试整个过程直到获得可工作的证书和有效的认证对象。在代码中,这一过程看起来像表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)。

不用存储证书 (如下文中Keeping Credentials in Memory 和 Keeping Credentials in a Persistent Store所诉), 获取有效证书唯一方法是提示用户。大多数时候,证书需要用户名和密码。 通过将认证对象传入CFHTTPAuthenticationRequiresUserNameAndPassword方法,你可以知道是否需要用户名和密码。如果证书确实需要用户名和密码,请提示用户提供并将其存储在证书字典中。对于NTLM服务器来说,证书还需要一个域。当你拥有一个新的证书,你可以使用resumeWithCredentials函数将认证对象应用于HTTP请求如表4-4。这所有的过程如表4-3所示。

提示: 在代码列表中,当注释以省略号开头和结束时,意味着该操作不在本文档的范围内,但需要实现。 这与描述正在发生什么操作的正常注释不同。

表 4-3 查找证书(如果有必要)并应用证书 Finding

// ...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();
    }
}

Keeping Credentials in Memory

如果你计划经常与一个认证服务器通信,可能值得重复使用证书避免多次提示输入服务器的账号密码。本节介绍了使用一次认证代码(比如握手认证)存储证书到内存以便随后使用的变化。

要再次使用证书,你需要对你代码进行三种数据结构更改。

1.创建一个可变数组存储所有的认证对象。

CFMutableArrayRef authArray;

instead of:

CFHTTPAuthenticationRef authentication;

2.创建一个字典用于 认证对象到证书的映射。

CFMutableDictionaryRef credentialsDict;

instead of:

CFMutableDictionaryRef credentials;

3.在您用于修改当前认证对象和当前凭据的任何位置维护这些结构。

CFDictionaryRemoveValue(credentialsDict, authentication);

instead of:

CFRelease(credentials);

现在,当创建HTTP请求后,在每次加载之前查找一个匹配的认证对象。在表4-5中可以看到一个用于查找适当对象的简单,未优化的方法。

表 4-5 Looking for a matching authentication object

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 Searching the credentials store

credentials = CFDictionaryGetValue(credentialsDict, authentication);

然后将匹配的认证对象和证书应用到你的HTTP原始请求并重新发送。

警告: 在接收到服务器质询前不要讲证书应用到HTTP请求。服务器自上次验证后可能已经更改,你可能会创造安全风险。

通过这些更改,你的应用程序将能够去在内存中存储认证对象和证书供稍后使用。

Keeping Credentials in a Persistent Store

将证书存储在内存中可防止用户在特定应用程序启动期间重新输入服务器的用户名和密码。但是,当应用程序退出后,这些证书将被释放。为了防止证书丢失,保存证书到持久存储中以便每一个服务器的证书只需要生成一次。建议使用钥匙串存储证书。即时你有许多钥匙串,本文档将默认的钥匙串称为钥匙串。使用钥匙串意味着你存储的认证信息也可以在尝试访问同一应用服务器的其他应用程序中使用,反之亦然。

在钥匙串中存储和检索证书需要两个函数:一个是用于查找认证的证书字典,另一个是保存最近请求的证书。这些函数将在本文档中声明为:

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

findCredentialsForAuthentication函数首先检查存储在内存中的证书字典,以查看证书是否本地缓存。见表4-6如何实现这些。

如果证书未在内存中缓存,则搜索钥匙串。使用h SecKeychainFindInternetPassword函数搜索钥匙串。这个函数需要大量的参数。参数以及如何用于HTTP认证证书的简短说明如下:

keychainOrArray
NULL 指定用户的默认钥匙串列表。
serverNameLength
serverName的长度, 通常是 strlen(serverName).
serverName
从HTTP请求解析的服务器名
securityDomainLength
安全域的长度, 如果没有安全域则为0。 在示例代码中, realm ? strlen(realm) : 0 被传入用来考虑两种情况。
securityDomain
认证对象的范围, 从 CFHTTPAuthenticationCopyRealm 函数中获得。
accountNameLength
accountName的长度 。因为 accountNameNULL, 这个值是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 Searching the keychain

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 返回成功, 创建包含一个钥匙串属性(SecKeychainAttribute)的钥匙串属性列表 (SecKeychainAttributeList)。钥匙串属性列表将包含用户名和密码。调用函数SecKeychainItemCopyContent并且传入SecKeychainFindInternetPassword返回的钥匙串引用对象(itemRef)去加载钥匙串属性列表。这个函数将用用户账户名和一个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,但需要传入用户名accountNameaccountName的长度accountNameLength

如果条目存在,修改条目来更改密码。设置钥匙串的data字段来包含用户名,以便你修改正确的属性。随后调用函数SecKeychainItemModifyContent并传入钥匙串引用对象(itemRef),钥匙串属性列表,和新的密码。
通过修改钥匙串条目而不是覆盖它,钥匙串条目将被适当地更新,并且任何相关联的元数据将仍然被保留。该条目应类似于表4-9中的条目。

表 4-9 Modifying the keychain entry

// 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 Storing a new keychain entry

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);

Authenticating Firewalls

验证防火墙非常类似于验证服务器,除了每个失败的HTTP请求都必须检查代理验证和服务器验证。这意味着您需要为代理服务器和源服务器单独存储(本地和持久)。 因此,失败的HTTP响应的过程现在将是:

  • 确定响应的状态代码是否为407(代理质询)。 如果是,则通过检查本地代理存储和持久代理存储来查找匹配的认证对象和凭据。 如果两者都不具有匹配的对象和凭证,则请求用户的凭证。 将身份验证对象应用于HTTP请求,然后重试。
  • 确定响应状态代码是否为401(服务器质询)。 如果是,请按照与407响应相同的过程,但使用原始服务器存储。

使用代理服务器时,还要强制执行一些细微的差异。 第一个是钥匙串调用的参数来自代理主机和端口,而不是来自源服务器的URL。 第二个是当向用户请求用户名和密码时,确保提示清楚地说明了密码是什么。

按照这些说明,您的应用程序应该能够使用验证防火墙。


官方文档

Communicating with Authenticating HTTP Servers

This chapter describes how to interact with authenticating HTTP servers by taking advantage of the CFHTTPAuthentication API. It explains how to find matching authentication objects and credentials, apply them to an HTTP request, and store them for later use.

In general, if an HTTP server returns a 401 or 407 response following your HTTP request, it means that the server is authenticating and requires credentials. In the CFHTTPAuthentication API, each set of credentials is stored in a CFHTTPAuthentication object. Therefore, every different authenticating server and every different user connecting to that server requires a separate CFHTTPAuthentication object. To communicate with the server, you need to apply your CFHTTPAuthentication object to the HTTP request. These steps are explained in more detail next.

Handling Authentication

Adding support for authentication will allow your application to talk with authenticating HTTP servers (if the server returns a 401 or 407 response). Even though HTTP authentication is not a difficult concept, it is a complicated process to execute. The procedure is as follows:

  1. The client sends an HTTP request to the server.
  2. The server returns a challenge to the client.
  3. The client bundles the original request with credentials and sends them back to the server.
  4. A negotiation takes place between the client and server.
  5. When the server has authenticated the client, it sends back the response to the request.

Performing this procedure requires a number of steps. A diagram of the entire procedure can be seen in Figure 4-1 and Figure 4-2.

Figure 4-1 Handling authentication

apply_2x.png

Figure 4-2 Finding an authentication object

authentication_2x.png

When an HTTP request returns a 401 or 407 response, the first step is for the client to find a valid CFHTTPAuthentication object. An authentication object contains credentials and other information that, when applied to an HTTP message request, verifies your identity with the server. If you've already authenticated once with the server, you will have a valid authentication object. However, in most cases, you will need to create this object from the response with the CFHTTPAuthenticationCreateFromResponse function. See Listing 4-1.

Note:All the sample code regarding authentication is adapted from the ImageClient application.

Listing 4-1 Creating an authentication object

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

If the new authentication object is valid, then you are done and can continue to the second step of Figure 4-1. If the authentication object is not valid, then throw away the authentication object and credentials and check to see if the credentials were bad. For more information about credentials, read "Security Credentials".

Bad credentials mean that the server did not accept the login information and it will continue to listen for new credentials. However, if the credentials were good but the server still rejected your request, then the server is refusing to speak with you, so you must give up. Assuming the credentials were bad, retry this entire process beginning with creating an authentication object until you get working credentials and a valid authentication object. In code, this procedure should look like the one in Listing 4-2.

Listing 4-2 Finding a valid authentication object

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);
    }
}

Now that you have a valid authentication object, continue following the flowchart in Figure 4-1. First, determine whether you need credentials. If you don't, then apply the authentication object to the HTTP request. The authentication object is applied to the HTTP request in Listing 4-4 (resumeWithCredentials).

Without storing credentials (as explained in Keeping Credentials in Memory and Keeping Credentials in a Persistent Store), the only way to obtain valid credentials is by prompting the user. Most of the time, a user name and password are needed for the credentials. By passing the authentication object to the CFHTTPAuthenticationRequiresUserNameAndPassword function you can see if a user name and password are necessary. If the credentials do need a user name and password, prompt the user for them and store them in the credentials dictionary. For an NTLM server, the credentials also require a domain. After you have the new credentials, you can apply the authentication object to the HTTP request using the resumeWithCredentials function from Listing 4-4. This whole process is shown in Listing 4-3.

Note: In code listings, when comments are preceded and succeeded by ellipses, it means that that action is outside the scope of this document, but does need to be implemented. This is different from normal comments which describe what action is taking place.

Listing 4-3 Finding credentials (if necessary) and applying them

// ...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();
    }
}

Listing 4-4 Applying the authentication object to a request

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();
    }
}

Keeping Credentials in Memory

If you plan on communicating with an authenticating server often, it may be worth reusing credentials to avoid prompting the user for the server's user name and password multiple times. This section explains the changes that should be made to one-time use authentication code (such as in Handling Authentication) to store credentials in memory for reuse later.

To reuse credentials, there are three data structure changes you need to make to your code.

  1. Create a mutable array to hold all the authentication objects.
CFMutableArrayRef authArray;

instead of:

CFHTTPAuthenticationRef authentication;
  1. Create a mapping from authentication objects to credentials using a dictionary.
CFMutableDictionaryRef credentialsDict;

instead of:

CFMutableDictionaryRef credentials;
  1. Maintain these structures everywhere you used to modify the current authentication object and the current credentials.
CFDictionaryRemoveValue(credentialsDict, authentication);

instead of:

CFRelease(credentials);

Now, after creating the HTTP request, look for a matching authentication object before each load. A simple, unoptimized method for finding the appropriate object can be seen in Listing 4-5.

Listing 4-5 Looking for a matching authentication object

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;
}

If the authentication array has a matching authentication object, then check the credentials store to see if the correct credentials are also available. Doing so prevents you from having to prompt the user for a user name and password again. Look for the credentials using the CFDictionaryGetValue function as shown in Listing 4-6.

Listing 4-6 Searching the credentials store

credentials = CFDictionaryGetValue(credentialsDict, authentication);

Then apply your matching authentication object and credentials to your original HTTP request and resend it.

Warning: Do not apply credentials to the HTTP request before receiving a server challenge. The server may have changed since the last time you authenticated and you could create a security risk.

With these changes, you application will be able to store authentication objects and credentials in memory for use later.

Keeping Credentials in a Persistent Store

Storing credentials in memory prevents a user from having to reenter a server's user name and password during that specific application launch. However, when the application quits, those credentials will be released. To avoid losing the credentials, save them in a persistent store so each server's credentials need to be generated only once. A keychain is the recommended place for storing credentials. Even though you can have multiple keychains, this document refers to the user's default keychain as the keychain. Using the keychain means that the authentication information that you store can also be used in other applications trying to access the same server, and vice versa.

Storing and retrieving credentials in the keychain requires two functions: one for finding the credentials dictionary for authentication and one for saving the credentials of the most recent request. These functions will be declared in this document as:

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

The function findCredentialsForAuthentication first checks the credentials dictionary stored in memory to see whether the credentials are cached locally. See Listing 4-6 for how to implement this.

If the credentials are not cached in memory, then search the keychain. To search the keychain, use the function SecKeychainFindInternetPassword. This function requires a large number of parameters. The parameters, and a short description of how they are used with HTTP authentication credentials, are:

keychainOrArray
NULL to specify the user's default keychain list.
serverNameLength
The length of serverName, usually strlen(serverName).
serverName
The server name parsed from the HTTP request.
securityDomainLength
The length of security domain, or 0 if there is no domain. In the sample code, realm ? strlen(realm) : 0 is passed to account for both situations.
securityDomain
The realm of the authentication object, obtained from the CFHTTPAuthenticationCopyRealm function.
accountNameLength
The length of accountName. Since the accountName is NULL, this value is 0.
accountName
There is no account name when fetching the keychain entry, so this should be NULL.
pathLength
The length of path, or 0 if there is no path. In the sample code, path ? strlen(path) : 0 is passed to account for both situations.
path
The path from the authentication object, obtained from the CFURLCopyPath function.
port
The port number, obtained from the function CFURLGetPortNumber.
protocol
A string representing the protocol type, such as HTTP or HTTPS. The protocol type is obtained by calling the CFURLCopyScheme function.
authenticationType
The authentication type, obtained from the function CFHTTPAuthenticationCopyMethod.
passwordLength
0, because no password is necessary when fetching a keychain entry.
passwordData
NULL, because no password is necessary when fetching a keychain entry.
itemRef
The keychain item reference object, SecKeychainItemRef, returned upon finding the correct keychain entry
When called properly, the code should look like that in Listing 4-7.

Listing 4-7 Searching the keychain

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

Assuming that SecKeychainFindInternetPassword returns successfully, create a keychain attribute list (SecKeychainAttributeList) containing a single keychain attribute (SecKeychainAttribute). The keychain attribute list will contain the user name and password. To load the keychain attribute list, call the function SecKeychainItemCopyContent and pass it the keychain item reference object (itemRef) that was returned by SecKeychainFindInternetPassword. This function will fill the keychain attribute with the account's user name, and a void ** as its password.

The user name and password can then be used to create a new set of credentials. Listing 4-8 shows this procedure.

Listing 4-8 Loading server credentials from the keychain

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);
}

Retrieving credentials from the keychain is only useful if you can store credentials in the keychain first. The steps are very similar to loading credentials. First, see if the credentials are already stored in the keychain. Call SecKeychainFindInternetPassword, but pass the user name for accountName and the length of accountName for accountNameLength.

If the entry exists, modify it to change the password. Set the data field of the keychain attribute to contain the user name, so that you modify the correct attribute. Then call the function SecKeychainItemModifyContent and pass the keychain item reference object (itemRef), the keychain attribute list, and the new password. By modifying the keychain entry rather than overwriting it, the keychain entry will be properly updated and any associated metadata will still be preserved. The entry should look like the one in Listing 4-9.

Listing 4-9 Modifying the keychain entry

// 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);

If the entry does not exist, then you will need to create it from scratch. The function SecKeychainAddInternetPassword accomplishes this task. Its parameters are the same as SecKeychainFindInternetPassword, but in contrast with the call to SecKeychainFindInternetPassword, you supply SecKeychainAddInternetPassword both a user name and a password. Release the keychain item reference object following a successful call to SecKeychainAddInternetPassword unless you need to use it for something else. See the function call in Listing 4-10.

Listing 4-10 Storing a new keychain entry

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);

Authenticating Firewalls

Authenticating firewalls is very similar to authenticating servers except that every failed HTTP request must be checked for both proxy authentication and server authentication. This means that you need separate stores (both local and persistent) for proxy servers and origin servers. Thus, the procedure for a failed HTTP response will now be:

  • Determine whether the response's status code was 407 (a proxy challenge). If it is, find a matching authentication object and credentials by checking the local proxy store and the persistent proxy store. If neither of those has a matching object and credentials, then request the credentials from the user. Apply the authentication object to the HTTP request and try again.
  • Determine whether the response's status code was 401 (a server challenge). If it is, follow the same procedure as with a 407 response, but use the origin server stores.

There are also a few minor differences to enforce when using proxy servers. The first is that the arguments to the keychain calls come from the proxy host and port, rather than from the URL for an origin server. The second is that when asking the user for a user name and password, make sure the prompt clearly states what the password is for.

By following these instructions, your application should be able to work with authenticating firewalls.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,311评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,339评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,671评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,252评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,253评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,031评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,340评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,973评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,466评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,937评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,039评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,701评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,254评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,259评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,497评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,786评论 2 345

推荐阅读更多精彩内容