OndoSDK 5.1.1

OndoSDK 5.1.1

Onfido 维护。



OndoSDK 5.1.1

  • OndoSDK 软件基金会

Alamofire: Elegant Networking in Swift

Build Status CocoaPods Compatible Carthage Compatible Platform Twitter

Alamofire 是一个用 Swift 编写的 HTTP 网络库。

特性

  • 链式请求/响应方法
  • URL/JSON/plist 参数编码
  • 上传文件/数据/流/MultipartFormData
  • 使用请求或恢复数据下载文件
  • 使用 URLCredential 进行身份验证
  • HTTP 响应验证
  • 带有进度的上传和下载进度闭包
  • cURL 命令输出
  • 动态调整和重试请求
  • TLS 证书和公钥固定
  • 网络可达性
  • 全面的单元和集成测试覆盖率
  • 完整的文档

组件库

为了使 Alamofire 专注于核心网络实现,Alamofire 软件基金会创建了额外的组件库,以将更多功能引入 Alamofire 生态系统。

  • AlamofireImage - 一个包含图像响应序列化器、UIImageUIImageView 扩展、自定义图像过滤器、自动清理的缓存和在优先级基础上的图像下载系统。
  • AlamofireNetworkActivityIndicator - 使用 Alamofire 控制iOS上的网络活动指示器可见性。它包含可配置的延迟计时器,有助于减轻闪烁,并支持不由 Alamofire 管理的 URLSession 实例。

需求

  • iOS 9.0+ / macOS 10.11+ / tvOS 9.0+ / watchOS 2.0+
  • Xcode 8.0+
  • Swift 3.0+

迁移指南

沟通

  • 如果您需要帮助,请使用 Stack Overflow。 (标签 'alamofire')
  • 如果您想提出一般性问题,请使用 Stack Overflow
  • 如果您发现了一个错误,请报告一个问题。
  • 如果您有一个功能请求,请报告一个问题。
  • 如果您愿意贡献,请提交一个pull请求。

安装

CocoaPods

CocoaPods 是 Cocoa 项目的依赖项管理器。您可以使用以下命令安装它:

$ gem install cocoapods

要构建 Alamofire 4.0.0+ 版本,需要 CocoaPods 1.1.0+。

使用 CocoaPods 将 Alamofire 集成到您的 Xcode 项目中,请在其 Podfile 中指定。

source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '10.0'
use_frameworks!

target '<Your Target Name>' do
    pod 'Alamofire', '~> 4.0'
end

然后,运行以下命令

$ pod install

Carthage

Carthage 是一个去中心化的依赖管理器,它构建您的依赖项并为您提供二进制框架。

您可以使用以下命令使用 Homebrew 安装 Carthage

$ brew update
$ brew install carthage

使用 Carthage 将 Alamofire 集成到您的 Xcode 项目中,请在您的 Cartfile 中指定。

github "Alamofire/Alamofire" ~> 4.0

运行 carthage update 构建框架,并将构建的 Alamofire.framework 拖动到您的 Xcode 项目中。

手动集成

如果您不想使用上述任一依赖项管理器,您可以手动将 Alamofire 集成到项目中。

嵌入式框架

  • 打开终端,使用 cd 命令进入您的顶级项目目录,如果项目不是作为 git 仓库初始化,则运行以下命令
$ git init
  • 通过运行以下命令将 Alamofire 添加为 git 子模块
$ git submodule add https://github.com/Alamofire/Alamofire.git
  • 打开新的 Alamofire 文件夹,并将 Alamofire.xcodeproj 拖动到您的应用程序 Xcode 项目的 Project Navigator 中。

    它应该嵌套在您的应用程序的蓝色项目图标下。它位于所有其他 Xcode 组之上或之下无关紧要。

  • 在 Project Navigator 中选择 Alamofire.xcodeproj,并验证部署目标是否与您的应用程序目标相匹配。

  • 接下来,在 Project Navigator 中选择您的应用程序项目(蓝色项目图标),导航到目标配置窗口,并在侧边栏中选择 "Targets" 下的应用程序目标。

  • 在窗口顶部的标签栏中,打开 "General" 选项卡。

  • 在 "Embedded Binaries" 部分下点击 + 按钮。

  • 您将看到两个不同的 Alamofire.xcodeproj 文件夹,每个文件夹中都有一个包含不同版本的 Alamofire.frameworkProducts 文件夹。

    您可以选择哪个 Products 文件夹无关紧要,但您选择的 Alamofire.framework 是否在顶部或底部至关重要。

  • 为 iOS 选择顶部的 Alamofire.framework,为 macOS 选择底部的。

    您可以通过检查项目的构建日志来验证您选择了哪个。Alamofire 的构建目标将列示为 Alamofire iOSAlamofire macOSAlamofire tvOSAlamofire watchOS

  • 就是这样!


import Alamofire

Alamofire.request("https://httpbin.org/get")

Alamofire.request("https://httpbin.org/get").responseJSON { response in
    print(response.request)  // original URL request
    print(response.response) // HTTP URL response
    print(response.data)     // server data
    print(response.result)   // result of response serialization

    if let JSON = response.result.value {
        print("JSON: \(JSON)")
    }
}

// Response Handler - Unserialized Response
func response(
    queue: DispatchQueue?,
    completionHandler: @escaping (DefaultDownloadResponse) -> Void)
    -> Self

// Response Data Handler - Serialized into Data
func responseData(
    queue: DispatchQueue?,
    completionHandler: @escaping (DataResponse<Data>) -> Void)
    -> Self

// Response String Handler - Serialized into String
func responseString(
    queue: DispatchQueue?,
    encoding: String.Encoding?,
    completionHandler: @escaping (DataResponse<String>) -> Void)
    -> Self

// Response JSON Handler - Serialized into Any
func responseJSON(
    queue: DispatchQueue?,
    completionHandler: @escaping (DataResponse<Any>) -> Void)
    -> Self

// Response PropertyList (plist) Handler - Serialized into Any
func responsePropertyList(
    queue: DispatchQueue?,
    completionHandler: @escaping (DataResponse<Any>) -> Void))
    -> Self

Alamofire.request("https://httpbin.org/get").response { response in
    print("Request: \(response.request)")
    print("Response: \(response.response)")
    print("Error: \(response.data)")

    if let data = response.data, let utf8Text = String(data: data, encoding: .utf8) {
    	print("Data: \(utf8Text)")
    }
}

我们强烈建议您利用其他响应序列化器,利用 ResponseResult 类型。

响应数据处理程序

responseData 处理程序使用 responseDataSerializer(将服务器数据序列化为其他类型的对象)来提取服务器返回的 Data。如果没有错误发生并且返回了 Data,响应 Result 将是 .success,而 value 将是 Data 类型。

Alamofire.request("https://httpbin.org/get").responseData { response in
    debugPrint("All Response Info: \(response)")

    if let data = response.result.value, let utf8Text = String(data: data, encoding: .utf8) {
    	print("Data: \(utf8Text)")
    }
}

响应字符串处理程序

responseString 处理程序使用 responseStringSerializer 将服务器返回的 Data 转换为指定编码的 String。如果没有错误发生并且服务器数据成功序列化为 String,响应 Result 将是 .success,而 value 将是 String 类型。

Alamofire.request("https://httpbin.org/get").responseString { response in
    print("Success: \(response.result.isSuccess)")
    print("Response String: \(response.result.value)")
}

