Alamofire是在URLSession和URL加载系统的基础上写的。所以,为了更好地学习这个框架,建议先熟悉下列几个底层网络协议栈:

Session Manager

高级别的方便的方法,例如Alamofire.request,使用的是默认的Alamofire.SessionManager,并且这个SessionManager是用默认URLSessionConfiguration配置的。

例如,下面两个语句是等价的:

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

我们可以自己创建后台会话和短暂会话的session manager,还可以自定义默认的会话配置来创建新的session manager,例如修改默认的header httpAdditionalHeaderstimeoutIntervalForRequest

用默认的会话配置创建一个Session Manager

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

用后台会话配置创建一个Session Manager

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

用默短暂会话配置创建一个Session Manager

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

修改会话配置

  1. var defaultHeaders = Alamofire.SessionManager.defaultHTTPHeaders
  2. defaultHeaders["DNT"] = "1 (Do Not Track Enabled)"
  3. let configuration = URLSessionConfiguration.default
  4. configuration.httpAdditionalHeaders = defaultHeaders
  5. let sessionManager = Alamofire.SessionManager(configuration: configuration)

注意:不推荐在Authorization或者Content-Type header使用。而应该使用Alamofire.requestAPI、URLRequestConvertibleParameterEncoding的headers参数。

会话代理

默认情况下,一个SessionManager实例创建一个SessionDelegate对象来处理底层URLSession生成的不同类型的代理回调。每个代理方法的实现处理常见的情况。然后,高级用户可能由于各种原因需要重写默认功能。

重写闭包

第一种自定义SessionDelegate的方法是通过重写闭包。我们可以在每个闭包重写SessionDelegate API对应的实现。下面是重写闭包的示例:

  1. /// 重写URLSessionDelegate的`urlSession(_:didReceive:completionHandler:)`方法
  2. open var sessionDidReceiveChallenge: ((URLSession, URLAuthenticationChallenge) -> (URLSession.AuthChallengeDisposition, URLCredential?))?
  3. /// 重写URLSessionDelegate的`urlSessionDidFinishEvents(forBackgroundURLSession:)`方法
  4. open var sessionDidFinishEventsForBackgroundURLSession: ((URLSession) -> Void)?
  5. /// 重写URLSessionTaskDelegate的`urlSession(_:task:willPerformHTTPRedirection:newRequest:completionHandler:)`方法
  6. open var taskWillPerformHTTPRedirection: ((URLSession, URLSessionTask, HTTPURLResponse, URLRequest) -> URLRequest?)?
  7. /// 重写URLSessionDataDelegate的`urlSession(_:dataTask:willCacheResponse:completionHandler:)`方法
  8. open var dataTaskWillCacheResponse: ((URLSession, URLSessionDataTask, CachedURLResponse) -> CachedURLResponse?)?

下面的示例演示了如何使用taskWillPerformHTTPRedirection来避免回调到任何apple.com域名。

  1. let sessionManager = Alamofire.SessionManager(configuration: URLSessionConfiguration.default)
  2. let delegate: Alamofire.SessionDelegate = sessionManager.delegate
  3. delegate.taskWillPerformHTTPRedirection = { session, task, response, request in
  4. var finalRequest = request
  5. if
  6. let originalRequest = task.originalRequest,
  7. let urlString = originalRequest.url?.urlString,
  8. urlString.contains("apple.com")
  9. {
  10. finalRequest = originalRequest
  11. }
  12. return finalRequest
  13. }

子类化

另一个重写SessionDelegate的实现的方法是把它子类化。通过子类化,我们可以完全自定义他的行为,或者为这个API创建一个代理并且仍然使用它的默认实现。通过创建代理,我们可以跟踪日志事件、发通知、提供前后实现。下面这个例子演示了如何子类化SessionDelegate,并且有回调的时候打印信息:

  1. class LoggingSessionDelegate: SessionDelegate {
  2. override func urlSession(
  3. _ session: URLSession,
  4. task: URLSessionTask,
  5. willPerformHTTPRedirection response: HTTPURLResponse,
  6. newRequest request: URLRequest,
  7. completionHandler: @escaping (URLRequest?) -> Void)
  8. {
  9. print("URLSession will perform HTTP redirection to request: \(request)")
  10. super.urlSession(
  11. session,
  12. task: task,
  13. willPerformHTTPRedirection: response,
  14. newRequest: request,
  15. completionHandler: completionHandler
  16. )
  17. }
  18. }

总的来说,无论是默认实现还是重写闭包,都应该提供必要的功能。子类化应该作为最后的选择。

请求

requestdownloaduploadstream方法的结果是DataRequestDownloadRequestUploadRequestStreamRequest,并且所有请求都继承自Request。所有的Request并不是直接创建的,而是由session manager创建的。

每个子类都有特定的方法,例如authenticatevalidateresponseJSONuploadProgress,都返回一个实例,以便方法链接(也就是用点语法连续调用方法)。

