• 【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
    來源:简书
    简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。
  • 相关阅读:
    leetcode701. Insert into a Binary Search Tree
    leetcode 958. Check Completeness of a Binary Tree 判断是否是完全二叉树 、222. Count Complete Tree Nodes
    leetcode 110. Balanced Binary Tree
    leetcode 104. Maximum Depth of Binary Tree 111. Minimum Depth of Binary Tree
    二叉树
    leetcode 124. Binary Tree Maximum Path Sum 、543. Diameter of Binary Tree(直径)
    5. Longest Palindromic Substring
    128. Longest Consecutive Sequence
    Mac OS下Android Studio的Java not found问题,androidfound
    安卓 AsyncHttpClient
  • 原文地址:https://www.cnblogs.com/jukaiit/p/9283507.html
Copyright © 2020-2023  润新知