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

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


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

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

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

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

用默認的會話配置創建一個Session Manager

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

用后臺會話配置創建一個Session Manager

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

用默短暫會話配置創建一個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參數。

會話代理

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

重寫閉包

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

/// 重寫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來避免回調到任何apple.com域名。

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

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

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

    return finalRequest
}

子類化

另一個重寫SessionDelegate的實現的方法是把它子類化。通過子類化,我們可以完全自定義他的行為,或者為這個API創建一個代理并且仍然使用它的默認實現。通過創建代理,我們可以跟蹤日志事件、發通知、提供前后實現。下面這個例子演示了如何子類化SessionDelegate,并且有回調的時候打印信息:

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

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

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

請求

requestdownload、uploadstream方法的結果是DataRequest、DownloadRequest、UploadRequestStreamRequest,并且所有請求都繼承自Request。所有的Request并不是直接創建的,而是由session manager創建的。

每個子類都有特定的方法,例如authenticatevalidate、responseJSONuploadProgress,都返回一個實例,以便方法鏈接(也就是用點語法連續調用方法)。

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

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

傳送請求

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

URLConvertible

遵循了URLConvertible協議的類型可以被用來構建URL,然后用來創建URL請求。String、URLURLComponent默認是遵循URLConvertible協議的。它們都可以作為url參數傳入request、uploaddownload方法:

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協議的類型將特定領域模型映射到服務器資源,因為這樣比較方便。

類型安全傳送
extension User: URLConvertible {
    static let baseURLString = "https://example.com"

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

URLRequestConvertible

遵循URLRequestConvertible協議的類型可以被用來構建URL請求。URLRequest默認遵循了URLRequestConvertible,允許被直接傳入request、uploaddownload(推薦用這種方法為單個請求自定義請求頭)。

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

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

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

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

Alamofire.request(urlRequest)

以一種有意義的方式和web應用程序交互的應用,都鼓勵使用自定義的遵循URLRequestConvertible協議的類型來保證請求端點的一致性。這種方法可以用來抽象服務器端的不一致性,并提供類型安全傳送,以及管理身份驗證憑據和其他狀態。

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

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

    // MARK: URLRequestConvertible

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

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

        return try URLEncoding.default.encode(urlRequest, with: result.parameters)
    }
}
Alamofire.request(Router.search(query: "foo bar", page: 1)) // 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

適配和重試請求

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

RequestAdapterRequestRetrier協議可以讓我們更容易地為特定的Web服務創建一個線程安全的認證系統。

RequestAdapter

RequestAdapter協議允許每一個SessionManagerRequest在創建之前被檢查和適配。一個非常特別的使用適配器方法是,在一個特定的認證類型,把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協議允許一個在執行過程中遇到error的請求被重試。當一起使用RequestAdapterRequestRetrier協議時,我們可以為OAuth1、OAuth2、Basic Auth(每次請求API都要提供用戶名和密碼)甚至是exponential backoff重試策略創建資格恢復系統。下面的例子演示了如何實現一個OAuth2 access token的恢復流程。

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

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

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

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

        return SessionManager(configuration: configuration)
    }()

    private let lock = NSLock()

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

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

    // MARK: - Initialization

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

    // MARK: - RequestAdapter

    func adapt(_ urlRequest: URLRequest) throws -> URLRequest {
        if let 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,并且根據失敗的順序來重試所有失敗的請求。(如果需要讓他們按照創建的時間順序來執行,可以使用他們的task identifier來排序)

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

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

自定義響應序列化

Alamofire為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, ...)。

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

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

響應映射

響應映射是自定義響應最簡單的方式。它轉換響應的值,同時保留最終錯誤和元數據。例如,我們可以把一個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標記,意思是調用loadUser方法可以不接收它的返回值;也可以用_來忽略返回值。

當 map/flatMap 閉包會產生比較大的數據量時,要保證這個閉包在子線程中執行:

@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也可以用于下載響應。

處理錯誤

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

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

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

創建一個自定義的響應序列化器

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

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

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

泛型響應對象序列化

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

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

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

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

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

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

            return .success(responseObject)
        }

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

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

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

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

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

同樣地方法可以用來處理返回對象集合的接口:

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

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

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

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

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

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

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

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

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

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

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

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

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

安全

對于安全敏感的數據來說,在與服務器和web服務交互時使用安全的HTTPS連接是非常重要的一步。默認情況下,Alamofire會使用蘋果安全框架內置的驗證方法來評估服務器提供的證書鏈。雖然保證了證書鏈是有效的,但是不能防止man-in-the-middle (MITM)攻擊或者其他潛在的漏洞。為了減少MITM攻擊,處理用戶的敏感數據或財務信息的應用,應該使用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負責存儲一個內部的服務器信任策略到特定主機的映射。這樣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握手成功:
    • 證書鏈必須是有效的。
    • 證書鏈必須包含一個已經固定的證書。
    • 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。

應用傳輸安全 (App Transport Security)

從iOS9開始,就添加了App Transport Security (ATS),使用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連接是否使用一個允許的密碼套件。在某些情況下,它需要設置為NONSExceptionAllowsInsecureHTTPLoads必須設置為YES,然后SessionDelegate才能接收到challenge回調。一旦challenge回調被調用,ServerTrustPolicyManager將接管服務器信任評估。如果我們要連接到一個僅支持小于1.2版本的TSL主機,那么還要指定NSTemporaryExceptionMinimumTLSVersion。

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

網絡可達性 (Network Reachability)

NetworkReachabilityManager監聽WWANWiFi網絡接口和主機地址的可達性變化。

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

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

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可以重新調用,這可以讓我們在重試請求之前更新原始請求的動態數據。


最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,837評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,196評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,688評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,654評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,456評論 6 406
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,955評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,044評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,195評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,725評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,608評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,802評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,318評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,048評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,422評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,673評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,424評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,762評論 2 372

推薦閱讀更多精彩內容