请求可以被暂停、恢复和取消:

  • suspend():暂停底层的任务和调度队列
  • resume():恢复底层的任务和调度队列。如果manager的startRequestsImmediately不是true,那么必须调用resume()来开始请求。
  • cancel():取消底层的任务,并产生一个error,error被传入任何已经注册的响应handlers。

传送请求

随着应用的不多增大,当我们建立网络栈的时候要使用通用的模式。在通用模式的设计中,一个很重要的部分就是如何传送请求。遵循Router设计模式的URLConvertibleURLRequestConvertible协议可以帮助我们。

URLConvertible

遵循了URLConvertible协议的类型可以被用来构建URL,然后用来创建URL请求。StringURLURLComponent默认是遵循URLConvertible协议的。它们都可以作为url参数传入requestuploaddownload方法:

  1. let urlString = "https://httpbin.org/post"
  2. Alamofire.request(urlString, method: .post)
  3. let url = URL(string: urlString)!
  4. Alamofire.request(url, method: .post)
  5. let urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: true)!
  6. Alamofire.request(urlComponents, method: .post)

以一种有意义的方式和web应用程序交互的应用,都鼓励使用自定义的遵循URLConvertible协议的类型将特定领域模型映射到服务器资源,因为这样比较方便。

类型安全传送
  1. extension User: URLConvertible {
  2. static let baseURLString = "https://example.com"
  3. func asURL() throws -> URL {
  4. let urlString = User.baseURLString + "/users/\(username)/"
  5. return try urlString.asURL()
  6. }
  7. }
  1. let user = User(username: "mattt")
  2. Alamofire.request(user) // https://example.com/users/mattt

URLRequestConvertible

遵循URLRequestConvertible协议的类型可以被用来构建URL请求。URLRequest默认遵循了URLRequestConvertible,允许被直接传入requestuploaddownload(推荐用这种方法为单个请求自定义请求头)。

  1. let url = URL(string: "https://httpbin.org/post")!
  2. var urlRequest = URLRequest(url: url)
  3. urlRequest.httpMethod = "POST"
  4. let parameters = ["foo": "bar"]
  5. do {
  6. urlRequest.httpBody = try JSONSerialization.data(withJSONObject: parameters, options: [])
  7. } catch {
  8. // No-op
  9. }
  10. urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
  11. Alamofire.request(urlRequest)

以一种有意义的方式和web应用程序交互的应用,都鼓励使用自定义的遵循URLRequestConvertible协议的类型来保证请求端点的一致性。这种方法可以用来抽象服务器端的不一致性,并提供类型安全传送,以及管理身份验证凭据和其他状态。

API参数抽象
  1. enum Router: URLRequestConvertible {
  2. case search(query: String, page: Int)
  3. static let baseURLString = "https://example.com"
  4. static let perPage = 50
  5. // MARK: URLRequestConvertible
  6. func asURLRequest() throws -> URLRequest {
  7. let result: (path: String, parameters: Parameters) = {
  8. switch self {
  9. case let .search(query, page) where page > 0:
  10. return ("/search", ["q": query, "offset": Router.perPage * page])
  11. case let .search(query, _):
  12. return ("/search", ["q": query])
  13. }
  14. }()
  15. let url = try Router.baseURLString.asURL()
  16. let urlRequest = URLRequest(url: url.appendingPathComponent(result.path))
  17. return try URLEncoding.default.encode(urlRequest, with: result.parameters)
  18. }
  19. }
  1. Alamofire.request(Router.search(query: "foo bar", page: 1)) // https://example.com/search?q=foo%20bar&offset=50
CRUD和授权
  1. import Alamofire
  2. enum Router: URLRequestConvertible {
  3. case createUser(parameters: Parameters)
  4. case readUser(username: String)
  5. case updateUser(username: String, parameters: Parameters)
  6. case destroyUser(username: String)
  7. static let baseURLString = "https://example.com"
  8. var method: HTTPMethod {
  9. switch self {
  10. case .createUser:
  11. return .post
  12. case .readUser:
  13. return .get
  14. case .updateUser:
  15. return .put
  16. case .destroyUser:
  17. return .delete
  18. }
  19. }
  20. var path: String {
  21. switch self {
  22. case .createUser:
  23. return "/users"
  24. case .readUser(let username):
  25. return "/users/\(username)"
  26. case .updateUser(let username, _):
  27. return "/users/\(username)"
  28. case .destroyUser(let username):
  29. return "/users/\(username)"
  30. }
  31. }
  32. // MARK: URLRequestConvertible
  33. func asURLRequest() throws -> URLRequest {
  34. let url = try Router.baseURLString.asURL()
  35. var urlRequest = URLRequest(url: url.appendingPathComponent(path))
  36. urlRequest.httpMethod = method.rawValue
  37. switch self {
  38. case .createUser(let parameters):
  39. urlRequest = try URLEncoding.default.encode(urlRequest, with: parameters)
  40. case .updateUser(_, let parameters):
  41. urlRequest = try URLEncoding.default.encode(urlRequest, with: parameters)
  42. default:
  43. break
  44. }
  45. return urlRequest
  46. }
  47. }
  1. Alamofire.request(Router.readUser("mattt")) // GET https://example.com/users/mattt

