Alamofire就是牛逼的框架AFNetwork开发者的母公司开发的一套基于swift实现的网络框架。Alamofire专注于核心网络的实现,Alamofire生态系统还有另外两个库:AlamofireImage,AlamofireNetworkActivityIndicator
- AlamofireImage:
一个图片库,包括图像响应序列化器、UIImage和UIImageView的扩展、自定义图像滤镜、内存中自动清除和基于优先级的图像下载系统。
- AlamofireNetworkActivityIndicator:
控制iOS应用的网络活动指示器。包含可配置的延迟计时器来帮助减少闪光,并且支持不受Alamofire管理的URLSession实例。
- 链式请求 / 响应方法
- URL / JSON / plist参数编码
- 上传文件 / 数据 / 流 / 多表单数据
- 使用请求或者断点下载来下载文件
- 使用URL凭据进行身份认证
- HTTP响应验证
- 包含进度的上传和下载闭包
- cURL命令的输出
- 动态适配和重试请求
- TLS证书和Public Key Pinning
- 网络可达性
- 全面的单元和集成测试覆盖率
iOS 10.0+ / macOS 10.12+ / tvOS 10.0+ / watchOS 3.0+
Xcode 10.2+
Swift 5+
source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '10.0'
use_frameworks!
target '项目名称' do
pod 'Alamofire', '~> 5.0.0-beta.5'
end
Alamofire.request("http://qq.com/")
实例1:
Alamofire.request("https://httpbin.org/get").responseJSON { response in
print(response.request) // 原始的URL请求
print(response.response) // HTTP URL响应
print(response.data) // 服务器返回的数据
print(response.result) // 响应序列化结果,在这个闭包里,存储的是JSON数据
if let JSON = response.result.value {
print("JSON: \(JSON)")
}
}
在上面的例子中,responseJSON handler直接拼接到请求后面,当请求完成后被调用。这个闭包一旦收到响应后,就会处理这个响应,并不会因为等待服务器的响应而造成阻塞执行。请求的结果仅在响应闭包的范围内可用。其他任何与服务器返回的响应或者数据相关的操作,都必须在这个闭包内执行。
实例2:
// 响应 Handler - 未序列化的响应
func response(
queue: DispatchQueue?,
completionHandler: @escaping (DefaultDataResponse) -> Void)
-> Self
// 响应数据 Handler - 序列化成数据类型
func responseData(
queue: DispatchQueue?,
completionHandler: @escaping (DataResponse<Data>) -> Void)
-> Self
// 响应字符串 Handler - 序列化成字符串类型
func responseString(
queue: DispatchQueue?,
encoding: String.Encoding?,
completionHandler: @escaping (DataResponse<String>) -> Void)
-> Self
// 响应 JSON Handler - 序列化成Any类型
func responseJSON(
queue: DispatchQueue?,
completionHandler: @escaping (DataResponse<Any>) -> Void)
-> Self
// 响应 PropertyList (plist) Handler - 序列化成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.error)")
if let data = response.data, let utf8Text = String(data: data, encoding: .utf8) {
print("Data: \(utf8Text)")
}
}
responseDataSerializer
(这个对象把服务器的数据序列化成其他类型)来提取服务器返回的数据。如果没有返回错误并且有数据返回,那么响应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)")
}
}
responseStringSerializer
对象根据指定的编码格式把服务器返回的数据转换成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)")
}
HTTPURLResponse
指定的格式。如果服务器无法确定编码格式,那么默认使用.isoLatin1
。responseJSONSerializer
根据指定的JSONSerialization.ReadingOptions
把服务器返回的数据转换成Any类型。如果没有返回错误并且服务器的数据成功地转换为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)")
}
}
Alamofire.request("https://httpbin.org/get")
.responseString { response in
print("Response String: \(response.result.value)")
}
.responseJSON { response in
print("Response JSON: \(response.result.value)")
}
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把所有完成的请求当做是成功的请求,无论响应的内容是什么。如果响应有一个不能被接受的状态码或者MIME类型,在响应handler之前调用validate将会产生错误。
手动验证:
Alamofire.request("https://httpbin.org/get")
.validate(statusCode: 200..<300)
.validate(contentType: ["application/json"])
.responseData { response in
switch response.result {
case .success:
print("Validation Successful")
case .failure(let error):
print(error)
}
}
Alamofire.request("https://httpbin.org/get").validate().responseJSON { response in
switch response.result {
case .success:
print("Validation Successful")
case .failure(let error):
print(error)
}
}
HTTPMethod
列举了下面的这些方法: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("https://httpbin.org/get") // 默认是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)
let headers: HTTPHeaders = [
"Authorization": "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==",
"Accept": "application/json"
]
Alamofire.request("https://httpbin.org/headers", headers: headers).responseJSON { response in
debugPrint(response)
}
URLSessionConfiguration
设置,这样就可以自动被用于任何URLSession
创建的URLSessionTask
。
Accept-Encoding
,默认是gzip;q=1.0, compress;q=0.5。
Accept-Language
,默认是系统的前6个偏好语言,格式类似于en;q=1.0。
User-Agent
,包含当前应用程序的版本信息。例如iOS Example/1.0 (com.alamofire.iOS-Example; build:1; iOS 10.0.0) Alamofire/4.0.0。
URLSessionConfiguration
,defaultHTTPHeaders
属性将会被更新,并且自定义的会话配置也会应用到新的SessionManager
实例。URLCredential
和URLAuthenticationChallenge
实现的。HTTP Basic
HTTP Digest
Kerberos
NTLM
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)
}
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)
}
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)
}
URL
、JSON
和PropertyList
。还支持遵循了ParameterEncoding
协议的自定义编码。URLEncoding
类型创建了一个URL编码的查询字符串来设置或者添加到一个现有的URL查询字符串,或者设置URL请求的请求体。查询字符串是否被设置或者添加到现有的URL查询字符串,或者被作为HTTP请求体,决定于编码的Destination
。编码的Destination有三个case:
.methodDependent
:为GET、HEAD和DELETE请求使用编码查询字符串来设置或者添加到现有查询字符串,并且使用其他HTTP方法来设置请求体。
.queryString
:设置或者添加编码查询字符串到现有查询字符串
.httpBody
:把编码查询字符串作为URL请求的请求体
一个编码请求的请求体的Content-Type
字段被设置为application/x-www-form-urlencoded; charset=utf-8
。因为没有公开的标准说明如何编码集合类型,所以按照惯例在key
后面添加[]
来表示数组的值(foo[]=1&foo[]=2
),在key外面包一个中括号来表示字典的值(foo[bar]=baz
)。
使用URL编码参数的GET请求
let parameters: Parameters = ["foo": "bar"]
// 下面这三种写法是等价的
Alamofire.request("https://httpbin.org/get", parameters: parameters) // encoding 默认是`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
let parameters: Parameters = [
"foo": "bar",
"baz": ["a", 1],
"qux": [
"x": 1,
"y": 2,
"z": 3
]
]
// 下面这三种写法是等价的
Alamofire.request("https://httpbin.org/post", method: .post, parameters: parameters)
Alamofire.request("https://httpbin.org/post", method: .post, parameters: parameters, encoding: URLEncoding.default)
Alamofire.request("https://httpbin.org/post", method: .post, parameters: parameters, encoding: URLEncoding.httpBody)
// HTTP body: foo=bar&baz[]=a&baz[]=1&qux[x]=1&qux[y]=2&qux[z]=3
.numeric
:把true编码为1,false编码为0
.literal
:把true编码为true,false编码为false
let encoding = URLEncoding(boolEncoding: .literal)
.brackets
: 在每个元素值的key后面加上一个[],如foo=[1,2]编码成foo[]=1&foo[]=2
.noBrackets
:不添加[],例如foo=[1,2]编码成``foo=1&foo=2`
let encoding = URLEncoding(arrayEncoding: .noBrackets)
JSONEncoding
类型创建了一个JOSN对象,并作为请求体。编码请求的请求头的Content-Type请求字段被设置为application/json
。let parameters: Parameters = [
"foo": [1,2,3],
"bar": [
"baz": "qux"
]
]
// 下面这两种写法是等价的
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
来创建一个属性列表对象,并作为请求体。编码请求的请求头的Content-Type
请求字段被设置为application/x-plist
。ParameterEncoding
类型不能满足我们的要求,可以创建自定义编码。下面演示如何快速自定义一个JSONStringArrayEncoding
类型把JSON字符串数组编码到请求中。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
}
}
ParameterEncodingAPI
可以在创建网络请求外面使用。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)
Alamofire可以把服务器的数据下载到内存(in-memory)或者硬盘(on-disk)中。所有Alamofire.requestAPI下载的数据都是存储在内存中。这比较适合小文件,更高效;但是不适合大文件,因为大文件会把内存耗尽。我们要使用Alamofire.downloadAPI把服务器的数据下载到硬盘中。
下面这个方法只适用于macOS。因为在其他平台不允许在应用沙盒外访问文件系统。下面会讲到如何在其他平台下载文件。
Alamofire.download("https://httpbin.org/image/png").responseData { response in
if let data = response.result.value {
let image = UIImage(data: data)
}
}
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.error == nil, let imagePath = response.destinationURL?.path {
let image = UIImage(contentsOfFile: imagePath)
}
}
let destination = DownloadRequest.suggestedDownloadDestination(directory: .documentDirectory)
Alamofire.download("https://httpbin.org/image/png", to: destination)
DownloadRequest
都可以使用downloadProgressAPI
来反馈下载进度。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)
}
}
downloadProgressAPI
还可以接受一个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会话会生成一个恢复数据。恢复数据可以被重新利用并在中断的位置继续下载。恢复数据可以通过下载响应访问,然后在重新开始请求的时候被利用。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
}
}
}
}
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)
}
}
)
let fileURL = Bundle.main.url(forResource: "video", withExtension: "mov")
Alamofire.upload(fileURL, to: "https://httpbin.org/post")
.uploadProgress { progress in // 默认在主线程中执行
print("Upload Progress: \(progress.fractionCompleted)")
}
.downloadProgress { progress in // 默认在主线程中执行
print("Download Progress: \(progress.fractionCompleted)")
}
.responseJSON { response in
debugPrint(response)
}
Alamofire.request("https://httpbin.org/get").responseJSON { response in
print(response.timeline)
}
上面的Timeline信息包括:
Latency: 0.428 seconds (延迟)
Request Duration: 0.428 seconds (请求时间)
Serialization Duration: 0.001 seconds (序列化时间)
Total Duration: 0.429 seconds (总时间)
Alamofire.request("https://httpbin.org/get").responseJSON { response in
print(response.metrics)
}
Alamofire.request("https://httpbin.org/get").responseJSON { response in
if #available(iOS 10.0. *) {
print(response.metrics)
}
}
CustomStringConvertible
和CustomDebugStringConvertible
协议来提供一些非常有用的调试工具。let request = Alamofire.request("https://httpbin.org/ip")
print(request)
// GET https://httpbin.org/ip (200)
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.request("https://httpbin.org/get")
let sessionManager = Alamofire.SessionManager.default
sessionManager.request("https://httpbin.org/get")
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)
//不推荐在Authorization或者Content-Type header使用。而应该使用Alamofire.requestAPI、URLRequestConvertible和ParameterEncoding的headers参数。
var defaultHeaders = Alamofire.SessionManager.defaultHTTPHeaders
defaultHeaders["DNT"] = "1 (Do Not Track Enabled)"
let configuration = URLSessionConfiguration.default
configuration.httpAdditionalHeaders = defaultHeaders
let sessionManager = Alamofire.SessionManager(configuration: configuration)
默认情况下,一个SessionManager
实例创建一个SessionDelegate
对象来处理底层URLSession
生成的不同类型的代理回调。每个代理方法的实现处理常见的情况。然后,高级用户可能由于各种原因需要重写默认功能。
有两种方式实现SessionDelegate
:
方式一:自定义SessionDelegate
的方法是通过重写闭包。我们可以在每个闭包重写SessionDelegate
API对应的实现。
方式二:重写SessionDelegate
的实现的方法是把它子类化。通过子类化,我们可以完全自定义他的行为,或者为这个API创建一个代理并且仍然使用它的默认实现。通过创建代理,我们可以跟踪日志事件、发通知、提供前后实现。
实现SessionDelegate
代码实例:
/// 重写URLSessionDelegate的`urlSession(_:didReceive:completionHandler:)`方法
open var sessionDidReceiveChallenge: ((URLSession, URLAuthenticationChallenge) -> (URLSession.AuthChallengeDisposition, URLCredential?))?
/// 重写URLSessionDelegate的`urlSessionDidFinishEvents(forBackgroundURLSession:)`方法
open var sessionDidFinishEventsForBackgroundURLSession: ((URLSession) -> Void)?
/// 重写URLSessionTaskDelegate的`urlSession(_:task:willPerformHTTPRedirection:newRequest:completionHandler:)`方法
open var taskWillPerformHTTPRedirection: ((URLSession, URLSessionTask, HTTPURLResponse, URLRequest) -> URLRequest?)?
/// 重写URLSessionDataDelegate的`urlSession(_:dataTask:willCacheResponse:completionHandler:)`方法
open var dataTaskWillCacheResponse: ((URLSession, URLSessionDataTask, CachedURLResponse) -> CachedURLResponse?)?
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
}
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
)
}
}
request
、download
、upload
和stream
方法的结果是DataRequest
、DownloadRequest
、UploadRequest
和StreamRequest
,并且所有请求都继承自Request
。所有的Request
并不是直接创建的,而是由session manager
创建的。
每个子类都有特定的方法,例如authenticate
、validate
、responseJSON
和uploadProgress
,都返回一个实例,以便方法链接(也就是用点语法连续调用方法)。
请求可以被暂停、恢复和取消:
suspend()
:暂停底层的任务和调度队列
resume()
:恢复底层的任务和调度队列。如果manager的startRequestsImmediately不是true,那么必须调用resume()来开始请求。
cancel()
:取消底层的任务,并产生一个error,error被传入任何已经注册的响应handlers。
Router
设计模式的URLConvertible
和URLRequestConvertible
协议可以帮助我们URLConvertible
协议的类型可以被用来构建URL,然后用来创建URL请求。String、URL和URLComponent默认是遵循URLConvertible协议的。它们都可以作为url参数传入request
、upload
和download
方法.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(urlComponents, method: .post)
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
协议的类型可以被用来构建URL请求。URLRequest
默认遵循了URLRequestConvertible
,允许被直接传入request
、upload
和download
(推荐用这种方法为单个请求自定义请求头)URLRequestConvertible
协议的类型来保证请求端点的一致性。这种方法可以用来抽象服务器端的不一致性,并提供类型安全传送,以及管理身份验证凭据和其他状态。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)
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)) // https://example.com/search?q=foo%20bar&offset=50
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 https://example.com/users/mattt
现在的大多数Web服务,都需要身份认证。现在比较常见的是OAuth
。通常是需要一个access token
来授权应用或者用户,然后才可以使用各种支持的Web服务。创建这些access token
是比较麻烦的,当access token
过期之后就比较麻烦了,我们需要重新创建一个新的。有许多线程安全问题要考虑。
RequestAdapter
和RequestRetrier
协议可以让我们更容易地为特定的Web服务创建一个线程安全的认证系统。
class AccessTokenAdapter: RequestAdapter {
private let accessToken: String
init(accessToken: String) {
self.accessToken = accessToken
}
func adapt(_ urlRequest: URLRequest) throws -> URLRequest {
var urlRequest = urlRequest
if let urlString = urlRequest.url?.absoluteString, 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
协议允许一个在执行过程中遇到error的请求被重试。当一起使用RequestAdapter
和RequestRetrier
协议时,我们可以为OAuth1
、OAuth2
、Basic Auth
(每次请求API都要提供用户名和密码)甚至是exponential backoff
重试策略创建资格恢复系统。下面的例子演示了如何实现一个OAuth2 access token
的恢复流程。实例282:
注意:下面代码不是一个全面的OAuth2解决方案。这仅仅是演示如何把RequestAdapter和RequestRetrier协议结合起来创建一个线程安全的恢复系统。
重申: 不要把这个例子复制到实际的开发应用中,这仅仅是一个例子。每个认证系统必须为每个特定的平台和认证类型重新定制。
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 urlString = urlRequest.url?.absoluteString, 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: Any],
let accessToken = json["access_token"] as? String,
let refreshToken = json["refresh_token"] as? String
{
completion(true, accessToken, refreshToken)
} 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
为SessionManager
被应用与adapter
和retrier
,他将会通过自动恢复access token
来处理一个非法的access token error
,并且根据失败的顺序来重试所有失败的请求。(如果需要让他们按照创建的时间顺序来执行,可以使用他们的task identifier
来排序)
上面这个例子仅仅检查了401
响应码,不是演示如何检查一个非法的access token error
。在实际开发应用中,我们想要检查realm
和www-authenticate header
响应,虽然这取决于OAuth2
的实现。
还有一个要重点注意的是,这个认证系统可以在多个session manager
之间共享。例如,可以在同一个Web服务集合使用default
和ephemeral
会话配置。上面这个例子可以在多个session manager
间共享一个oauthHandler
实例,来管理一个恢复流程。
data
、strings
、JSON
和Property List
提供了内置的响应序列化:Alamofire.request(...).responseData { (resp: DataResponse<Data>) in ... }
Alamofire.request(...).responseString { (resp: DataResponse<String>) in ... }
Alamofire.request(...).responseJSON { (resp: DataResponse<Any>) in ... }
Alamofire.request(...).responsePropertyList { resp: DataResponse<Any>) in ... }
Data
, String
, Any
)或者error (network
, validation
errors
),以及元数据 (URL Request, HTTP headers, status code, metrics, …)。Alamofire.request("https://example.com/users/mattt").responseJSON { (response: DataResponse<Any>) in
let userResponse = response.map { json in
// We assume an existing User(json: Any) initializer
return User(json: json)
}
// Process userResponse, of type DataResponse:
if let user = userResponse.value {
print("User: { username: \(user.username), name: \(user.name) }")
}
}
Alamofire.request("https://example.com/users/mattt").responseJSON { response in
let userResponse = response.flatMap { json in
try User(json: json)
}
}
@discardableResult
func loadUser(completionHandler: @escaping (DataResponse<User>) -> Void) -> Alamofire.DataRequest {
return Alamofire.request("https://example.com/users/mattt").responseJSON { response in
let userResponse = response.flatMap { json in
try User(json: json)
}
completionHandler(userResponse)
}
}
loadUser { response in
if let user = userResponse.value {
print("User: { username: \(user.username), name: \(user.name) }")
}
}
@discardableResult
func loadUser(completionHandler: @escaping (DataResponse<User>) -> Void) -> Alamofire.DataRequest {
let utilityQueue = DispatchQueue.global(qos: .utility)
return Alamofire.request("https://example.com/users/mattt").responseJSON(queue: utilityQueue) { response in
let userResponse = response.flatMap { json in
try User(json: json)
}
DispatchQueue.main.async {
completionHandler(userResponse)
}
}
}
enum BackendError: Error {
case network(error: Error) // 捕获任何从URLSession API产生的错误
case dataSerialization(error: Error)
case jsonSerialization(error: Error)
case xmlSerialization(error: Error)
case objectSerialization(reason: String)
}
strings
、JSON
和Property List
提供了内置的响应序列化,但是我们可以通过扩展Alamofire.DataRequest
或者Alamofire.DownloadRequest
来添加其他序列化。extension DataRequest {
static func xmlResponseSerializer() -> DataResponseSerializer<ONOXMLDocument> {
return DataResponseSerializer { request, response, data, error in
// 把任何底层的URLSession error传递给 .network case
guard error == nil else { return .failure(BackendError.network(error: error!)) }
// 使用Alamofire已有的数据序列化器来提取数据,error为nil,因为上一行代码已经把不是nil的error过滤了
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
)
}
}
泛型可以用来提供自动的、类型安全的响应对象序列化。
代码1
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)") }
}
}
man-in-the-middle
(MITM
)攻击或者其他潜在的漏洞。为了减少MITM
攻击,处理用户的敏感数据或财务信息的应用,应该使用ServerTrustPolicy
提供的certificate
或者public key pinning
。let serverTrustPolicy = ServerTrustPolicy.pinCertificates(
certificates: ServerTrustPolicy.certificates(),
validateCertificateChain: true,
validateHost: true
)
属性 | 作用 |
---|---|
performDefaultEvaluation | 使用默认的server trust评估,允许我们控制是否验证challenge提供的host。 |
pinCertificates | 使用pinned certificates来验证server trust。如果pinned certificates匹配其中一个服务器证书,那么认为server trust是有效的。 |
pinPublicKeys | 使用pinned public keys来验证server trust。如果pinned public keys匹配其中一个服务器证书公钥,那么认为server trust是有效的。 |
disableEvaluation | 禁用所有评估,总是认为server trust是有效的。 |
customEvaluation | 使用相关的闭包来评估server trust的有效性,我们可以完全控制整个验证过程。但是要谨慎使用。 |
let serverTrustPolicies: [String: ServerTrustPolicy] = [
"test.example.com": .pinCertificates(
certificates: ServerTrustPolicy.certificates(),
validateCertificateChain: true,
validateHost: true
),
"insecure.expired-apis.com": .disableEvaluation
]
let sessionManager = SessionManager(
serverTrustPolicyManager: ServerTrustPolicyManager(policies: serverTrustPolicies)
)
test.example.com
:始终使用证书链固定的证书和启用主机验证,因此需要以下条件才能是TLS握手成功:
(1) 证书链必须是有效的。
(2) 证书链必须包含一个已经固定的证书。
(3) Challenge主机必须匹配主机证书链的子证书。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
,服务器信任评估就会验证与challenge
主机名字匹配的在证书里面的主机名字。如果他们不匹配,验证失败。如果设置为false
,仍然会评估整个证书链,但是不会验证子证书的主机名字。validateHost
设置为true
。Pinning certificate
和 public keys
都可以通过validateCertificateChain
参数拥有验证证书链的选项。把它设置为true
,除了对Pinning certificate
和 public keys
进行字节相等检查外,还将会验证整个证书链。如果是false
,将会跳过证书链验证,但还会进行字节相等检查。validateCertificateChain
设置为true
。ServerTrustPolicyManager
和多个ServerTrustPolicy
对象可能没什么影响。如果我们不断看到CFNetwork
SSLHandshake failed (-9806)错误
,我们可能遇到了这个问题。苹果的ATS
系统重写了整个challenge系统,除非我们在plist
文件中配置ATS
设置来允许应用评估服务器信任。<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/>
<!-- 可选的: 指定TLS的最小版本 -->
<key>NSTemporaryExceptionMinimumTLSVersion</key>
<string>TLSv1.2</string>
</dict>
</dict>
</dict>
</dict>
NSExceptionRequiresForwardSecrecy
设置为NO
取决于TLS连接是否使用一个允许的密码套件。在某些情况下,它需要设置为NO。NSExceptionAllowsInsecureHTTPLoads
必须设置为YES
,然后SessionDelegate
才能接收到challenge
回调。一旦challenge
回调被调用,ServerTrustPolicyManager
将接管服务器信任评估。如果我们要连接到一个仅支持小于1.2版本的TSL主机,那么还要指定NSTemporaryExceptionMinimumTLSVersion
。let manager = NetworkReachabilityManager(host: "www.apple.com")
manager?.listener = { status in
print("Network Status Changed: \(status)")
}
manager?.startListening()