如果没有指定编码,Alamofire 将使用从服务器提供的 HTTPURLResponse 中指定的文本编码。如果服务器响应不能确定文本编码,它将默认为 .isoLatin1

响应 JSON 处理程序

responseJSON 处理程序使用 responseJSONSerializer 将服务器返回的 Data 转换为使用指定 JSONSerialization.ReadingOptionsAny 类型。如果没有错误发生并且服务器数据成功序列化为 JSON 对象,响应 Result 将是 .success,而 value 将是 Any 类型。

Alamofire.request("https://httpbin.org/get").responseJSON { response in
    debugPrint(response)

    if let json = response.result.value {
        print("JSON: \(json)")
    }
}

所有 JSON 序列化都在 Foundation 框架的 JSONSerialization API 中处理。

链式响应处理程序

响应处理程序甚至可以串联使用

Alamofire.request("https://httpbin.org/get")
    .responseString { response in
        print("Response String: \(response.result.value)")
    }
    .responseJSON { response in
        print("Response JSON: \(response.result.value)")
    }

重要的是要注意,在相同的 Request 上使用多个响应处理程序需要将服务器数据序列化多次。每个响应处理程序一次。

响应处理器队列

默认情况下,响应处理器在主分发队列上执行。然而,也可以提供自定义的分发队列。

let utilityQueue = DispatchQueue.global(qos: .utility)

Alamofire.request("https://httpbin.org/get").responseJSON(queue: utilityQueue) { response in
    print("Executing response handler on utility queue")
}

响应验证

默认情况下,Alamofire将任何完成的请求视为成功,而不管响应的内容如何。在响应处理器之前调用validate会导致在响应具有不可接受的状态码或MIME类型时生成错误。

手动验证

Alamofire.request("https://httpbin.org/get")
    .validate(statusCode: 200..<300)
    .validate(contentType: ["application/json"])
    .response { response in
	    switch response.result {
	    case .success:
    	    print("Validation Successful")
	    case .failure(let error):
    	    print(error)
	    }
    }

自动验证

自动验证状态码是否在200...299范围内,以及响应的Content-Type头部与请求的Accept头部是否匹配(如果提供了的话)。

Alamofire.request("https://httpbin.org/get").validate().responseJSON { response in
    switch response.result {
    case .success:
        print("Validation Successful")
    case .failure(let error):
        print(error)
    }
}

响应缓存

响应缓存由系统框架层上的URLCache处理,它提供了一个组合的内存和磁盘缓存,并允许您操作内存和磁盘部分的尺寸。

默认情况下,Alamofire利用共享的URLCache。要自定义它,请参阅会话管理器配置部分。

HTTP 方法

HTTP 方法枚举列出了在 RFC 7231 §4.3 中定义的 HTTP 方法。

public enum HTTPMethod: String {
    case options = "OPTIONS"
    case get     = "GET"
    case head    = "HEAD"
    case post    = "POST"
    case put     = "PUT"
    case patch   = "PATCH"
    case delete  = "DELETE"
    case trace   = "TRACE"
    case connect = "CONNECT"
}

这些值可以作为 Alamofire.request API 的 method 参数传递。

Alamofire.request("https://httpbin.org/get") // method defaults to `.get`

Alamofire.request("https://httpbin.org/post", method: .post)
Alamofire.request("https://httpbin.org/put", method: .put)
Alamofire.request("https://httpbin.org/delete", method: .delete)

Alamofire.request 方法参数的默认值为 .get

参数编码

Alamofire 支持 URL、JSON 和 PropertyList 三种参数编码类型。它还可以支持任何符合 ParameterEncoding 协议的自定义编码。

URL 编码

URLEncoding 类型创建一个编码后的查询字符串,可以将其设置为或附加到任何现有的 URL 查询字符串,或将其设置为 URL 请求的 HTTP 主体。查询字符串是否设置为或附加到现有的 URL 查询字符串或设置为 HTTP 主体取决于编码的 Destination。编码的 Destination 枚举有三个情况:

  • .methodDependent - 将编码后的查询字符串结果应用于 GETHEADDELETE 请求的现有查询字符串,并为任何其他 HTTP 请求设置为 HTTP 主体。
  • .queryString - 设置或附加编码后的查询字符串结果到现有的查询字符串。
  • .httpBody - 将编码后的查询字符串结果设置为 URL 请求的 HTTP 主体。

编码后具有 HTTP 主题的请求的 HTTP 头字段 Content-Type 设置为 application/x-www-form-urlencoded; charset=utf-8。由于尚未发布有关如何编码集合类型的规范,所以使用约定是将方括号 [] 附加到索引键的末尾(例如,foo[]=1&foo[]=2),以及将键用方括号包围以表示嵌套字典值(例如,foo[bar]=baz)。

带有 URL 编码参数的 GET 请求
let parameters: Parameters = ["foo": "bar"]

// All three of these calls are equivalent
Alamofire.request("https://httpbin.org/get", parameters: parameters) // encoding defaults to `URLEncoding.default`
Alamofire.request("https://httpbin.org/get", parameters: parameters, encoding: URLEncoding.default)
Alamofire.request("https://httpbin.org/get", parameters: parameters, encoding: URLEncoding(destination: .methodDependent))

// https://httpbin.org/get?foo=bar
带有URL编码参数的POST请求
let parameters: Parameters = [
    "foo": "bar",
    "baz": ["a", 1],
    "qux": [
        "x": 1,
        "y": 2,
        "z": 3
    ]
]

// All three of these calls are equivalent
Alamofire.request("https://httpbin.org/post", parameters: parameters)
Alamofire.request("https://httpbin.org/post", parameters: parameters, encoding: URLEncoding.default)
Alamofire.request("https://httpbin.org/post", parameters: parameters, encoding: URLEncoding.httpBody)

// HTTP body: foo=bar&baz[]=a&baz[]=1&qux[x]=1&qux[y]=2&qux[z]=3

JSON编码

JSONEncoding 类型创建参数对象的JSON表示形式,并将其设置为请求的HTTP正文。编码请求的Content-Type HTTP标头字段设置为application/json

带有JSON编码参数的POST请求
let parameters: Parameters = [
    "foo": [1,2,3],
    "bar": [
        "baz": "qux"
    ]
]

// Both calls are equivalent
Alamofire.request("https://httpbin.org/post", method: .post, parameters: parameters, encoding: JSONEncoding.default)
Alamofire.request("https://httpbin.org/post", method: .post, parameters: parameters, encoding: JSONEncoding(options: []))

// HTTP body: {"foo": [1, 2, 3], "bar": {"baz": "qux"}}

属性列表编码

PropertyListEncoding 使用 PropertyListSerialization 根据关联的格式和写入选项值创建参数对象的plist表示形式,并将其设置为请求正文。编码请求的 Content-Type HTTP标头字段设置为 application/x-plist

自定义编码

如果提供的 ParameterEncoding 类型不符合您的需求,您可以创建自己的自定义编码。以下是一个快速示例,说明如何构建一个自定义的 JSONStringArrayEncoding 类型,以便将JSON字符串数组编码到 Request 中。

struct JSONStringArrayEncoding: ParameterEncoding {
	private let array: [String]

    init(array: [String]) {
        self.array = array
    }