适配和重试请求

现在的大多数Web服务,都需要身份认证。现在比较常见的是OAuth。通常是需要一个access token来授权应用或者用户,然后才可以使用各种支持的Web服务。创建这些access token是比较麻烦的,当access token过期之后就比较麻烦了,我们需要重新创建一个新的。有许多线程安全问题要考虑。

RequestAdapterRequestRetrier协议可以让我们更容易地为特定的Web服务创建一个线程安全的认证系统。

RequestAdapter

RequestAdapter协议允许每一个SessionManagerRequest在创建之前被检查和适配。一个非常特别的使用适配器方法是,在一个特定的认证类型,把Authorization header拼接到请求。

  1. class AccessTokenAdapter: RequestAdapter {
  2. private let accessToken: String
  3. init(accessToken: String) {
  4. self.accessToken = accessToken
  5. }
  6. func adapt(_ urlRequest: URLRequest) throws -> URLRequest {
  7. var urlRequest = urlRequest
  8. if let urlString = urlRequest.url?.absoluteString, urlString.hasPrefix("https://httpbin.org") {
  9. urlRequest.setValue("Bearer " + accessToken, forHTTPHeaderField: "Authorization")
  10. }
  11. return urlRequest
  12. }
  13. }
  1. let sessionManager = SessionManager()
  2. sessionManager.adapter = AccessTokenAdapter(accessToken: "1234")
  3. sessionManager.request("https://httpbin.org/get")

RequestRetrier

RequestRetrier协议允许一个在执行过程中遇到error的请求被重试。当一起使用RequestAdapterRequestRetrier协议时,我们可以为OAuth1、OAuth2、Basic Auth(每次请求API都要提供用户名和密码)甚至是exponential backoff重试策略创建资格恢复系统。下面的例子演示了如何实现一个OAuth2 access token的恢复流程。

免责声明:这不是一个全面的OAuth2解决方案。这仅仅是演示如何把RequestAdapterRequestRetrier协议结合起来创建一个线程安全的恢复系统。

重申: 不要把这个例子复制到实际的开发应用中,这仅仅是一个例子。每个认证系统必须为每个特定的平台和认证类型重新定制。

  1. class OAuth2Handler: RequestAdapter, RequestRetrier {
  2. private typealias RefreshCompletion = (_ succeeded: Bool, _ accessToken: String?, _ refreshToken: String?) -> Void
  3. private let sessionManager: SessionManager = {
  4. let configuration = URLSessionConfiguration.default
  5. configuration.httpAdditionalHeaders = SessionManager.defaultHTTPHeaders
  6. return SessionManager(configuration: configuration)
  7. }()
  8. private let lock = NSLock()
  9. private var clientID: String
  10. private var baseURLString: String
  11. private var accessToken: String
  12. private var refreshToken: String
  13. private var isRefreshing = false
  14. private var requestsToRetry: [RequestRetryCompletion] = []
  15. // MARK: - Initialization
  16. public init(clientID: String, baseURLString: String, accessToken: String, refreshToken: String) {
  17. self.clientID = clientID
  18. self.baseURLString = baseURLString
  19. self.accessToken = accessToken
  20. self.refreshToken = refreshToken
  21. }
  22. // MARK: - RequestAdapter
  23. func adapt(_ urlRequest: URLRequest) throws -> URLRequest {
  24. if let urlString = urlRequest.url?.absoluteString, urlString.hasPrefix(baseURLString) {
  25. var urlRequest = urlRequest
  26. urlRequest.setValue("Bearer " + accessToken, forHTTPHeaderField: "Authorization")
  27. return urlRequest
  28. }
  29. return urlRequest
  30. }
  31. // MARK: - RequestRetrier
  32. func should(_ manager: SessionManager, retry request: Request, with error: Error, completion: @escaping RequestRetryCompletion) {
  33. lock.lock() ; defer { lock.unlock() }
  34. if let response = request.task?.response as? HTTPURLResponse, response.statusCode == 401 {
  35. requestsToRetry.append(completion)
  36. if !isRefreshing {
  37. refreshTokens { [weak self] succeeded, accessToken, refreshToken in
  38. guard let strongSelf = self else { return }
  39. strongSelf.lock.lock() ; defer { strongSelf.lock.unlock() }
  40. if let accessToken = accessToken, let refreshToken = refreshToken {
  41. strongSelf.accessToken = accessToken
  42. strongSelf.refreshToken = refreshToken
  43. }
  44. strongSelf.requestsToRetry.forEach { $0(succeeded, 0.0) }
  45. strongSelf.requestsToRetry.removeAll()
  46. }
  47. }
  48. } else {
  49. completion(false, 0.0)
  50. }
  51. }
  52. // MARK: - Private - Refresh Tokens
  53. private func refreshTokens(completion: @escaping RefreshCompletion) {
  54. guard !isRefreshing else { return }
  55. isRefreshing = true
  56. let urlString = "\(baseURLString)/oauth2/token"
  57. let parameters: [String: Any] = [
  58. "access_token": accessToken,
  59. "refresh_token": refreshToken,
  60. "client_id": clientID,
  61. "grant_type": "refresh_token"
  62. ]
  63. sessionManager.request(urlString, method: .post, parameters: parameters, encoding: JSONEncoding.default)
  64. .responseJSON { [weak self] response in
  65. guard let strongSelf = self else { return }
  66. if
  67. let json = response.result.value as? [String: Any],
  68. let accessToken = json["access_token"] as? String,
  69. let refreshToken = json["refresh_token"] as? String
  70. {
  71. completion(true, accessToken, refreshToken)
  72. } else {
  73. completion(false, nil, nil)
  74. }
  75. strongSelf.isRefreshing = false
  76. }
  77. }
  78. }
  1. let baseURLString = "https://some.domain-behind-oauth2.com"
  2. let oauthHandler = OAuth2Handler(
  3. clientID: "12345678",
  4. baseURLString: baseURLString,
  5. accessToken: "abcd1234",
  6. refreshToken: "ef56789a"
  7. )
  8. let sessionManager = SessionManager()
  9. sessionManager.adapter = oauthHandler
  10. sessionManager.retrier = oauthHandler
  11. let urlString = "\(baseURLString)/some/endpoint"
  12. sessionManager.request(urlString).validate().responseJSON { response in
  13. debugPrint(response)
  14. }

