【iOS開發(fā)】Alamofire框架的使用二 —— 高級用法

這篇文章是 Alamofire 5.0 以前的文檔,最新文檔請查看:Alamofire 5 的使用 - 高級用法


這邊文章介紹的是Alamofire框架的高級用法,如果之前沒有看過基本用法的,可以先去看看【iOS開發(fā)】Alamofire框架的使用一 —— 基本用法

Alamofire是在URLSession和URL加載系統(tǒng)的基礎上寫的。所以,為了更好地學習這個框架,建議先熟悉下列幾個底層網(wǎng)絡協(xié)議棧:

Session Manager

高級別的方便的方法,例如Alamofire.request,使用的是默認的Alamofire.SessionManager,并且這個SessionManager是用默認URLSessionConfiguration配置的。

例如,下面兩個語句是等價的:

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

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

我們可以自己創(chuàng)建后臺會話和短暫會話的session manager,還可以自定義默認的會話配置來創(chuàng)建新的session manager,例如修改默認的header httpAdditionalHeaderstimeoutIntervalForRequest

用默認的會話配置創(chuàng)建一個Session Manager

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

用后臺會話配置創(chuàng)建一個Session Manager

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

用默短暫會話配置創(chuàng)建一個Session Manager

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

修改會話配置

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)

注意:不推薦在Authorization或者Content-Type header使用。而應該使用Alamofire.requestAPI、URLRequestConvertibleParameterEncoding的headers參數(shù)。

會話代理

默認情況下,一個SessionManager實例創(chuàng)建一個SessionDelegate對象來處理底層URLSession生成的不同類型的代理回調(diào)。每個代理方法的實現(xiàn)處理常見的情況。然后,高級用戶可能由于各種原因需要重寫默認功能。

重寫閉包

第一種自定義SessionDelegate的方法是通過重寫閉包。我們可以在每個閉包重寫SessionDelegate API對應的實現(xiàn)。下面是重寫閉包的示例:

/// 重寫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?)?

下面的示例演示了如何使用taskWillPerformHTTPRedirection來避免回調(diào)到任何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的實現(xiàn)的方法是把它子類化。通過子類化,我們可以完全自定義他的行為,或者為這個API創(chuàng)建一個代理并且仍然使用它的默認實現(xiàn)。通過創(chuàng)建代理,我們可以跟蹤日志事件、發(fā)通知、提供前后實現(xiàn)。下面這個例子演示了如何子類化SessionDelegate,并且有回調(diào)的時候打印信息:

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

總的來說,無論是默認實現(xiàn)還是重寫閉包,都應該提供必要的功能。子類化應該作為最后的選擇。

請求

requestdownloaduploadstream方法的結果是DataRequestDownloadRequestUploadRequestStreamRequest,并且所有請求都繼承自Request。所有的Request并不是直接創(chuàng)建的,而是由session manager創(chuàng)建的。

每個子類都有特定的方法,例如authenticatevalidateresponseJSONuploadProgress,都返回一個實例,以便方法鏈接(也就是用點語法連續(xù)調(diào)用方法)。

請求可以被暫停、恢復和取消:

  • suspend():暫停底層的任務和調(diào)度隊列
  • resume():恢復底層的任務和調(diào)度隊列。如果manager的startRequestsImmediately不是true,那么必須調(diào)用resume()來開始請求。
  • cancel():取消底層的任務,并產(chǎn)生一個error,error被傳入任何已經(jīng)注冊的響應handlers。

傳送請求

隨著應用的不多增大,當我們建立網(wǎng)絡棧的時候要使用通用的模式。在通用模式的設計中,一個很重要的部分就是如何傳送請求。遵循Router設計模式的URLConvertibleURLRequestConvertible協(xié)議可以幫助我們。

URLConvertible

遵循了URLConvertible協(xié)議的類型可以被用來構建URL,然后用來創(chuàng)建URL請求。StringURLURLComponent默認是遵循URLConvertible協(xié)議的。它們都可以作為url參數(shù)傳入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(urlComponents, method: .post)

以一種有意義的方式和web應用程序交互的應用,都鼓勵使用自定義的遵循URLConvertible協(xié)議的類型將特定領域模型映射到服務器資源,因為這樣比較方便。

類型安全傳送
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協(xié)議的類型可以被用來構建URL請求。URLRequest默認遵循了URLRequestConvertible,允許被直接傳入requestuploaddownload(推薦用這種方法為單個請求自定義請求頭)。

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協(xié)議的類型來保證請求端點的一致性。這種方法可以用來抽象服務器端的不一致性,并提供類型安全傳送,以及管理身份驗證憑據(jù)和其他狀態(tài)。

API參數(shù)抽象
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
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 https://example.com/users/mattt

適配和重試請求

現(xiàn)在的大多數(shù)Web服務,都需要身份認證。現(xiàn)在比較常見的是OAuth。通常是需要一個access token來授權應用或者用戶,然后才可以使用各種支持的Web服務。創(chuàng)建這些access token是比較麻煩的,當access token過期之后就比較麻煩了,我們需要重新創(chuàng)建一個新的。有許多線程安全問題要考慮。