    func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest {
        var urlRequest = urlRequest.urlRequest

        let data = try JSONSerialization.data(withJSONObject: array, options: [])

        if urlRequest.value(forHTTPHeaderField: "Content-Type") == nil {
            urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
        }

        urlRequest.httpBody = data

        return urlRequest
    }
}

URLRequest的自动参数编码

ParameterEncoding接口除了在网络请求中使用外还可以在其它场合使用。

let url = URL(string: "https://httpbin.org/get")!
var urlRequest = URLRequest(url: url)

let parameters: Parameters = ["foo": "bar"]
let encodedURLRequest = try URLEncoding.queryString.encode(urlRequest, with: parameters)

HTTP头部

在全局request方法中直接支持向Request对象添加自定义HTTP头部,这使得为不断变化的Request对象附加HTTP头部变得很容易。

let headers: HTTPHeaders = [
    "Authorization": "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==",
    "Accept": "application/json"
]

Alamofire.request("https://httpbin.org/headers", headers: headers).responseJSON { response in
    debugPrint(response)
}

对于不经常变化的HTTP头部,建议在URLSessionConfiguration中设置它们,这样它们就会被自动应用到由底层URLSession创建的任何URLSessionTask。有关更多信息,请参阅Session Manager Configurations部分。

默认的Alamofire SessionManager为每个Request提供一个默认的头部集。这包括

  • Accept-Encoding,默认为gzip;q=1.0, compress;q=0.5,根据RFC 7230 §4.2.3
  • Accept-Language,默认设置为系统最喜欢的6种语言中的前6种,格式为en;q=1.0,根据RFC 7231 §5.3.5
  • User-Agent,包含有关当前应用版本的信息。例如:iOS Example/1.0 (com.alamofire.iOS-Example; build:1; iOS 10.0.0) Alamofire/4.0.0,根据RFC 7231 §5.5.3

如果您需要自定义这些头部,应创建自定义的URLSessionManagerConfiguration,更新defaultHTTPHeaders属性,并将配置应用到新的SessionManager实例。

身份验证

身份验证由系统框架级别通过URLCredentialURLAuthenticationChallenge处理。

支持的认证方案

HTTP基本认证

在《Request》对象上使用的authenticate方法会在适当的情况下自动将一个URLCredential提供一个给URLAuthenticationChallenge

let user = "user"
let password = "password"

Alamofire.request("https://httpbin.org/basic-auth/\(user)/\(password)")
    .authenticate(user: user, password: password)
    .responseJSON { response in
        debugPrint(response)
    }

根据您的服务器实现,可能也需要设置一个Authorization头部

let user = "user"
let password = "password"

var headers: HTTPHeaders = [:]

if let authorizationHeader = Request.authorizationHeader(user: user, password: password) {
    headers[authorizationHeader.key] = authorizationHeader.value
}

Alamofire.request("https://httpbin.org/basic-auth/user/password", headers: headers)
    .responseJSON { response in
        debugPrint(response)
    }

使用URLCredential进行认证

let user = "user"
let password = "password"

let credential = URLCredential(user: user, password: password, persistence: .forSession)

Alamofire.request("https://httpbin.org/basic-auth/\(user)/\(password)")
    .authenticate(usingCredential: credential)
    .responseJSON { response in
        debugPrint(response)
    }

使用URLCredential进行认证时,请注意,当服务器发出请求挑战时,底层的URLSession实际上会发出两个请求。第一个请求不会包含凭证,这“可能”会触发服务器的挑战。然后Alamofire接收到的挑战,将凭证附加到请求中,并由底层的URLSession重新尝试请求。

将数据下载到文件

使用Alamofire制作的从服务器获取数据的请求可以下载数据到内存或磁盘。到目前为止,所有示例中使用Alamofire.request API总是会下载服务器数据到内存中。这对于小型负载来说效率很高,但是对于大型负载来说可能会使得应用程序耗尽内存。因此,您还可以使用Alamofire.download API将服务器数据下载到磁盘上的临时文件。

Alamofire.download("https://httpbin.org/image/png").responseData { response in
	if let data = response.result.value {
	    let image = UIImage(data: data)
	}
}

如果您在应用程序处于后台时需要下载数据,也应使用Alamofire.download API。更多详细信息,请参阅会话管理配置部分。

下载文件目标位置

您还可以提供一个DownloadFileDestination闭包来将文件从临时目录移动到最后目标位置。在实际上将临时文件移动到destinationURL之前,闭包中指定的DownloadOptions将被执行。目前支持的两个DownloadOptions

  • .createIntermediateDirectories - 如果指定,则为目标URL创建中间目录。
  • .removePreviousFile - 如果指定,将删除目标URL中之前存在的文件。
let destination: DownloadRequest.DownloadFileDestination = { _, _ in
	let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
	let fileURL = documentsURL.appendPathComponent("pig.png")

    return (fileURL, [.removePreviousFile, .createIntermediateDirectories])
}

Alamofire.download(urlString, to: destination).response { response in
    print(response)

	if response.result.isSuccess, let imagePath = response.destinationURL?.path {
	    let image = UIImage(contentsOfFile: imagePath)
	}
}

您还可以使用建议的下载目标API。

let destination = DownloadRequest.suggestedDownloadDestination(directory: .documentDirectory)
Alamofire.download("https://httpbin.org/image/png", to: destination)

下载进度

许多情况下,向用户报告下载进度都可能是有帮助的。任何DownloadRequest都可以使用downloadProgress API来报告下载进度。

Alamofire.download("https://httpbin.org/image/png")
    .downloadProgress { progress in
        print("Download Progress: \(progress.fractionCompleted)")
    }
    .responseData { response in
    	if let data = response.result.value {
	        let image = UIImage(data: data)
    	}
    }

downloadProgress API还接受一个queue参数,它定义了下载进度闭包应该在哪个DispatchQueue上调用。

let utilityQueue = DispatchQueue.global(qos: .utility)

Alamofire.download("https://httpbin.org/image/png")
    .downloadProgress(queue: utilityQueue) { progress in
        print("Download Progress: \(progress.fractionCompleted)")
    }
    .responseData { response in
    	if let data = response.result.value {
	        let image = UIImage(data: data)
    	}
    }

恢复下载

如果DownloadRequest被取消或中断,底层的URL会话可能会为活动的DownloadRequest生成恢复数据。如果发生这种情况,可以重新使用恢复数据从上次停止的地方重新启动DownloadRequest。恢复数据可以通过下载响应访问,并在尝试重新启动请求时重用它。

class ImageRequestor {
	private var resumeData: Data?
	private var image: UIImage?

    func fetchImage(completion: (UIImage?) -> Void) {
    	guard image == nil else { completion(image) ; return }

		let destination: DownloadRequest.DownloadFileDestination = { _, _ in
			let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
			let fileURL = documentsURL.appendPathComponent("pig.png")

		    return (fileURL, [.removePreviousFile, .createIntermediateDirectories])
		}

    	let request: DownloadRequest

        if let resumeData = resumeData {
			request = Alamofire.download(resumingWith: resumeData)
		} else {
			request = Alamofire.download("https://httpbin.org/image/png")
        }

        request.responseData { response in
        	switch response.result {
        	case .success(let data):
		        self.image = UIImage(data: data)
        	case .failure:
        		self.resumeData = response.resumeData
        	}
        }
    }
}

将数据上传到服务器

