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

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

請求

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方法:

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,允許被直接傳入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協議的類型來保證請求端點的一致性。這種方法可以用來抽象服務器端的不一致性,並提供類型安全傳送,以及管理身份驗證憑據和其他狀態。

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服務,都需要身份認證。現在比較常見的是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可以重新調用,這可以讓我們在重試請求之前更新原始請求的動態數據。




作者:Lebron_James
鏈接:https://www.jianshu.com/p/903b678d2d3f
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯系作者獲得授權並注明出處。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM