這篇文章是 Alamofire 5.0 以前的文檔,最新文檔請查看:Alamofire 5 的使用 - 高級用法
這邊文章介紹的是Alamofire框架的高級用法,如果之前沒有看過基本用法的,可以先去看看【iOS開發】Alamofire框架的使用一 —— 基本用法
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服務,都需要身份認證?,F在比較常見的是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
可以重新調用,這可以讓我們在重試請求之前更新原始請求的動態數據。