当使用JSON或URL编码参数将相对较小的数据发送到服务器时,通常使用Alamofire.request API就足够了。如果您需要从文件URL或InputStream发送大量数据,则应使用Alamofire.upload API。

如果您在应用程序处于后台时需要上传数据,也应使用Alamofire.upload API。有关更多信息,请参阅会话管理配置部分。

上传数据

let imageData = UIPNGRepresentation(image)!

Alamofire.upload(imageData, to: "https://httpbin.org/post").responseJSON { response in
    debugPrint(response)
}

上传文件

let fileURL = Bundle.main.url(forResource: "video", withExtension: "mov")

Alamofire.upload(fileURL, to: "https://httpbin.org/post").responseJSON { response in
    debugPrint(response)
}

上传多部分表单数据

Alamofire.upload(
    multipartFormData: { multipartFormData in
        multipartFormData.append(unicornImageURL, withName: "unicorn")
        multipartFormData.append(rainbowImageURL, withName: "rainbow")
    },
    to: "https://httpbin.org/post",
    encodingCompletion: { encodingResult in
    	switch encodingResult {
    	case .success(let upload, _, _):
            upload.responseJSON { response in
                debugPrint(response)
            }
    	case .failure(let encodingError):
    	    print(encodingError)
    	}
    }
)

上传进度

当用户等待上传完成时,有时显示上传进度对用户来说很有帮助。任何UploadRequest都可以使用uploadProgressdownloadProgress API报告上传进度和响应数据的下载进度。

let fileURL = Bundle.main.url(forResource: "video", withExtension: "mov")

Alamofire.upload(fileURL, to: "https://httpbin.org/post")
    .uploadProgress { progress in // main queue by default
        print("Upload Progress: \(progress.fractionCompleted)")
    }
    .downloadProgress { progress in // main queue by default
        print("Download Progress: \(progress.fractionCompleted)")
    }
    .responseJSON { response in
        debugPrint(response)
    }

统计指标

时间线

Alamofire会在Request的生命周期中收集计时信息,并创建一个作为所有响应类型属性的Timeline对象。

Alamofire.request("https://httpbin.org/get").responseJSON { response in
    print(response.timeline)
}

上述报告了以下Timeline信息

  • 延迟:0.428秒
  • 请求持续时间:0.428秒
  • 序列化持续时间:0.001秒
  • 总持续时间:0.429秒

URL会话任务指标

在iOS和tvOS 10及macOS 10.12中,Apple引入了新的URLSessionTaskMetrics API。任务指标封装了关于请求和响应执行的某些出色的统计信息。API与Timeline非常相似,但它提供了许多Alamofire无法计算更多的统计数据。可以通过任何响应类型访问这些指标。

Alamofire.request("https://httpbin.org/get").responseJSON { response in
	print(response.metrics)
}

需要注意的是,这些API仅适用于iOS和tvOS 10及macOS 10.12。因此,根据您的部署目标,您可能需要在可用性检查中使用这些API。

Alamofire.request("https://httpbin.org/get").responseJSON { response in
    if #available(iOS 10.0. *) {
		print(response.metrics)
    }
}

cURL命令输出

调试平台问题可能会让人感到很沮丧。幸运的是,Alamofire的Request对象同时遵守CustomStringConvertibleCustomDebugStringConvertible协议,提供了一些非常有用的调试工具。

CustomStringConvertible

let request = Alamofire.request("https://httpbin.org/ip")

print(request)
// GET https://httpbin.org/ip (200)

CustomDebugStringConvertible

let request = Alamofire.request("https://httpbin.org/get", parameters: ["foo": "bar"])
debugPrint(request)

输出

$ curl -i \
	-H "User-Agent: Alamofire/4.0.0" \
	-H "Accept-Encoding: gzip;q=1.0, compress;q=0.5" \
	-H "Accept-Language: en;q=1.0,fr;q=0.9,de;q=0.8,zh-Hans;q=0.7,zh-Hant;q=0.6,ja;q=0.5" \
	"https://httpbin.org/get?foo=bar"

高级用法

Alamofire是建立在URLSession和Foundation URL加载系统之上的。要充分利用此框架,建议您熟悉底层网络堆栈的概念和能力。

推荐阅读

会话管理器

最高层便利方法,如Alamofire.request,使用默认的Alamofire.SessionManager实例,它已配置为默认的URLSessionConfiguration

因此,以下两个语句是等效的

Alamofire.request("https://httpbin.org/get")
let sessionManager = Alamofire.SessionManager.default
sessionManager.request("https://httpbin.org/get")

应用程序可以创建用于后台和短暂会话的会话管理器,以及自定义默认会话配置的新管理器,例如用于默认头(httpAdditionalHeaders)或超时间隔(timeoutIntervalForRequest)。

创建具有默认配置的会话管理器

let configuration = URLSessionConfiguration.default
let sessionManager = Alamofire.SessionManager(configuration: configuration)

创建具有后台配置的会话管理器

let configuration = URLSessionConfiguration.background(withIdentifier: "com.example.app.background")
let sessionManager = Alamofire.SessionManager(configuration: configuration)

创建具有临时配置的会话管理器

let configuration = URLSessionConfiguration.ephemeral
let sessionManager = Alamofire.SessionManager(configuration: configuration)

修改会话配置

var defaultHeaders = Alamofire.SessionManager.default.defaultHTTPHeaders
defaultHeaders["DNT"] = "1 (Do Not Track Enabled)"

let configuration = URLSessionConfiguration.default
configuration.httpAdditionalHeaders = defaultHeaders

let sessionManager = Alamofire.SessionManager(configuration: configuration)

不推荐用于 授权内容类型 标题。相反,使用顶级 Alamofire.request API 中的 headers 参数、URLRequestConvertibleParameterEncoding

会话代理

默认情况下,Alamofire 的 SessionManager 实例创建一个 SessionDelegate 对象来处理由底层的 URLSession 生成的各种类型的代理回调。每个代理方法的实现处理这些类型调用的最常见用例,将复杂性从顶级 API 抽象出来。然而,经验丰富的用户可能会根据各种原因需要覆盖默认功能。

覆盖闭包

首次定制《SessionDelegate》行为的方式是通过使用覆盖闭包。每个闭包都允许您覆盖匹配的《SessionDelegate》API的实现,同时仍然使用所有其他API的默认实现。这使得可以轻松地定制子集的功能。以下是覆盖闭包的一些示例。

/// Overrides default behavior for URLSessionDelegate method `urlSession(_:didReceive:completionHandler:)`.
open var sessionDidReceiveChallenge: ((URLSession, URLAuthenticationChallenge) -> (URLSession.AuthChallengeDisposition, URLCredential?))?

/// Overrides default behavior for URLSessionDelegate method `urlSessionDidFinishEvents(forBackgroundURLSession:)`.
open var sessionDidFinishEventsForBackgroundURLSession: ((URLSession) -> Void)?

/// Overrides default behavior for URLSessionTaskDelegate method `urlSession(_:task:willPerformHTTPRedirection:newRequest:completionHandler:)`.
open var taskWillPerformHTTPRedirection: ((URLSession, URLSessionTask, HTTPURLResponse, URLRequest) -> URLRequest?)?

/// Overrides default behavior for URLSessionDataDelegate method `urlSession(_:dataTask:willCacheResponse:completionHandler:)`.
open var dataTaskWillCacheResponse: ((URLSession, URLSessionDataTask, CachedURLResponse) -> CachedURLResponse?)?