一旦OAuth2HandlerSessionManager被应用与adapterretrier,他将会通过自动恢复access token来处理一个非法的access token error,并且根据失败的顺序来重试所有失败的请求。(如果需要让他们按照创建的时间顺序来执行,可以使用他们的task identifier来排序)

上面这个例子仅仅检查了401响应码,不是演示如何检查一个非法的access token error。在实际开发应用中,我们想要检查realmwww-authenticate header响应,虽然这取决于OAuth2的实现。

还有一个要重点注意的是,这个认证系统可以在多个session manager之间共享。例如,可以在同一个Web服务集合使用defaultephemeral会话配置。上面这个例子可以在多个session manager间共享一个oauthHandler实例,来管理一个恢复流程。

自定义响应序列化

Alamofire为data、strings、JSON和Property List提供了内置的响应序列化:

  1. Alamofire.request(...).responseData { (resp: DataResponse<Data>) in ... }
  2. Alamofire.request(...).responseString { (resp: DataResponse<String>) in ... }
  3. Alamofire.request(...).responseJSON { (resp: DataResponse<Any>) in ... }
  4. Alamofire.request(...).responsePropertyList { resp: DataResponse<Any>) in ... }

这些响应包装了反序列化的值(Data, String, Any)或者error (network, validation errors),以及元数据 (URL Request, HTTP headers, status code, metrics, ...)。

我们可以有多个方法来自定义所有响应元素:

  • 响应映射
  • 处理错误
  • 创建一个自定义的响应序列化器
  • 泛型响应对象序列化

响应映射

响应映射是自定义响应最简单的方式。它转换响应的值,同时保留最终错误和元数据。例如,我们可以把一个json响应DataResponse<Any>转换为一个保存应用模型的的响应,例如DataResponse<User>。使用DataResponse.map来进行响应映射:

  1. Alamofire.request("https://example.com/users/mattt").responseJSON { (response: DataResponse<Any>) in
  2. let userResponse = response.map { json in
  3. // We assume an existing User(json: Any) initializer
  4. return User(json: json)
  5. }
  6. // Process userResponse, of type DataResponse<User>:
  7. if let user = userResponse.value {
  8. print("User: { username: \(user.username), name: \(user.name) }")
  9. }
  10. }

当转换可能会抛出错误时,使用flatMap方法:

  1. Alamofire.request("https://example.com/users/mattt").responseJSON { response in
  2. let userResponse = response.flatMap { json in
  3. try User(json: json)
  4. }
  5. }

响应映射非常适合自定义completion handler:

  1. @discardableResult
  2. func loadUser(completionHandler: @escaping (DataResponse<User>) -> Void) -> Alamofire.DataRequest {
  3. return Alamofire.request("https://example.com/users/mattt").responseJSON { response in
  4. let userResponse = response.flatMap { json in
  5. try User(json: json)
  6. }
  7. completionHandler(userResponse)
  8. }
  9. }
  10. loadUser { response in
  11. if let user = userResponse.value {
  12. print("User: { username: \(user.username), name: \(user.name) }")
  13. }
  14. }

上面代码中loadUser方法被@discardableResult标记,意思是调用loadUser方法可以不接收它的返回值;也可以用_来忽略返回值。

