Alamofire 用法(二)

Wednesday, April 11, 2018

上篇文章讲了一些基础的请求和响应的方法,这篇文章再详细讲解下 Alamofire 的一些高级用法

Alamofire 是在 URLSession 和 URL 加载系统的基础上写的。所以再以后,我还会对 URLSeesion 进行详细的讲解一番。

Session Manager

我们平常使用的封装的方法,Alamofire.request,使用的是默认的 Alamofire.SessionManager,并且这个 SessionManager 是使用默认的 URLSessionConfiguration 配置的。

<code class="language-objectivec">Alamofire.request("https://httpbin.org/get")

let sessionManager = Alamofire.SessionManager.default
sessionManager.request("https://httpbin.org/get")
</code>

我们可以自己创建后台会话和短暂会话的 session manager,还可以自定义默认的会话配置来创建新的 session manager,例如修改默认的 header httpAdditionalHeaderstimeoutIntervalForRequest

用默认的会话配置创建一个 Session Manager

<code class="language-objectivec">let configuration = URLSessionConfiguration.default
let sessionManager = Alamofire.SessionManager(configuration: configuration)
</code>

用后台会话配置创建一个 Session Manager

<code class="language-objectivec">let configuration = URLSessionConfiguration.background(withIdentifier: "com.example.app.background")
let sessionManager = Alamofire.SessionManager(configuration: configuration)
</code>

用短暂会话配置创建一个 Session Manager

<code class="language-objectivec">let configuration = URLSessionConfiguration.ephemeral
let sessionManager = Alamofire.SessionManager(configuration: configuration)
</code>

修改会话配置

<code class="language-objectivec">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)
</code>
  • 这里需要注意的是,如果我们要修改 Authorization 或者 Content-Type,应该使用 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(): 暂停底层的任务和调度队列
    • resumen(): 恢复底层的任务和调度队列,如果 manager 的 startRequestsImmediately 不是 true,那么必须调用 resume() 来开始请求。
    • cancel(): 取消底层的任务,并产生一个 error,error 被传入任何已经注册的响应 handlers。

传送请求

  • 当我们建立网络栈的时候要使用通用的模式。在通用模式的设计中,一个很重要的部分就是如何传送请求。遵循 Router 设计模式的 URLConvertibleURLRequestConvertible 协议可以帮助我们。

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 协议的类型将特定领域模型映射到服务器资源。这样会比较方便。

类型安全传送
<code class="language-objectivec">extension User: URLConvertible {
    static let baseURLString = "https://example.com"

    func asURL() throws -> URL {
        let urlString = User.baseURLString + "/users/\(username)/"
        return try urlString.asURL()
    }
}
</code>





<code class="language-objectivec">let user = User(username: "mattt")
Alamofire.request(user) // https://example.com/users/mattt
</code>

URLRequestConertible

  • 遵循 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 参数抽象
<code class="language-objectivec">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)
    }
}
</code>





<code class="language-objectivec">Alamofire.request(Router.search(query: "foo bar", page: 1)) // https://example.com/search?q=foo%20bar&offset=50
</code>

CRUD 和授权

<code class="language-objectivec">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
    }
}
</code>





<code class="language-objectivec">Alamofire.request(Router.readUser("mattt")) // GET https://example.com/users/mattt
</code>

适配和重试请求

  • 现在的大多数Web服务,都需要身份认证。现在比较常见的是OAuth。通常是需要一个access token来授权应用或者用户,然后才可以使用各种支持的Web服务。创建这些access token是比较麻烦的,当access token过期之后就比较麻烦了,我们需要重新创建一个新的。有许多线程安全问题要考虑。
  • RequestAdapter 和 RequestRetrier 协议可以让我们更容易地为特定的 Web 服务创建一个线程安全的认证系统。

RequestAdapter

RequestAdapter 协议允许每一个 SessionManager 的 Request 在创建之前被检查和适配。一个非常特别的使用适配器方法是,在一个特定的认证类型,把 Authorization header 拼接到请求。

<code class="language-objectivec">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
    }

}
</code>





<code class="language-objectivec">let sessionManager = SessionManager()
sessionManager.adapter = AccessTokenAdapter(accessToken: "1234")

sessionManager.request("https://httpbin.org/get")
</code>

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实例,来管理一个恢复流程。

安全

  • 对于安全敏感的数据来说,在与服务器和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握手成功。
  • 其他主机将会默认使用苹果提供的验证。

网络可达性 (Network Reachability)

NetworkReachabilityManager监听WWAN和WiFi网络接口和主机地址的可达性变化。

<code class="language-objectivec">let manager = NetworkReachabilityManager(host: "www.apple.com")

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

manager?.startListening()
</code>
  • 注意:要确保manager被强引用,否则会接收不到状态变化。另外,在主机字符串中不要包含scheme,也就是说要把 https:// 去掉,否则无法监听。

  • 当使用网络可达性来决定接下来要做什么时,有以下几点需要重点注意的:

    • 不要使用Reachability来决定是否发送一个网络请求。
    • 我们必须要发送请求。
    • 当Reachability恢复了,要重试网络请求。
    • 即使网络请求失败,在这个时候也非常适合重试请求。
    • 网络可达性的状态非常适合用来决定为什么网络请求会失败。
    • 如果一个请求失败,应该告诉用户是离线导致请求失败的,而不是技术错误,例如请求超时。
iOSSwift

URLSession 介绍

Alamofire 用法(一)