Ios 使用NSURLSession的HTTP基本身份验证
我试图用Ios 使用NSURLSession的HTTP基本身份验证,ios,http,nsurlsession,basic-authentication,urlsession,Ios,Http,Nsurlsession,Basic Authentication,Urlsession,我试图用NSURLSession实现HTTP基本身份验证,但遇到了几个问题。在回答之前,请阅读整个问题,我怀疑这是与另一个问题的重复 根据我运行的测试,NSURLSession的行为如下: 第一个请求总是在没有授权标题的情况下发出 如果第一个请求失败,出现401未经授权的响应和WWW-Authenticate-Basic-realm=…头,则会自动重试 在重试请求之前,会话将通过查看会话配置的NSURLCredentialStorage或调用URLSession:task:didReceive
NSURLSession
实现HTTP基本身份验证,但遇到了几个问题。在回答之前,请阅读整个问题,我怀疑这是与另一个问题的重复
根据我运行的测试,NSURLSession
的行为如下:
- 第一个请求总是在没有
标题的情况下发出授权
- 如果第一个请求失败,出现
401未经授权的
响应和
头,则会自动重试WWW-Authenticate-Basic-realm=…
- 在重试请求之前,会话将通过查看会话配置的
或调用NSURLCredentialStorage
委托方法(或两者)尝试获取凭据URLSession:task:didReceiveChallenge:completionHandler:
- 如果可以获得凭据,则使用正确的
标头重试请求。如果没有,则在没有标头的情况下重试(这很奇怪,因为在本例中,这是完全相同的请求)授权
- 如果第二个请求成功,任务将透明地报告为成功,并且您甚至不会收到请求尝试两次的通知。如果不是,则报告第二个请求失败(但不是第一个)
POST
正文将发送两次,这是一种可怕的开销
我已尝试将授权
标题手动添加到会话配置的httpAdditionalHeaders
中,但仅当在创建会话之前设置了属性时,此操作才有效。之后试图修改会话.configuration.httpAdditionalHeaders的操作无效。文档中还明确指出,授权
标题不应手动设置
所以我的问题是:如果我需要在获得凭据之前启动会话,并且如果我想确保在第一次发出请求时始终使用正确的
授权
标题,我该怎么办
下面是我在测试中使用的代码示例。你可以用它重现我上面描述的所有行为 请注意,为了能够看到双重请求,您需要使用自己的http服务器并记录请求,或者通过记录所有请求的代理进行连接(我已经为此使用了代理) 注1:当向同一端点发出一行多个请求时,我上面描述的行为只涉及第一个请求。第一次使用正确的
授权
头尝试后续请求。但是,如果您等待一段时间(大约1分钟),会话将返回默认行为(第一个请求尝试了两次)
注2:这没有直接关系,但对会话配置的urlCredentialStorage
使用自定义NSURLCredentialStorage
似乎不起作用。仅使用默认值(根据文档,它是共享的NSURLCredentialStorage
)有效
注3:我曾尝试使用Alamofire,但由于它基于
NSURLSession
,因此其行为方式与之完全相同。如果可能,服务器应在客户端完成发送正文之前很久发出错误响应。然而,在许多高级服务器端语言中,这是很困难的,而且即使您这样做,也不能保证上传会停止
真正的问题是,您正在使用单个POST请求执行大型上载。这使得身份验证成为问题,并且如果在上传过程中连接中断,也会阻止任何有用的上传继续。分块上传基本上解决了所有问题:
- 对于您的第一个请求,只发送适合的数量,而不添加额外的以太网数据包,即计算您的典型头大小,mod乘以1500字节,添加几十个字节以便更好地测量,从1500中减去,并为您的第一个数据块硬编码该大小。最多,你浪费了几包
- 对于后续块,请将大小放大
- 当请求失败时,询问服务器它得到了多少,然后从停止上载的位置重试
- 发出请求,在完成上载后通知服务器
- 使用cron作业或其他方法定期清除服务器端的部分上载
也就是说,如果您不能控制服务器端,通常的解决方法是在POST请求之前发送一个经过身份验证的GET请求。这将最大限度地减少浪费的数据包,同时只要网络可靠,大部分数据包仍能正常工作。为什么不让第一个请求除了获得适当的授权标头外,始终不做任何工作?下一个请求(和向前)做所有的重提。尽量不要覆盖<代码> DeDeAccEvestPrime @ Gelnay.这是一个我甚至不想考虑的丑陋的解决方案。仍然毫无理由地消耗带宽,正如我所说的,这不仅仅是会话的第一个请求,而是每次停留约1分钟而不发送任何内容时的第一个请求,因此这也非常不切实际。@RunLoop尝试过这个方法。还尝试使用完成处理程序中的每个可能答案。不起作用。请尝试使用临时选项设置NSURLSession,并仅使用附加标题选项设置授权。谢谢您的回答。我已经在做分块上传了,但是分块本身仍然很大。现在,我找到了一种方法,可以在每个请求中注入适当的头,这样它就可以正常工作了。我希望在
URLSession
级别找到解决方案。我感到非常惊讶的是,我不能预先配置身份验证,而是必须
class URLSessionTest: NSObject, URLSessionDelegate
{
static let shared = URLSessionTest()
func start()
{
let requestURL = URL(string: "https://httpbin.org/basic-auth/username/password")!
let credential = URLCredential(user: "username", password: "password", persistence: .forSession)
let protectionSpace = URLProtectionSpace(host: "httpbin.org", port: 443, protocol: NSURLProtectionSpaceHTTPS, realm: "Fake Realm", authenticationMethod: NSURLAuthenticationMethodHTTPBasic)
let useHTTPHeader = false
let useCredentials = true
let useCustomCredentialsStorage = false
let useDelegateMethods = true
let sessionConfiguration = URLSessionConfiguration.default
if (useHTTPHeader) {
let authData = "\(credential.user!):\(credential.password!)".data(using: .utf8)!
let authValue = "Basic " + authData.base64EncodedString()
sessionConfiguration.httpAdditionalHeaders = ["Authorization": authValue]
}
if (useCredentials) {
if (useCustomCredentialsStorage) {
let urlCredentialStorage = URLCredentialStorage()
urlCredentialStorage.set(credential, for: protectionSpace)
sessionConfiguration.urlCredentialStorage = urlCredentialStorage
} else {
sessionConfiguration.urlCredentialStorage?.set(credential, for: protectionSpace)
}
}
let delegate = useDelegateMethods ? self : nil
let session = URLSession(configuration: sessionConfiguration, delegate: delegate, delegateQueue: nil)
self.makeBasicAuthTest(url: requestURL, session: session) {
self.makeBasicAuthTest(url: requestURL, session: session) {
DispatchQueue.main.asyncAfter(deadline: .now() + 61.0) {
self.makeBasicAuthTest(url: requestURL, session: session) {}
}
}
}
}
func makeBasicAuthTest(url: URL, session: URLSession, completion: @escaping () -> Void)
{
let task = session.dataTask(with: url) { (data, response, error) in
if let response = response {
print("response : \(response)")
}
if let data = data {
if let json = try? JSONSerialization.jsonObject(with: data, options: .allowFragments) {
print("json : \(json)")
} else if data.count > 0, let string = String(data: data, encoding: .utf8) {
print("string : \(string)")
} else {
print("data : \(data)")
}
}
if let error = error {
print("error : \(error)")
}
print()
DispatchQueue.main.async(execute: completion)
}
task.resume()
}
@objc(URLSession:didReceiveChallenge:completionHandler:)
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Swift.Void)
{
print("Session authenticationMethod: \(challenge.protectionSpace.authenticationMethod)")
if (challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodHTTPBasic) {
let credential = URLCredential(user: "username", password: "password", persistence: .forSession)
completionHandler(.useCredential, credential)
} else {
completionHandler(.performDefaultHandling, nil)
}
}
@objc(URLSession:task:didReceiveChallenge:completionHandler:)
func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Swift.Void)
{
print("Task authenticationMethod: \(challenge.protectionSpace.authenticationMethod)")
if (challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodHTTPBasic) {
let credential = URLCredential(user: "username", password: "password", persistence: .forSession)
completionHandler(.useCredential, credential)
} else {
completionHandler(.performDefaultHandling, nil)
}
}
}