当 map/flatMap 闭包会产生比较大的数据量时,要保证这个闭包在子线程中执行:

  1. @discardableResult
  2. func loadUser(completionHandler: @escaping (DataResponse<User>) -> Void) -> Alamofire.DataRequest {
  3. let utilityQueue = DispatchQueue.global(qos: .utility)
  4. return Alamofire.request("https://example.com/users/mattt").responseJSON(queue: utilityQueue) { response in
  5. let userResponse = response.flatMap { json in
  6. try User(json: json)
  7. }
  8. DispatchQueue.main.async {
  9. completionHandler(userResponse)
  10. }
  11. }
  12. }

mapflatMap也可以用于下载响应。

处理错误

在实现自定义响应序列化器或者对象序列化方法前,思考如何处理所有可能出现的错误是非常重要的。有两个方法:1)传递未修改的错误,在响应时间处理;2)把所有的错误封装在一个Error类型中。

例如,下面是等会要用用到的后端错误:

  1. enum BackendError: Error {
  2. case network(error: Error) // 捕获任何从URLSession API产生的错误
  3. case dataSerialization(error: Error)
  4. case jsonSerialization(error: Error)
  5. case xmlSerialization(error: Error)
  6. case objectSerialization(reason: String)
  7. }

创建一个自定义的响应序列化器

Alamofire为strings、JSON和Property List提供了内置的响应序列化,但是我们可以通过扩展Alamofire.DataRequest或者Alamofire.DownloadRequest来添加其他序列化。

例如,下面这个例子是一个使用Ono (一个实用的处理iOS和macOS平台的XML和HTML的方式)的响应handler的实现:

  1. extension DataRequest {
  2. static func xmlResponseSerializer() -> DataResponseSerializer<ONOXMLDocument> {
  3. return DataResponseSerializer { request, response, data, error in
  4. // 把任何底层的URLSession error传递给 .network case
  5. guard error == nil else { return .failure(BackendError.network(error: error!)) }
  6. // 使用Alamofire已有的数据序列化器来提取数据,error为nil,因为上一行代码已经把不是nil的error过滤了
  7. let result = Request.serializeResponseData(response: response, data: data, error: nil)
  8. guard case let .success(validData) = result else {
  9. return .failure(BackendError.dataSerialization(error: result.error! as! AFError))
  10. }
  11. do {
  12. let xml = try ONOXMLDocument(data: validData)
  13. return .success(xml)
  14. } catch {
  15. return .failure(BackendError.xmlSerialization(error: error))
  16. }
  17. }
  18. }
  19. @discardableResult
  20. func responseXMLDocument(
  21. queue: DispatchQueue? = nil,
  22. completionHandler: @escaping (DataResponse<ONOXMLDocument>) -> Void)
  23. -> Self
  24. {
  25. return response(
  26. queue: queue,
  27. responseSerializer: DataRequest.xmlResponseSerializer(),
  28. completionHandler: completionHandler
  29. )
  30. }
  31. }

泛型响应对象序列化

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

  1. protocol ResponseObjectSerializable {
  2. init?(response: HTTPURLResponse, representation: Any)
  3. }
  4. extension DataRequest {
  5. func responseObject<T: ResponseObjectSerializable>(
  6. queue: DispatchQueue? = nil,
  7. completionHandler: @escaping (DataResponse<T>) -> Void)
  8. -> Self
  9. {
  10. let responseSerializer = DataResponseSerializer<T> { request, response, data, error in
  11. guard error == nil else { return .failure(BackendError.network(error: error!)) }
  12. let jsonResponseSerializer = DataRequest.jsonResponseSerializer(options: .allowFragments)
  13. let result = jsonResponseSerializer.serializeResponse(request, response, data, nil)
  14. guard case let .success(jsonObject) = result else {
  15. return .failure(BackendError.jsonSerialization(error: result.error!))
  16. }
  17. guard let response = response, let responseObject = T(response: response, representation: jsonObject) else {
  18. return .failure(BackendError.objectSerialization(reason: "JSON could not be serialized: \(jsonObject)"))
  19. }
  20. return .success(responseObject)
  21. }
  22. return response(queue: queue, responseSerializer: responseSerializer, completionHandler: completionHandler)
  23. }
  24. }
  1. struct User: ResponseObjectSerializable, CustomStringConvertible {
  2. let username: String
  3. let name: String
  4. var description: String {
  5. return "User: { username: \(username), name: \(name) }"
  6. }
  7. init?(response: HTTPURLResponse, representation: Any) {
  8. guard
  9. let username = response.url?.lastPathComponent,
  10. let representation = representation as? [String: Any],
  11. let name = representation["name"] as? String
  12. else { return nil }
  13. self.username = username
  14. self.name = name
  15. }
  16. }
  1. Alamofire.request("https://example.com/users/mattt").responseObject { (response: DataResponse<User>) in
  2. debugPrint(response)
  3. if let user = response.result.value {
  4. print("User: { username: \(user.username), name: \(user.name) }")
  5. }
  6. }