以下是使用《taskWillPerformHTTPRedirection》避免跟踪任何《apple.com》域名重定向的简短示例。

let sessionManager = Alamofire.SessionManager(configuration: URLSessionConfiguration.default)
let delegate: Alamofire.SessionDelegate = sessionManager.delegate

delegate.taskWillPerformHTTPRedirection = { session, task, response, request in
    var finalRequest = request

    if
        let originalRequest = task.originalRequest,
        let urlString = originalRequest.url?.urlString,
        urlString.contains("apple.com")
    {
        finalRequest = originalRequest
    }

    return finalRequest
}

继承

另一种覆盖《SessionDelegate》默认实现的方式是实现它。继承允许您完全自定义API的行为,或者创建API的代理,同时仍然使用默认实现。创建代理允许您记录事件、发出通知、提供前钩和后钩实现等。以下是继承《SessionDelegate》并在发生重定向时记录消息的快速示例。

class LoggingSessionDelegate: SessionDelegate {
    override func urlSession(
        _ session: URLSession,
        task: URLSessionTask,
        willPerformHTTPRedirection response: HTTPURLResponse,
        newRequest request: URLRequest,
        completionHandler: @escaping (URLRequest?) -> Void)
    {
        print("URLSession will perform HTTP redirection to request: \(request)")

        super.urlSession(
            session,
            task: task,
            willPerformHTTPRedirection: response,
            newRequest: request,
            completionHandler: completionHandler
        )
    }
}

一般来说,默认实现或覆盖闭包应提供所需的功能。继承应仅作为最后的手段使用。

重要的是要记住,《subdelegates》在默认实现中被初始化和销毁。在继承时要小心,以避免内存泄漏。

请求

《request》,《download》,《upload》或《stream》方法的结果是继承自《Request》的《DataRequest》,《DownloadRequest》,《UploadRequest》和《StreamRequest》。所有《Request》实例都是由所属会话管理器创建的,永远不会直接初始化。

每个子类都有专门的方法,如《authenticate》,《validate》,《responseJSON》和《uploadProgress》,这些方法都返回调用者实例,以促进链式调用。

请求可以被挂起、恢复和取消

  • suspend():挂起底层任务和分发队列。
  • resume():恢复底层任务和分发队列。如果所属管理器没有将《startRequestsImmediately》设置为《true》,则请求必须调用《resume》才能启动。
  • cancel():取消底层任务,产生一个错误,并将它传递给所有注册的响应处理程序。

请求路由

随着应用程序大小的增加,在构建你的网络堆栈时采用常见的模式是很重要的。该设计的一个重要部分是您的请求路由方式。Alamofire 的 `URLConvertible` 和 `URLRequestConvertible` 协议,以及 `Router` 设计模式,都旨在提供帮助。

URLConvertible

采用 `URLConvertible` 协议的类型可以被用来构造 URL,然后这些 URL 被用来构建内部 URL 请求。默认情况下,StringURLURLComponents 都遵循 `URLConvertible` 协议,这使得它们中的任何一个都可以作为 url 参数传递给 requestuploaddownload 方法。

let urlString = "https://httpbin.org/post"
Alamofire.request(urlString, method: .post)

let url = URL(string: urlString)!
Alamofire.request(url, method: .post)

let urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: true)
Alamofire.request(.post, URLComponents)

计算与 Web 应用程序以显著方式交互的应用程序鼓励自定义类型遵循 `URLConvertible` 协议,作为一种方便地将特定领域模型映射到服务器资源的方式。

类型安全的路由
extension User: URLConvertible {
    static let baseURLString = "https://example.com"

    func asURL() throws -> URL {
    	let urlString = User.baseURLString + "/users/\(username)/"
        return try urlString.asURL()
    }
}
let user = User(username: "mattt")
Alamofire.request(user) // https://example.com/users/mattt

URLRequestConvertible

采用 `URLRequestConvertible` 协议的类型可以用来构造 URL 请求。默认情况下,URLRequest 符合 `URLRequestConvertible` 协议,可以直接传递给 requestuploaddownload 方法(这是指定单个请求自定义 HTTP 体的推荐方式)。

let url = URL(string: "https://httpbin.org/post")!
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = "POST"

let parameters = ["foo": "bar"]

do {
    urlRequest.httpBody = try JSONSerialization.data(withJSONObject: parameters, options: [])
} catch {
    // No-op
}

urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")

Alamofire.request(urlRequest)

建议与 Web 应用程序以显著方式交互的应用程序有自己的自定义类型遵循 `URLRequestConvertible`,以确保请求端点的始终一致性。这种方法可以用于封装服务端的不一致性,提供类型安全的路由,并管理认证凭证和其他状态。

API 参数抽象
enum Router: URLRequestConvertible {
    case search(query: String, page: Int)

    static let baseURLString = "https://example.com"
    static let perPage = 50

    // MARK: URLRequestConvertible

    func asURLRequest() throws -> URLRequest {
        let result: (path: String, parameters: Parameters) = {
            switch self {
            case let .search(query, page) where page > 0:
                return ("/search", ["q": query, "offset": Router.perPage * page])
            case let .search(query, _):
                return ("/search", ["q": query])
            }
        }()

        let url = try Router.baseURLString.asURL()
        let urlRequest = URLRequest(url: url.appendingPathComponent(result.path))

        return try URLEncoding.default.encode(urlRequest, with: result.parameters)
    }
}
Alamofire.request(Router.search(query: "foo bar", page: 1)) // ?q=foo%20bar&offset=50
CRUD & 授权
import Alamofire

enum Router: URLRequestConvertible {
    case createUser(parameters: Parameters)
    case readUser(username: String)
    case updateUser(username: String, parameters: Parameters)
    case destroyUser(username: String)

    static let baseURLString = "https://example.com"

    var method: HTTPMethod {
        switch self {
        case .createUser:
            return .post
        case .readUser:
            return .get
        case .updateUser:
            return .put
        case .destroyUser:
            return .delete
        }
    }

    var path: String {
        switch self {
        case .createUser:
            return "/users"
        case .readUser(let username):
            return "/users/\(username)"
        case .updateUser(let username, _):
            return "/users/\(username)"
        case .destroyUser(let username):
            return "/users/\(username)"
        }
    }

    // MARK: URLRequestConvertible

    func asURLRequest() throws -> URLRequest {
    	let url = try Router.baseURLString.asURL()

        var urlRequest = URLRequest(url: url.appendingPathComponent(path))
        urlRequest.httpMethod = method.rawValue

        switch self {
        case .createUser(let parameters):
            urlRequest = try URLEncoding.default.encode(urlRequest, with: parameters)
        case .updateUser(_, let parameters):
            urlRequest = try URLEncoding.default.encode(urlRequest, with: parameters)
        default:
            break
        }

        return urlRequest
    }
}
Alamofire.request(Router.readUser("mattt")) // GET /users/mattt

适配和重试请求

现在大多数网络服务都采用某种认证系统。其中较常见的是OAuth。这通常涉及生成一个访问令牌,授权您的应用程序或用户调用各种支持的网络服务。虽然创建这些初始访问令牌可能很费力,但当您的访问令牌过期需要获取一个新令牌时,这会变得更加复杂。这里需要考虑许多线程安全问题。