RequestAdapterRequestRetrier協(xié)議可以讓我們更容易地為特定的Web服務創(chuàng)建一個線程安全的認證系統(tǒng)。

RequestAdapter

RequestAdapter協(xié)議允許每一個SessionManagerRequest在創(chuàng)建之前被檢查和適配。一個非常特別的使用適配器方法是,在一個特定的認證類型,把Authorization header拼接到請求。

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

RequestRetrier協(xié)議允許一個在執(zhí)行過程中遇到error的請求被重試。當一起使用RequestAdapterRequestRetrier協(xié)議時,我們可以為OAuth1、OAuth2、Basic Auth(每次請求API都要提供用戶名和密碼)甚至是exponential backoff重試策略創(chuàng)建資格恢復系統(tǒng)。下面的例子演示了如何實現(xiàn)一個OAuth2 access token的恢復流程。

免責聲明:這不是一個全面的OAuth2解決方案。這僅僅是演示如何把RequestAdapterRequestRetrier協(xié)議結合起來創(chuàng)建一個線程安全的恢復系統(tǒng)。

重申: 不要把這個例子復制到實際的開發(fā)應用中,這僅僅是一個例子。每個認證系統(tǒng)必須為每個特定的平臺和認證類型重新定制。

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

一旦OAuth2HandlerSessionManager被應用與adapterretrier,他將會通過自動恢復access token來處理一個非法的access token error,并且根據(jù)失敗的順序來重試所有失敗的請求。(如果需要讓他們按照創(chuàng)建的時間順序來執(zhí)行,可以使用他們的task identifier來排序)

上面這個例子僅僅檢查了401響應碼,不是演示如何檢查一個非法的access token error。在實際開發(fā)應用中,我們想要檢查realmwww-authenticate header響應,雖然這取決于OAuth2的實現(xiàn)。

還有一個要重點注意的是,這個認證系統(tǒng)可以在多個session manager之間共享。例如,可以在同一個Web服務集合使用defaultephemeral會話配置。上面這個例子可以在多個session manager間共享一個oauthHandler實例,來管理一個恢復流程。

自定義響應序列化

Alamofire為data、strings、JSON和Property List提供了內(nèi)置的響應序列化:

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),以及元數(shù)據(jù) (URL Request, HTTP headers, status code, metrics, ...)。

我們可以有多個方法來自定義所有響應元素:

  • 響應映射
  • 處理錯誤
  • 創(chuàng)建一個自定義的響應序列化器
  • 泛型響應對象序列化

響應映射

響應映射是自定義響應最簡單的方式。它轉換響應的值,同時保留最終錯誤和元數(shù)據(jù)。例如,我們可以把一個json響應DataResponse<Any>轉換為一個保存應用模型的的響應,例如DataResponse<User>。使用DataResponse.map來進行響應映射:

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<User>:
    if let user = userResponse.value {
        print("User: { username: \(user.username), name: \(user.name) }")
    }
}

當轉換可能會拋出錯誤時,使用flatMap方法:

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

響應映射非常適合自定義completion handler:

@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) }")
    }
}

上面代碼中loadUser方法被@discardableResult標記,意思是調(diào)用loadUser方法可以不接收它的返回值;也可以用_來忽略返回值。

當 map/flatMap 閉包會產(chǎn)生比較大的數(shù)據(jù)量時,要保證這個閉包在子線程中執(zhí)行:

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

mapflatMap也可以用于下載響應。

處理錯誤

在實現(xiàn)自定義響應序列化器或者對象序列化方法前,思考如何處理所有可能出現(xiàn)的錯誤是非常重要的。有兩個方法:1)傳遞未修改的錯誤,在響應時間處理;2)把所有的錯誤封裝在一個Error類型中。

例如,下面是等會要用用到的后端錯誤:

enum BackendError: Error {
    case network(error: Error) // 捕獲任何從URLSession API產(chǎn)生的錯誤
    case dataSerialization(error: Error)
    case jsonSerialization(error: Error)
    case xmlSerialization(error: Error)
    case objectSerialization(reason: String)
}

創(chuàng)建一個自定義的響應序列化器

Alamofire為strings、JSON和Property List提供了內(nèi)置的響應序列化,但是我們可以通過擴展Alamofire.DataRequest或者Alamofire.DownloadRequest來添加其他序列化。

例如,下面這個例子是一個使用Ono (一個實用的處理iOS和macOS平臺的XML和HTML的方式)的響應handler的實現(xiàn):

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已有的數(shù)據(jù)序列化器來提取數(shù)據(jù),error為nil,因為上一行代碼已經(jīng)把不是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
        )
    }
}

泛型響應對象序列化

泛型可以用來提供自動的、類型安全的響應對象序列化。

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

安全