同样地方法可以用来处理返回对象集合的接口:

  1. protocol ResponseCollectionSerializable {
  2. static func collection(from response: HTTPURLResponse, withRepresentation representation: Any) -> [Self]
  3. }
  4. extension ResponseCollectionSerializable where Self: ResponseObjectSerializable {
  5. static func collection(from response: HTTPURLResponse, withRepresentation representation: Any) -> [Self] {
  6. var collection: [Self] = []
  7. if let representation = representation as? [[String: Any]] {
  8. for itemRepresentation in representation {
  9. if let item = Self(response: response, representation: itemRepresentation) {
  10. collection.append(item)
  11. }
  12. }
  13. }
  14. return collection
  15. }
  16. }
  1. extension DataRequest {
  2. @discardableResult
  3. func responseCollection<T: ResponseCollectionSerializable>(
  4. queue: DispatchQueue? = nil,
  5. completionHandler: @escaping (DataResponse<[T]>) -> Void) -> Self
  6. {
  7. let responseSerializer = DataResponseSerializer<[T]> { request, response, data, error in
  8. guard error == nil else { return .failure(BackendError.network(error: error!)) }
  9. let jsonSerializer = DataRequest.jsonResponseSerializer(options: .allowFragments)
  10. let result = jsonSerializer.serializeResponse(request, response, data, nil)
  11. guard case let .success(jsonObject) = result else {
  12. return .failure(BackendError.jsonSerialization(error: result.error!))
  13. }
  14. guard let response = response else {
  15. let reason = "Response collection could not be serialized due to nil response."
  16. return .failure(BackendError.objectSerialization(reason: reason))
  17. }
  18. return .success(T.collection(from: response, withRepresentation: jsonObject))
  19. }
  20. return response(responseSerializer: responseSerializer, completionHandler: completionHandler)
  21. }
  22. }
  1. struct User: ResponseObjectSerializable, ResponseCollectionSerializable, CustomStringConvertible {
  2. let username: String
  3. let name: String
  4. var description: String {
  5. return "User: { username: \(username), name: \(name) }"
  6. }
  7. init?(response: HTTPURLResponse, representation: Any) {
  8. guard
  9. let username = response.url?.lastPathComponent,
  10. let representation = representation as? [String: Any],
  11. let name = representation["name"] as? String
  12. else { return nil }
  13. self.username = username
  14. self.name = name
  15. }
  16. }
  1. Alamofire.request("https://example.com/users").responseCollection { (response: DataResponse<[User]>) in
  2. debugPrint(response)
  3. if let users = response.result.value {
  4. users.forEach { print("- \($0)") }
  5. }
  6. }

安全

对于安全敏感的数据来说,在与服务器和web服务交互时使用安全的HTTPS连接是非常重要的一步。默认情况下,Alamofire会使用苹果安全框架内置的验证方法来评估服务器提供的证书链。虽然保证了证书链是有效的,但是不能防止man-in-the-middle (MITM)攻击或者其他潜在的漏洞。为了减少MITM攻击,处理用户的敏感数据或财务信息的应用,应该使用ServerTrustPolicy提供的certificate或者public key pinning。

ServerTrustPolicy

在通过HTTPS安全连接连接到服务器时,ServerTrustPolicy枚举通常会评估URLAuthenticationChallenge提供的server trust。

  1. let serverTrustPolicy = ServerTrustPolicy.pinCertificates(
  2. certificates: ServerTrustPolicy.certificates(),
  3. validateCertificateChain: true,
  4. validateHost: true
  5. )

在验证的过程中,有多种方法可以让我们完全控制server trust的评估:

  • 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的有效性,我们可以完全控制整个验证过程。但是要谨慎使用。

服务器信任策略管理者 (Server Trust Policy Manager)

ServerTrustPolicyManager负责存储一个内部的服务器信任策略到特定主机的映射。这样Alamofire就可以评估每个主机不同服务器信任策略。

  1. let serverTrustPolicies: [String: ServerTrustPolicy] = [
  2. "test.example.com": .pinCertificates(
  3. certificates: ServerTrustPolicy.certificates(),
  4. validateCertificateChain: true,
  5. validateHost: true
  6. ),
  7. "insecure.expired-apis.com": .disableEvaluation
  8. ]
  9. let sessionManager = SessionManager(
  10. serverTrustPolicyManager: ServerTrustPolicyManager(policies: serverTrustPolicies)
  11. )

注意:要确保有一个强引用引用着SessionManager实例,否则当sessionManager被销毁时,请求将会取消。

这些服务器信任策略将会形成下面的结果:

  • test.example.com:始终使用证书链固定的证书和启用主机验证,因此需要以下条件才能是TLS握手成功:

    • 证书链必须是有效的。
    • 证书链必须包含一个已经固定的证书。
    • Challenge主机必须匹配主机证书链的子证书。
  • insecure.expired-apis.com:将从不评估证书链,并且总是允许TLS握手成功。
  • 其他主机将会默认使用苹果提供的验证。
子类化服务器信任策略管理者