RequestAdapterRequestRetrier协议的创建是为了使得为特定网络服务集创建线程安全的认证系统更加容易。

RequestAdapter

RequestAdapter协议允许在使用SessionManager执行每个Request之前,对其进行检查和适配。使用适配器的一种非常具体的方式是为具有特定类型认证的请求添加一个Authorization头。

class AccessTokenAdapter: RequestAdapter {
	private let accessToken: String

	init(accessToken: String) {
		self.accessToken = accessToken
	}

	func adapt(_ urlRequest: URLRequest) throws -> URLRequest {
	    var urlRequest = urlRequest

	    if urlRequest.urlString.hasPrefix("https://httpbin.org") {
		    urlRequest.setValue("Bearer " + accessToken, forHTTPHeaderField: "Authorization")
	    }

	    return urlRequest
	}
}
let sessionManager = SessionManager()
sessionManager.adapter = AccessTokenAdapter(accessToken: "1234")

sessionManager.request("https://httpbin.org/get")

RequestRetrier

RequestRetrier协议允许在执行过程中遇到ErrorRequest进行重试。当同时使用RequestAdapterRequestRetrier协议时,您可以创建用于OAuth1、OAuth2、基本认证甚至指数退避重试策略的凭证刷新系统。可能性是无限的。以下是如何实现OAuth2访问令牌刷新流的示例。

免责声明:这不是一个全局的OAuth2解决方案。这只是一个示例,展示了如何可以将RequestAdapterRequestRetrier一起使用来创建线程安全的刷新系统。

重申一下,请勿复制此示例代码并将其放入生产应用程序中。这只是一个示例。每个认证系统都必须针对特定平台和认证类型进行定制。

class OAuth2Handler: RequestAdapter, RequestRetrier {
    private typealias RefreshCompletion = (_ succeeded: Bool, _ accessToken: String?, _ refreshToken: String?) -> Void

    private let sessionManager: SessionManager = {
        let configuration = URLSessionConfiguration.default
        configuration.httpAdditionalHeaders = SessionManager.defaultHTTPHeaders

        return SessionManager(configuration: configuration)
    }()

    private let lock = NSLock()

    private var clientID: String
    private var baseURLString: String
    private var accessToken: String
    private var refreshToken: String

    private var isRefreshing = false
    private var requestsToRetry: [RequestRetryCompletion] = []

    // MARK: - Initialization

    public init(clientID: String, baseURLString: String, accessToken: String, refreshToken: String) {
        self.clientID = clientID
        self.baseURLString = baseURLString
        self.accessToken = accessToken
        self.refreshToken = refreshToken
    }

    // MARK: - RequestAdapter

    func adapt(_ urlRequest: URLRequest) throws -> URLRequest {
        if let url = urlRequest.url, url.urlString.hasPrefix(baseURLString) {
            var urlRequest = urlRequest
            urlRequest.setValue("Bearer " + accessToken, forHTTPHeaderField: "Authorization")
            return urlRequest
        }

        return urlRequest
    }

    // MARK: - RequestRetrier

    func should(_ manager: SessionManager, retry request: Request, with error: Error, completion: @escaping RequestRetryCompletion) {
        lock.lock() ; defer { lock.unlock() }

        if let response = request.task.response as? HTTPURLResponse, response.statusCode == 401 {
            requestsToRetry.append(completion)

            if !isRefreshing {
                refreshTokens { [weak self] succeeded, accessToken, refreshToken in
                    guard let strongSelf = self else { return }

                    strongSelf.lock.lock() ; defer { strongSelf.lock.unlock() }

                    if let accessToken = accessToken, let refreshToken = refreshToken {
                        strongSelf.accessToken = accessToken
                        strongSelf.refreshToken = refreshToken
                    }

                    strongSelf.requestsToRetry.forEach { $0(succeeded, 0.0) }
                    strongSelf.requestsToRetry.removeAll()
                }
            }
        } else {
            completion(false, 0.0)
        }
    }

    // MARK: - Private - Refresh Tokens

    private func refreshTokens(completion: @escaping RefreshCompletion) {
        guard !isRefreshing else { return }

        isRefreshing = true

        let urlString = "\(baseURLString)/oauth2/token"

        let parameters: [String: Any] = [
            "access_token": accessToken,
            "refresh_token": refreshToken,
            "client_id": clientID,
            "grant_type": "refresh_token"
        ]

        sessionManager.request(urlString, method: .post, parameters: parameters, encoding: JSONEncoding.default)
            .responseJSON { [weak self] response in
                guard let strongSelf = self else { return }

                if let json = response.result.value as? [String: String] {
                    completion(true, json["access_token"], json["refresh_token"])
                } else {
                    completion(false, nil, nil)
                }

                strongSelf.isRefreshing = false
            }
    }
}
let baseURLString = "https://some.domain-behind-oauth2.com"

let oauthHandler = OAuth2Handler(
    clientID: "12345678",
    baseURLString: baseURLString,
    accessToken: "abcd1234",
    refreshToken: "ef56789a"
)

let sessionManager = SessionManager()
sessionManager.adapter = oauthHandler
sessionManager.retrier = oauthHandler

let urlString = "\(baseURLString)/some/endpoint"

sessionManager.request(urlString).validate().responseJSON { response in
    debugPrint(response)
}

一旦将OAuth2Handler应用为SessionManageradapterretrier,它将处理无效访问令牌错误,自动刷新访问令牌并重试所有失败请求,顺序与它们失败时相同。

如果您需要它们按创建顺序执行,则可以根据任务标识符对它们进行排序。

上述示例仅检查401响应代码,这还不够强大,但它演示了如何检查无效访问令牌错误。在生产应用程序中,您可能希望检查realm以及最可能检查响应中的www-authenticate头,尽管这取决于OAuth2的实现。

另一个重要的注意事项是,此认证系统可以在多个会话管理器之间共享。例如,您可能需要为同一组Web服务同时使用默认临时会话配置。上面的例子允许相同的oauthHandler实例在多个会话管理器之间共享,用于管理单个刷新流。

自定义响应序列化

处理错误

在实现自定义响应序列化或对象序列化方法之前,考虑如何处理可能发生的错误非常重要。有两种基本选项:不修改地传递现有错误,在响应时处理;或将所有错误包装在您应用特定的Error类型中。

例如,这里有一个简单的BackendError枚举,将在后面的例子中使用。

enum BackendError: Error {
    case network(error: Error) // Capture any underlying Error from the URLSession API
    case dataSerialization(error: Error)
    case jsonSerialization(error: Error)
    case xmlSerialization(error: Error)
    case objectSerialization(reason: String)
}

创建自定义响应序列化器

Alamofire为字符串、JSON和属性列表提供了内置的响应序列化,但可以在Alamofire.DataRequest和/或Alamofire.DownloadRequest的扩展中添加其他序列化。

例如,以下是如何使用Ono实现响应处理程序的示例。

extension DataRequest {
    static func xmlResponseSerializer() -> DataResponseSerializer<ONOXMLDocument> {
        return DataResponseSerializer { request, response, data, error in
            // Pass through any underlying URLSession error to the .network case.
            guard error == nil else { return .failure(BackendError.network(error: error!)) }

            // Use Alamofire's existing data serializer to extract the data, passing the error as nil, as it has
            // already been handled.
            let result = Request.serializeResponseData(response: response, data: data, error: nil)

            guard case let .success(validData) = result else {
                return .failure(BackendError.dataSerialization(error: result.error! as! AFError))
            }

            do {
                let xml = try ONOXMLDocument(data: validData)
                return .success(xml)
            } catch {
                return .failure(BackendError.xmlSerialization(error: error))
            }
        }
    }