對于安全敏感的數(shù)據(jù)來說,在與服務器和web服務交互時使用安全的HTTPS連接是非常重要的一步。默認情況下,Alamofire會使用蘋果安全框架內(nèi)置的驗證方法來評估服務器提供的證書鏈。雖然保證了證書鏈是有效的,但是不能防止man-in-the-middle (MITM)攻擊或者其他潛在的漏洞。為了減少MITM攻擊,處理用戶的敏感數(shù)據(jù)或財務信息的應用,應該使用ServerTrustPolicy提供的certificate或者public key pinning。

ServerTrustPolicy

在通過HTTPS安全連接連接到服務器時,ServerTrustPolicy枚舉通常會評估URLAuthenticationChallenge提供的server trust。

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

在驗證的過程中,有多種方法可以讓我們完全控制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負責存儲一個內(nèi)部的服務器信任策略到特定主機的映射。這樣Alamofire就可以評估每個主機不同服務器信任策略。

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

注意:要確保有一個強引用引用著SessionManager實例,否則當sessionManager被銷毀時,請求將會取消。

這些服務器信任策略將會形成下面的結果:

  • test.example.com:始終使用證書鏈固定的證書和啟用主機驗證,因此需要以下條件才能是TLS握手成功:
    • 證書鏈必須是有效的。
    • 證書鏈必須包含一個已經(jīng)固定的證書。
    • 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參數(shù)。把這個值設為true,服務器信任評估就會驗證與challenge主機名字匹配的在證書里面的主機名字。如果他們不匹配,驗證失敗。如果設置為false,仍然會評估整個證書鏈,但是不會驗證子證書的主機名字。

注意:建議在實際開發(fā)中,把validateHost設置為true

驗證證書鏈

Pinning certificate 和 public keys 都可以通過validateCertificateChain參數(shù)擁有驗證證書鏈的選項。把它設置為true,除了對Pinning certificate 和 public keys進行字節(jié)相等檢查外,還將會驗證整個證書鏈。如果是false,將會跳過證書鏈驗證,但還會進行字節(jié)相等檢查。

還有很多情況會導致禁用證書鏈認證。最常用的方式就是自簽名和過期的證書。在這些情況下,驗證始終會失敗。但是字節(jié)相等檢查會保證我們從服務器接收到證書。

注意:建議在實際開發(fā)中,把validateCertificateChain設置為true

應用傳輸安全 (App Transport Security)

從iOS9開始,就添加了App Transport Security (ATS),使用ServerTrustPolicyManager和多個ServerTrustPolicy對象可能沒什么影響。如果我們不斷看到CFNetwork SSLHandshake failed (-9806)錯誤,我們可能遇到了這個問題。蘋果的ATS系統(tǒng)重寫了整個challenge系統(tǒng),除非我們在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連接是否使用一個允許的密碼套件。在某些情況下,它需要設置為NONSExceptionAllowsInsecureHTTPLoads必須設置為YES,然后SessionDelegate才能接收到challenge回調(diào)。一旦challenge回調(diào)被調(diào)用,ServerTrustPolicyManager將接管服務器信任評估。如果我們要連接到一個僅支持小于1.2版本的TSL主機,那么還要指定NSTemporaryExceptionMinimumTLSVersion

注意:在實際開發(fā)中,建議始終使用有效的證書。

網(wǎng)絡可達性 (Network Reachability)

NetworkReachabilityManager監(jiān)聽WWANWiFi網(wǎng)絡接口和主機地址的可達性變化。

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

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

manager?.startListening()

注意:要確保manager被強引用,否則會接收不到狀態(tài)變化。另外,在主機字符串中不要包含scheme,也就是說要把https://去掉,否則無法監(jiān)聽。

當使用網(wǎng)絡可達性來決定接下來要做什么時,有以下幾點需要重點注意的:

  • 不要使用Reachability來決定是否發(fā)送一個網(wǎng)絡請求。
    • 我們必須要發(fā)送請求。
  • 當Reachability恢復了,要重試網(wǎng)絡請求。
    • 即使網(wǎng)絡請求失敗,在這個時候也非常適合重試請求。
  • 網(wǎng)絡可達性的狀態(tài)非常適合用來決定為什么網(wǎng)絡請求會失敗。
    • 如果一個請求失敗,應該告訴用戶是離線導致請求失敗的,而不是技術錯誤,例如請求超時。

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

FAQ

Alamofire的起源是什么?

Alamofire是根據(jù) Alamo Fire flower 命名的,是一種矢車菊的混合變種,德克薩斯的州花。

Router和Request Adapter的邏輯是什么?

簡單和靜態(tài)的數(shù)據(jù),例如paths、parameters和共同的headers放在Router。動態(tài)的數(shù)據(jù),例如一個Authorization header,它的值會隨著一個認證系統(tǒng)變化,放在RequestAdapter

動態(tài)的數(shù)據(jù)必須放在ReqeustAdapter的原因是要支持重試操作。當重試一個請求時,原來的請求不會重新建立,也就意味著Router不會再重新調(diào)用。RequestAdapter可以重新調(diào)用,這可以讓我們在重試請求之前更新原始請求的動態(tài)數(shù)據(jù)。


最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內(nèi)容