如果我们需要一个更灵活的服务器信任策略来匹配其他行为(例如通配符域名),可以子类化ServerTrustPolicyManager,并且重写serverTrustPolicyForHost方法。

  1. class CustomServerTrustPolicyManager: ServerTrustPolicyManager {
  2. override func serverTrustPolicy(forHost host: String) -> ServerTrustPolicy? {
  3. var policy: ServerTrustPolicy?
  4. // Implement your custom domain matching behavior...
  5. return policy
  6. }
  7. }

验证主机

.performDefaultEvaluation.pinCertificates.pinPublicKeys这三个服务器信任策略都带有一个validateHost参数。把这个值设为true,服务器信任评估就会验证与challenge主机名字匹配的在证书里面的主机名字。如果他们不匹配,验证失败。如果设置为false,仍然会评估整个证书链,但是不会验证子证书的主机名字。

注意:建议在实际开发中,把validateHost设置为true

验证证书链

Pinning certificate 和 public keys 都可以通过validateCertificateChain参数拥有验证证书链的选项。把它设置为true,除了对Pinning certificate 和 public keys进行字节相等检查外,还将会验证整个证书链。如果是false,将会跳过证书链验证,但还会进行字节相等检查。

还有很多情况会导致禁用证书链认证。最常用的方式就是自签名和过期的证书。在这些情况下,验证始终会失败。但是字节相等检查会保证我们从服务器接收到证书。

注意:建议在实际开发中,把validateCertificateChain设置为true

应用传输安全 (App Transport Security)

从iOS9开始,就添加了App Transport Security (ATS),使用ServerTrustPolicyManager和多个ServerTrustPolicy对象可能没什么影响。如果我们不断看到CFNetwork SSLHandshake failed (-9806)错误,我们可能遇到了这个问题。苹果的ATS系统重写了整个challenge系统,除非我们在plist文件中配置ATS设置来允许应用评估服务器信任。

  1. <dict>
  2. <key>NSAppTransportSecurity</key>
  3. <dict>
  4. <key>NSExceptionDomains</key>
  5. <dict>
  6. <key>example.com</key>
  7. <dict>
  8. <key>NSExceptionAllowsInsecureHTTPLoads</key>
  9. <true/>
  10. <key>NSExceptionRequiresForwardSecrecy</key>
  11. <false/>
  12. <key>NSIncludesSubdomains</key>
  13. <true/>
  14. <!-- 可选的: 指定TLS的最小版本 -->
  15. <key>NSTemporaryExceptionMinimumTLSVersion</key>
  16. <string>TLSv1.2</string>
  17. </dict>
  18. </dict>
  19. </dict>
  20. </dict>

是否需要把NSExceptionRequiresForwardSecrecy设置为NO取决于TLS连接是否使用一个允许的密码套件。在某些情况下,它需要设置为NONSExceptionAllowsInsecureHTTPLoads必须设置为YES,然后SessionDelegate才能接收到challenge回调。一旦challenge回调被调用,ServerTrustPolicyManager将接管服务器信任评估。如果我们要连接到一个仅支持小于1.2版本的TSL主机,那么还要指定NSTemporaryExceptionMinimumTLSVersion

注意:在实际开发中,建议始终使用有效的证书。

网络可达性 (Network Reachability)

NetworkReachabilityManager监听WWANWiFi网络接口和主机地址的可达性变化。

  1. let manager = NetworkReachabilityManager(host: "www.apple.com")
  2. manager?.listener = { status in
  3. print("Network Status Changed: \(status)")
  4. }
  5. manager?.startListening()

注意:要确保manager被强引用,否则会接收不到状态变化。另外,在主机字符串中不要包含scheme,也就是说要把https://去掉,否则无法监听。

当使用网络可达性来决定接下来要做什么时,有以下几点需要重点注意的:

  • 不要使用Reachability来决定是否发送一个网络请求。

    • 我们必须要发送请求。
  • 当Reachability恢复了,要重试网络请求。
    • 即使网络请求失败,在这个时候也非常适合重试请求。
  • 网络可达性的状态非常适合用来决定为什么网络请求会失败。
    • 如果一个请求失败,应该告诉用户是离线导致请求失败的,而不是技术错误,例如请求超时。

有兴趣的可以看看WWDC 2012 Session 706, "Networking Best Practices"

FAQ

Alamofire的起源是什么?

Alamofire是根据 Alamo Fire flower 命名的,是一种矢车菊的混合变种,德克萨斯的州花。

Router和Request Adapter的逻辑是什么?

简单和静态的数据,例如paths、parameters和共同的headers放在Router。动态的数据,例如一个Authorization header,它的值会随着一个认证系统变化,放在RequestAdapter

动态的数据必须放在ReqeustAdapter的原因是要支持重试操作。当重试一个请求时,原来的请求不会重新建立,也就意味着Router不会再重新调用。RequestAdapter可以重新调用,这可以让我们在重试请求之前更新原始请求的动态数据。


