Alamofire是在URLSession
和URL加載系統的基礎上寫的。所以,為了更好地學習這個框架,建議先熟悉下列幾個底層網絡協議棧:
- URL Loading System Programming Guide >>
- URLSession Class Reference >>
- URLCache Class Reference >>
- URLAuthenticationChallenge Class Reference >>
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 httpAdditionalHeaders
和timeoutIntervalForRequest
。
用默認的會話配置創建一個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.request
API、URLRequestConvertible
和ParameterEncoding
的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 ) } }
總的來說,無論是默認實現還是重寫閉包,都應該提供必要的功能。子類化應該作為最后的選擇。
請求
request
、download
、upload
和stream
方法的結果是DataRequest
、DownloadRequest
、UploadRequest
和StreamRequest
,並且所有請求都繼承自Request
。所有的Request
並不是直接創建的,而是由session manager創建的。
每個子類都有特定的方法,例如authenticate
、validate
、responseJSON
和uploadProgress
,都返回一個實例,以便方法鏈接(也就是用點語法連續調用方法)。
請求可以被暫停、恢復和取消:
suspend()
:暫停底層的任務和調度隊列resume()
:恢復底層的任務和調度隊列。如果manager的startRequestsImmediately
不是true
,那么必須調用resume()
來開始請求。cancel()
:取消底層的任務,並產生一個error,error被傳入任何已經注冊的響應handlers。
傳送請求
隨着應用的不多增大,當我們建立網絡棧的時候要使用通用的模式。在通用模式的設計中,一個很重要的部分就是如何傳送請求。遵循Router
設計模式的URLConvertible
和URLRequestConvertible
協議可以幫助我們。
URLConvertible
遵循了URLConvertible
協議的類型可以被用來構建URL,然后用來創建URL請求。String
、URL
和URLComponent
默認是遵循URLConvertible
協議的。它們都可以作為url
參數傳入request
、upload
和download
方法:
let urlString = "https://httpbin.org/post" Alamofire.request(urlString, method: .post) let url = URL(string: urlString)! Alamofire.request(url, method: .post) let urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: true)! Alamofire.request(urlComponents, method: .post)
以一種有意義的方式和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
、upload
和download
(推薦用這種方法為單個請求自定義請求頭)。
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過期之后就比較麻煩了,我們需要重新創建一個新的。有許多線程安全問題要考慮。
RequestAdapter
和RequestRetrier
協議可以讓我們更容易地為特定的Web服務創建一個線程安全的認證系統。
RequestAdapter
RequestAdapter
協議允許每一個SessionManager
的Request
在創建之前被檢查和適配。一個非常特別的使用適配器方法是,在一個特定的認證類型,把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的請求被重試。當一起使用RequestAdapter
和RequestRetrier
協議時,我們可以為OAuth1、OAuth2、Basic Auth(每次請求API都要提供用戶名和密碼)甚至是exponential backoff重試策略創建資格恢復系統。下面的例子演示了如何實現一個OAuth2 access token的恢復流程。
免責聲明:這不是一個全面的OAuth2解決方案。這僅僅是演示如何把RequestAdapter
和RequestRetrier
協議結合起來創建一個線程安全的恢復系統。
重申: 不要把這個例子復制到實際的開發應用中,這僅僅是一個例子。每個認證系統必須為每個特定的平台和認證類型重新定制。
class OAuth2Handler: RequestAdapter, RequestRetrier { private typealias RefreshCompletion = (_ succeeded: Bool, _ accessToken: String?, _ refreshToken: String?) -> Void private let sessionManager: SessionManager = { let configuration = URLSessionConfiguration.default configuration.httpAdditionalHeaders = SessionManager.defaultHTTPHeaders return SessionManager(configuration: configuration) }() private let lock = NSLock() private var clientID: String private var baseURLString: String private var accessToken: String private var refreshToken: String private var isRefreshing = false private var requestsToRetry: [RequestRetryCompletion] = [] // MARK: - Initialization public init(clientID: String, baseURLString: String, accessToken: String, refreshToken: String) { self.clientID = clientID self.baseURLString = baseURLString self.accessToken = accessToken self.refreshToken = refreshToken } // MARK: - RequestAdapter func adapt(_ urlRequest: URLRequest) throws -> URLRequest { if let urlString = urlRequest.url?.absoluteString, urlString.hasPrefix(baseURLString) { var urlRequest = urlRequest urlRequest.setValue("Bearer " + accessToken, forHTTPHeaderField: "Authorization") return urlRequest } return urlRequest } // MARK: - RequestRetrier func should(_ manager: SessionManager, retry request: Request, with error: Error, completion: @escaping RequestRetryCompletion) { lock.lock() ; defer { lock.unlock() } if let response = request.task?.response as? HTTPURLResponse, response.statusCode == 401 { requestsToRetry.append(completion) if !isRefreshing { refreshTokens { [weak self] succeeded, accessToken, refreshToken in guard let strongSelf = self else { return } strongSelf.lock.lock() ; defer { strongSelf.lock.unlock() } if let accessToken = accessToken, let refreshToken = refreshToken { strongSelf.accessToken = accessToken strongSelf.refreshToken = refreshToken } strongSelf.requestsToRetry.forEach { $0(succeeded, 0.0) } strongSelf.requestsToRetry.removeAll() } } } else { completion(false, 0.0) } } // MARK: - Private - Refresh Tokens private func refreshTokens(completion: @escaping RefreshCompletion) { guard !isRefreshing else { return } isRefreshing = true let urlString = "\(baseURLString)/oauth2/token" let parameters: [String: Any] = [ "access_token": accessToken, "refresh_token": refreshToken, "client_id": clientID, "grant_type": "refresh_token" ] sessionManager.request(urlString, method: .post, parameters: parameters, encoding: JSONEncoding.default) .responseJSON { [weak self] response in guard let strongSelf = self else { return } if let json = response.result.value as? [String: Any], let accessToken = json["access_token"] as? String, let refreshToken = json["refresh_token"] as? String { completion(true, accessToken, refreshToken) } else { completion(false, nil, nil) } strongSelf.isRefreshing = false } } }
let baseURLString = "https://some.domain-behind-oauth2.com" let oauthHandler = OAuth2Handler( clientID: "12345678", baseURLString: baseURLString, accessToken: "abcd1234", refreshToken: "ef56789a" ) let sessionManager = SessionManager() sessionManager.adapter = oauthHandler sessionManager.retrier = oauthHandler let urlString = "\(baseURLString)/some/endpoint" sessionManager.request(urlString).validate().responseJSON { response in debugPrint(response) }
一旦OAuth2Handler
為SessionManager
被應用與adapter
和retrier
,他將會通過自動恢復access token來處理一個非法的access token error,並且根據失敗的順序來重試所有失敗的請求。(如果需要讓他們按照創建的時間順序來執行,可以使用他們的task identifier來排序)
上面這個例子僅僅檢查了401
響應碼,不是演示如何檢查一個非法的access token error。在實際開發應用中,我們想要檢查realm
和www-authenticate
header響應,雖然這取決於OAuth2的實現。
還有一個要重點注意的是,這個認證系統可以在多個session manager之間共享。例如,可以在同一個Web服務集合使用default
和ephemeral
會話配置。上面這個例子可以在多個session manager間共享一個oauthHandler
實例,來管理一個恢復流程。
自定義響應序列化
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) } } }
map
和flatMap
也可以用於下載響應。
處理錯誤
在實現自定義響應序列化器或者對象序列化方法前,思考如何處理所有可能出現的錯誤是非常重要的。有兩個方法: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連接是否使用一個允許的密碼套件。在某些情況下,它需要設置為NO
。NSExceptionAllowsInsecureHTTPLoads
必須設置為YES
,然后SessionDelegate
才能接收到challenge回調。一旦challenge回調被調用,ServerTrustPolicyManager
將接管服務器信任評估。如果我們要連接到一個僅支持小於1.2
版本的TSL主機,那么還要指定NSTemporaryExceptionMinimumTLSVersion
。
注意:在實際開發中,建議始終使用有效的證書。
網絡可達性 (Network Reachability)
NetworkReachabilityManager
監聽WWAN
和WiFi
網絡接口和主機地址的可達性變化。
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
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯系作者獲得授權並注明出處。