    @discardableResult
    func responseXMLDocument(
        queue: DispatchQueue? = nil,
        completionHandler: @escaping (DataResponse<ONOXMLDocument>) -> Void)
        -> Self
    {
        return response(
            queue: queue,
            responseSerializer: DataRequest.xmlResponseSerializer(),
            completionHandler: completionHandler
        )
    }
}

泛型响应对象序列化

可以使用泛型来实现自动、类型安全的响应对象序列化。

protocol ResponseObjectSerializable {
    init?(response: HTTPURLResponse, representation: Any)
}

extension DataRequest {
    func responseObject<T: ResponseObjectSerializable>(
        queue: DispatchQueue? = nil,
        completionHandler: @escaping (DataResponse<T>) -> Void)
        -> Self
    {
        let responseSerializer = DataResponseSerializer<T> { request, response, data, error in
            guard error == nil else { return .failure(BackendError.network(error: error!)) }

            let jsonResponseSerializer = DataRequest.jsonResponseSerializer(options: .allowFragments)
            let result = jsonResponseSerializer.serializeResponse(request, response, data, nil)

            guard case let .success(jsonObject) = result else {
                return .failure(BackendError.jsonSerialization(error: result.error!))
            }

            guard let response = response, let responseObject = T(response: response, representation: jsonObject) else {
                return .failure(BackendError.objectSerialization(reason: "JSON could not be serialized: \(jsonObject)"))
            }

            return .success(responseObject)
        }

        return response(queue: queue, responseSerializer: responseSerializer, completionHandler: completionHandler)
    }
}
struct User: ResponseObjectSerializable, CustomStringConvertible {
    let username: String
    let name: String

    var description: String {
        return "User: { username: \(username), name: \(name) }"
    }

    init?(response: HTTPURLResponse, representation: Any) {
        guard
            let username = response.url?.lastPathComponent,
            let representation = representation as? [String: Any],
            let name = representation["name"] as? String
        else { return nil }

        self.username = username
        self.name = name
    }
}
Alamofire.request("https://example.com/users/mattt").responseObject { (response: DataResponse<User>) in
    debugPrint(response)

    if let user = response.result.value {
        print("User: { username: \(user.username), name: \(user.name) }")
    }
}

同样,这种方法也可以用来处理返回对象集合表示的端点。

protocol ResponseCollectionSerializable {
    static func collection(from response: HTTPURLResponse, withRepresentation representation: Any) -> [Self]
}

extension ResponseCollectionSerializable where Self: ResponseObjectSerializable {
    static func collection(from response: HTTPURLResponse, withRepresentation representation: Any) -> [Self] {
        var collection: [Self] = []

        if let representation = representation as? [[String: Any]] {
            for itemRepresentation in representation {
                if let item = Self(response: response, representation: itemRepresentation) {
                    collection.append(item)
                }
            }
        }

        return collection
    }
}
extension DataRequest {
    @discardableResult
    func responseCollection<T: ResponseCollectionSerializable>(
        queue: DispatchQueue? = nil,
        completionHandler: @escaping (DataResponse<[T]>) -> Void) -> Self
    {
        let responseSerializer = DataResponseSerializer<[T]> { request, response, data, error in
            guard error == nil else { return .failure(BackendError.network(error: error!)) }

            let jsonSerializer = DataRequest.jsonResponseSerializer(options: .allowFragments)
            let result = jsonSerializer.serializeResponse(request, response, data, nil)

            guard case let .success(jsonObject) = result else {
                return .failure(BackendError.jsonSerialization(error: result.error!))
            }

            guard let response = response else {
                let reason = "Response collection could not be serialized due to nil response."
                return .failure(BackendError.objectSerialization(reason: reason))
            }

            return .success(T.collection(from: response, withRepresentation: jsonObject))
        }

        return response(responseSerializer: responseSerializer, completionHandler: completionHandler)
    }
}
struct User: ResponseObjectSerializable, ResponseCollectionSerializable, CustomStringConvertible {
    let username: String
    let name: String

    var description: String {
        return "User: { username: \(username), name: \(name) }"
    }

    init?(response: HTTPURLResponse, representation: Any) {
        guard
            let username = response.url?.lastPathComponent,
            let representation = representation as? [String: Any],
            let name = representation["name"] as? String
        else { return nil }

        self.username = username
        self.name = name
    }
}
Alamofire.request("https://example.com/users").responseCollection { (response: DataResponse<[User]>) in
    debugPrint(response)

    if let users = response.result.value {
        users.forEach { print("- \($0)") }
    }
}

安全

在与服务器和Web服务进行通信时,使用安全的HTTPS连接是保护敏感数据的重要步骤。默认情况下,Alamofire将使用Security框架提供的内置验证来评估服务器提供的证书链。虽然这保证了证书链是有效的,但它并不能防止中间人攻击(MITM)或其他潜在的安全漏洞。为了缓解MITM攻击,处理敏感客户数据或财务信息的应用程序应该使用ServerTrustPolicy提供的证书或公钥固定。

ServerTrustPolicy

ServerTrustPolicy枚举负责评估在通过安全的HTTPS连接连接到服务器时,URLAuthenticationChallenge提供的服务器信任。

let serverTrustPolicy = ServerTrustPolicy.pinCertificates(
    certificates: ServerTrustPolicy.certificatesInBundle(),
    validateCertificateChain: true,
    validateHost: true
)

有许多种服务器信任评估情况,让您可以完全控制验证过程。

  • performDefaultEvaluation:使用默认的服务器信任评估,同时允许您控制是否验证挑战提供的宿主。
  • pinCertificates:使用固定的证书来验证服务器信任。如果其中一个固定的证书与服务器证书中的任何一个匹配,则认为服务器信任是有效的。
  • pinPublicKeys:使用固定的公钥来验证服务器信任。如果其中一个固定的公钥与服务器证书公钥中的任何一个匹配,则认为服务器信任是有效的。
  • disableEvaluation:禁用所有评估,这会将任何服务器信任始终视为有效。
  • customEvaluation:使用相关的闭包评估服务器信任的有效性,从而让您完全控制验证过程。请谨慎使用。

Server Trust Policy Manager

ServerTrustPolicyManager负责将服务器信任策略存储为特定主机的内部映射。这使得Alamofire可以针对不同的服务器信任策略评估每个主机。

let serverTrustPolicies: [String: ServerTrustPolicy] = [
    "test.example.com": .pinCertificates(
        certificates: ServerTrustPolicy.certificatesInBundle(),
        validateCertificateChain: true,
        validateHost: true
    ),
    "insecure.expired-apis.com": .disableEvaluation
]

let sessionManager = SessionManager(
    serverTrustPolicyManager: ServerTrustPolicyManager(policies: serverTrustPolicies)
)

请确保保留新的SessionManager实例的引用,否则当您的sessionManager被释放时,您的所有请求都将被取消。

以下服务器信任策略将导致以下行为

  • test.example.com始终使用证书固定,启用证书链和宿主验证,因此需要满足以下条件才能允许TLS握手成功
    • 证书链MUST是有效的。
    • 证书链MUST包含一个固定的证书。
    • 挑战宿主MUST与证书链的叶证书中的宿主匹配。
  • insecure.expired-apis.com永远不会评估证书链,并且始终允许TLS握手成功。
  • 所有其他宿主将使用苹果提供的默认评估。
子类化服务器信任策略管理器

如果您需要更灵活的服务器信任策略匹配行为(例如通配符域名),则可以子类化 ServerTrustPolicyManager 并重写 serverTrustPolicyForHost 方法以实现您自己的自定义实现。

class CustomServerTrustPolicyManager: ServerTrustPolicyManager {
    override func serverTrustPolicy(forHost host: String) -> ServerTrustPolicy? {
        var policy: ServerTrustPolicy?

        // Implement your custom domain matching behavior...

        return policy
    }
}

验证主机

服务信任策略 .performDefaultEvaluation.pinCertificates.pinPublicKeys 都接受一个 validateHost 参数。将值设置为 true 将导致服务器信任评估验证证书中的主机名是否与挑战的主机名匹配。如果它们不匹配,评估将失败。将 validateHost 的值设置为 false 将仍会评估完整的证书链,但不会验证叶证书的主机名。

建议在生产环境中始终将 validateHost 设置为 true

验证证书链

固定证书和公钥都提供了使用 validateCertificateChain 参数验证证书链的选项。通过将此值设置为 true,将在对固定证书或公钥执行字节相等性检查之外评估完整的证书链。将值设置为 false 将跳过证书链验证,但仍然执行字节相等性检查。

在某些情况下,禁用证书链验证是有意义的。禁用验证的最常见用例是自签名证书和过期证书。在这两种情况下,评估都会失败,但是字节相等性检查将确保您从服务器收到了您预期的证书。

建议在生产环境中始终将 validateCertificateChain 设置为 true

应用程序传输安全

随着iOS 9中引入应用传输安全(ATS),使用多个ServerTrustPolicy对象的自定义ServerTrustPolicyManager可能没有任何效果。如果您持续遇到CFNetwork SSLHandshake failed (-9806)错误,这可能就是您遇到的问题。除非您在应用配置文件(plist)中配置ATS设置以禁用一部分功能,否则Apple的ATS系统会覆盖整个挑战系统,以便您的应用评估服务器信任。

如果您遇到这个问题(自签名证书概率较高),可以通过以下方式将其添加到您的Info.plist中解决此问题。

<dict>
	<key>NSAppTransportSecurity</key>
	<dict>
		<key>NSExceptionDomains</key>
		<dict>
			<key>example.com</key>
			<dict>
				<key>NSExceptionAllowsInsecureHTTPLoads</key>
				<true/>
				<key>NSExceptionRequiresForwardSecrecy</key>
				<false/>
				<key>NSIncludesSubdomains</key>
				<true/>
				<!-- Optional: Specify minimum TLS version -->
				<key>NSTemporaryExceptionMinimumTLSVersion</key>
				<string>TLSv1.2</string>
			</dict>
		</dict>
	</dict>
</dict>

您是否需要将NSExceptionRequiresForwardSecrecy设置为NO取决于您的TLS连接是否使用允许的加密套件。在特定情况下,可能需要将其设置为NO。为了允许SessionDelegate接收挑战回调,必须将NSExceptionAllowsInsecureHTTPLoads设置为YES。一旦调用挑战回调,ServerTrustPolicyManager将接管服务器信任评估。如果您尝试连接到只支持小于1.2的TLS版本的宿主机,可能还需要指定NSTemporaryExceptionMinimumTLSVersion

建议在生产环境中始终使用有效的证书。

网络可达性

NetworkReachabilityManager侦听全球移动通信网络(WWAN)和WiFi网络接口中主机和地址的可达性变化。

let manager = NetworkReachabilityManager(host: "www.apple.com")

manager?.listener = { status in
    print("Network Status Changed: \(status)")
}

manager?.startListening()

请确保记住在上面的示例中保留manager,否则不会报告状态变化。

使用网络可达性确定下一步要做什么时,有一些重要的事项需要记住。

  • 不要使用可达性来确定是否应该发送网络请求。
    • 您应该始终发送它。
  • 当可达性恢复时,请使用事件重试失败的网络请求。
    • 即使网络请求仍然可能失败,这也是重新尝试它们的好时机。
  • 网络可达性状态可以用来确定为什么网络请求可能失败。
    • 如果网络请求失败,告诉用户网络请求失败是因为离线而不是更技术性的错误(如“请求超时”),将更有用。

建议查看WWDC 2012 讲座 706,“网络最佳实践”以获取更多信息。


公开雷达

以下雷达对Alamofire当前实现有影响。

  • rdar://21349340 - 测试案例中的免插拔桥接问题导致编译器发出警告
  • rdar://26761490 - Swift字符串插值在常规使用中引起内存泄漏
  • rdar://26870455 - 在模拟器中背景URL会话配置不起作用
  • rdar://26849668 - 一些URLProtocol API没有正确处理URLRequest

常见问题

Alamofire这个名字的由来是什么?

Alamofire的名字来源于阿拉莫火烈花,这是德克萨斯州官方州花蓝骨海棠的一个杂交变种。

什么是路由器与请求适配器中的逻辑?

简单静态数据,如路径、参数和常见头信息,应放在Router中。动态数据,如根据认证系统可以改变值的Authorization头信息,应放在RequestAdapter中。

将动态数据必须放在RequestAdapter中的原因是支持重试操作。当请求重试时,原始请求不会被重建,这意味着不会再次调用Router。再次调用RequestAdapter允许在重试请求之前更新原始请求上的动态数据。


致谢

Alamofire由Alamofire软件基金会拥有和维护。您可以在Twitter上关注他们@AlamofireSF以获取项目更新和版本发布。

安全披露

如果您相信您已经识别出 Alamofire 中的安全漏洞,请尽快通过电子邮件报告,电子邮件地址为:[email protected]。请勿将其发布在公开的问题跟踪器上。

捐助

ASF 正在寻求筹集资金以正式注册为联邦非营利组织。注册将使会员们获得一些法律保护,并允许我们将捐款免税使用。向 ASF 捐助将使我们能够

  • 支付我们注册为联邦非营利组织的法律费用
  • 支付我们保持非营利组织良好状态年度法律费用
  • 支付我们的邮件服务器费用,以帮助我们处理所有问题和安全问题
  • 可能资助测试服务器,让我们更容易测试边缘情况
  • 可能资助开发者全职从事我们的一些项目

ASF 库在社区中的采用非常令人惊叹。我们对您对项目表现出的热情深感谦卑,并希望继续尽我们所能推动项目向前发展。随着您的持续支持,ASF 将能够扩大其影响范围,并为核心成员提供更好的法律保障。如果您在工作中使用我们的任何库,请看看您的雇主是否对捐助感兴趣。我们的初始目标是筹集1000美元,以便我们将所有法律问题都安排妥当,并启动这次活动。您今天能捐赠的任何金额都能帮助我们达到我们的目标,都将非常感激。

Click here to lend your support to: Alamofire Software Foundation and make a donation at pledgie.com !

许可证

Alamofire 在 MIT 许可证下发布。有关详细信息,请参阅 LICENSE。