作者:Lebron_James
链接:https://www.jianshu.com/p/903b678d2d3f
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

【iOS开发】Alamofire框架的使用二 高级用法的更多相关文章

  1. 【iOS开发】Alamofire框架的使用一基本用法

    Alamofire框架的使用一 —— 基本用法 对于使用Objective-C的开发者,一定非常熟悉AFNetworking这个网络框架.在苹果推出的Swift之后,AFNetworking的作者专门 ...

  2. sqlalchemy(二)高级用法

    sqlalchemy(二)高级用法 本文将介绍sqlalchemy的高级用法. 外键以及relationship 首先创建数据库,在这里一个user对应多个address,因此需要在address上增 ...

  3. 李洪强iOS开发之 - enum与typedef enum的用法

    李洪强iOS开发之 - enum与typedef enum的用法 01 - 定义枚举类型 上面我们就在ViewController.h定义了一个枚举类型,枚举类型的值默认是连续的自然数,例如例子中的T ...

  4. iOS开发基础框架

    ---恢复内容开始--- //appdelegate ////  AppDelegate.m//  iOS开发架构////  Copyright © 2016年 Chason. All rights ...

  5. iOS 开发中的争议(二)

    这是该系列的第二篇.在本文中,我想讨论的是:对于 UI 界面的编写工作,到底应该用 xib/storyboard 完成,还是用手写代码来完成? 本着 “使用过才有发言权” 原则,我介绍一下我的经历: ...

  6. iOS开发多线程篇 08 —GCD的常见用法

    iOS开发多线程篇—GCD的常见用法 一.延迟执行 1.介绍 iOS常见的延时执行有2种方式 (1)调用NSObject的方法 [self performSelector:@selector(run) ...

  7. iOS开发UIKit框架-可视化编程-XIB

    1. Interface Builder 可视化编程 1> 概述 GUI : 图形用户界面(Graphical User Interface, 简称GUI, 又称图形化界面) 是指采用图形方式显 ...

  8. iOS开发-网络框架-b

    网络框架(以下称NJAFNetworking)是基于AFNetworking框架的简单封装,基本功能包括POST请求,GET请求,上传文件,下载文件,网络状态,缓存等. 为什么要使用NJAFNetwo ...

  9. iOS开发工程师面试题(二)

    1.手写冒泡跟插入排序 冒泡排序来源于生活常识,相当于把数组竖起来,轻的向上,重的向下.void bubbleSort(int[] unsorted) { ; i < unsorted.Leng ...

随机推荐

  1. Python档案袋(异常与异常捕获 )

    无异常捕获 程序遇到异常会中断 print( xxx ) print("---- 完 -----") 得到结果为: 有异常捕获 程序遇到异常会进入异常处理,并继续执行下面程序 tr ...

  2. HTML常用特殊字符编码对照表以及其对应英文

    符号 说明 对应编码(使用时去掉空格) 英文 & AND 符号 & amp; ampersand < 小于 & lt; little > 大于 & gt; ...

  3. Socket网络编程知识点

    静态方法    与类无关,不能访问类里的任何属性和方法类方法    只能访问类变量属性@property    把一个方法变成一个静态属性,    flight.status    @status.s ...

  4. idea操作整理

    前言 这篇记录一下,在idea使用的过程中一些加快开发效率的操作. live template  postfix 当使用一个数字或者一个参数按照以下写法会自动变成例子中的情况 100.for -&g ...

  5. 修改sql数据库名称

    USE master; GO DECLARE @SQL VARCHAR(MAX); SET @SQL='' SELECT @SQL=@SQL+'; KILL '+RTRIM(SPID) FROM ma ...

  6. 理解 docker 容器中的 uid 和 gid

    默认情况下,容器中的进程以 root 用户权限运行,并且这个 root 用户和宿主机中的 root 是同一个用户.听起来是不是很可怕,因为这就意味着一旦容器中的进程有了适当的机会,它就可以控制宿主机上 ...

  7. JDBC设计理念浅析 JDBC简介(一)

    概念 JDBC是J2EE的标准规范之一,J2EE就是为了规范JAVA解决企业级应用开发制定的一系列规范,JDBC也不例外. JDBC是用于Java编程语言和数据库之间的数据库无关连接的标准Java A ...

  8. DSAPI多功能组件编程应用-参考-Win32API常数

    DSAPI多功能组件编程应用-参考-Win32API常数 在编程过程中,常常需要使用Win32API来实现一些特定功能,而Win32API又往往需要使用一些API常数,百度搜索常数值,查手册,也就成了 ...

  9. 杭电ACM2009--求数列的和

    求数列的和 Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/32768 K (Java/Others)Total Submi ...

  10. C# 批量删除Word超链接

    对于Word文档中包含较多的超链接,如果一个个来删除很花费时间和精力,本篇文章将提供一种可用于批量删除Word中的超链接的方法.这里的超链接可以是页眉页脚处的超链接.正文中的超链接.表格中的超链接.文 ...