Alamofire-Result 3.1.5-karumi

Alamofire-Result 3.1.5-karumi

测试已测试
语言语言 SwiftSwift
许可 MIT
发布最后发布2016年3月
SPM支持 SPM

Davide Mendolia 维护。



Alamofire-Result 3.1.5-karumi

  • Alamofire 软件基金会和 Karumi

Alamofire: Result<Elegant> Networking in Swift

Alamofire+Result 是 Alamofire 的一个分支,Alamofire 是一个用 Swift 编写的 HTTP 网络库,结合了 antitypical/Result 来以函数式方式处理网络结果。

免责声明:这是一个基于 Alamofire 的分支,原本使用 antitypical/Result 而不是 Alamofire 结果。在提交任何问题之前,请务必先检查 Alamofire 仓库。

功能特性

  • [x] 可链式请求/响应方法
  • [x] URL/JSON/plist 参数编码
  • [x] 上传文件/数据/流/多部分表单数据
  • [x] 使用请求或恢复数据下载
  • [x] 使用 NSURLCredential 进行身份验证
  • [x] HTTP 响应验证
  • [x] TLS 证书和公共密钥固定
  • [x] 进度闭包 & NSProgress
  • [x] cURL 调试输出
  • [x] 综合单元测试覆盖
  • [x] 完整文档

要求

  • iOS 8.0+ / Mac OS X 10.9+ / tvOS 9.0+ / watchOS 2.0+
  • Xcode 7.2+

迁移指南

沟通

  • 如果您 需要帮助,请使用 Stack Overflow。 (标签‘alamofire’)
  • 如果您想 提问,请使用 Stack Overflow
  • 如果您 发现了一个错误,请提交一个 issue。
  • 如果您 有以下功能请求,请提交一个 issue。
  • 如果您 愿意贡献,提交一个 pull request。

安装

嵌入式框架需要最小部署目标为 iOS 8 或 OS X Mavericks (10.9)。

由于缺少对框架的支持,Alamofire 不再支持 iOS 7。没有框架,运行 Travis-CI 对 iOS 7 执行将需要第二个重复的测试目标。这个单独的测试套件将需要导入所有 Swift 文件,并且测试将需要复制和重新编写。这种分割将难以维护以确保 Alamofire 生态系统的最高质量。

手动方式

如果您不希望使用上述任何一种依赖管理器,可以将 Alamofire 手动集成到您的项目中。

嵌入式框架

  • 打开 Terminal,使用 cd 命令进入您的顶级项目目录,如果您没有将项目初始化为 git 仓库,请运行以下命令:
$ git init
  • 通过以下命令将 Alamofire 添加为 git 子模块
$ git submodule add https://github.com/Karumi/Alamofire-Result.git
  • 打开新的 Alamofire 文件夹,并将 Alamofire.xcodeproj 拖到您的应用程序的 Xcode 项目的“项目导航器”中。

    它应该嵌套在您的应用程序的蓝色项目图标下面。它是否位于所有其他 Xcode 组的上方或下方并不重要。

  • 在“项目导航器”中选择 Alamofire.xcodeproj,并检查部署目标是否符合您的应用程序目标。

  • 接下来,在“项目导航器”中(蓝色项目图标)选择您的应用程序项目,进入目标配置窗口,并在侧边栏的“目标”标题下选择应用程序目标。
  • 在窗口顶部的标签栏中,打开“通用”面板。
  • 在“嵌入式二进制文件”部分下点击 + 按钮。
  • 您将看到两个不同的 Alamofire.xcodeproj 文件夹,每个文件夹中都有一个嵌套在“Products”文件夹中的 Alamofire.framework 的两个不同版本。

    选择哪个“Products”文件夹并不重要,但选择顶部或底部的 Alamofire.framework 是有关系的。

  • 为 iOS 选择顶部的 Alamofire.framework,为 OS X 选择底部的。

    您可以通过检查项目的构建日志来验证您选择了哪一个。对于 Alamofire 的构建目标将列为 Alamofire iOSAlamofire OSX

  • 就这些了!

Alamofire.framework 会自动添加为目标依赖项,链接框架和嵌入式框架,在复制文件构建阶段即可,这些都是您在模拟器和设备上构建所需的所有内容。


用法

发送请求

import Alamofire_Result

Alamofire.request(.GET, "https://httpbin.org/get")

响应处理

Alamofire_Result.request(.GET, "https://httpbin.org/get", parameters: ["foo": "bar"])
         .responseJSON { response in
             print(response.request)  // original URL request
             print(response.response) // URL response
             print(response.data)     // server data
             print(response.result)   // result of response serialization

             if let JSON = response.result.value {
                 print("JSON: \(JSON)")
             }
         }

Alamofire 中的网络请求是 异步 进行的。对于不熟悉异步编程概念的程序员来说,这种方法可能是一个令人沮丧的来源,但这样做有 很好的理由

而不是阻塞执行以等待从服务器获得响应,您需要指定一个回调,一旦收到响应就处理它。请求的结果仅在响应处理程序的作用域内可用。所有依赖于响应或从服务器收到的数据的执行都必须在处理程序内完成。

响应序列化

内建响应方法

  • response()
  • responseData()
  • responseString(encoding: NSStringEncoding)
  • responseJSON(options: NSJSONReadingOptions)
  • responsePropertyList(options: NSPropertyListReadOptions)

响应处理程序

Alamofire_Result.request(.GET, "https://httpbin.org/get", parameters: ["foo": "bar"])
         .response { request, response, data, error in
             print(request)
             print(response)
             print(data)
             print(error)
          }

response 序列化器不会评估响应数据中的任何部分。它只是直接从 URL 会话代理将所有信息转发出去。我们强烈建议您利用其他响应序列化程序,利用 ResponseResult 类型。

响应数据处理程序

Alamofire_Result.request(.GET, "https://httpbin.org/get", parameters: ["foo": "bar"])
         .responseData { response in
             print(response.request)
             print(response.response)
             print(response.result)
          }

响应字符串处理程序

Alamofire_Result.request(.GET, "https://httpbin.org/get")
         .responseString { response in
             print("Success: \(response.result.isSuccess)")
             print("Response String: \(response.result.value)")
         }

响应JSON处理程序

Alamofire_Result.request(.GET, "https://httpbin.org/get")
         .responseJSON { response in
             debugPrint(response)
         }

响应处理器链

响应处理器甚至可以链式使用

Alamofire_Result.request(.GET, "https://httpbin.org/get")
         .responseString { response in
             print("Response String: \(response.result.value)")
         }
         .responseJSON { response in
             print("Response JSON: \(response.result.value)")
         }

HTTP方法

Alamofire_Result.Method 列出了在 RFC 7231 §4.3 中定义的HTTP方法

public enum Method: String {
    case OPTIONS, GET, HEAD, POST, PUT, PATCH, DELETE, TRACE, CONNECT
}

这些值可以作为Alamofire.request方法的第一個參數傳遞

Alamofire_Result.request(.POST, "https://httpbin.org/post")

Alamofire_Result.request(.PUT, "https://httpbin.org/put")

Alamofire_Result.request(.DELETE, "https://httpbin.org/delete")

参数

带有URL编码参数的GET请求

Alamofire_Result.request(.GET, "https://httpbin.org/get", parameters: ["foo": "bar"])
// http://httpbin.org/get?foo=bar

带有URL编码参数的POST请求

let parameters = [
    "foo": "bar",
    "baz": ["a", 1],
    "qux": [
        "x": 1,
        "y": 2,
        "z": 3
    ]
]

Alamofire_Result.request(.POST, "https://httpbin.org/post", parameters: parameters)
// HTTP body: foo=bar&baz[]=a&baz[]=1&qux[x]=1&qux[y]=2&qux[z]=3

参数编码

参数还可以使用 ParameterEncoding 枚举被编码为JSON、属性列表或任何自定义格式

enum ParameterEncoding {
    case URL
    case URLEncodedInURL
    case JSON
    case PropertyList(format: NSPropertyListFormat, options: NSPropertyListWriteOptions)
    case Custom((URLRequestConvertible, [String: AnyObject]?) -> (NSMutableURLRequest, NSError?))

    func encode(request: NSURLRequest, parameters: [String: AnyObject]?) -> (NSURLRequest, NSError?)
    { ... }
}
  • URL:一个查询字符串,用于将现有URL查询设置或追加到任何用于 GETHEADDELETE 请求的URL,或者将作为请求体设置任何其他HTTP方法的请求体。带有请求体的编码请求的HTTP头字段Content-Type 被设置为 application/x-www-form-urlencoded。由于没有公开的规格说明如何编码集合类型,Alamofire遵循在数组值的关键字后追加 [] 的约定 (foo[]=1&foo[]=2),并在嵌套字典值周围添加括号以关键字的形式来追加 (foo[bar]=baz)。
  • URLEncodedInURL:创建要设置或追加到任何现有URL查询的查询字符串。与.URL情况使用相同的实现,但始终将编码结果应用于URL。
  • JSON:使用 NSJSONSerialization 创建参数对象的JSON表示形式,将其设置为请求体。编码请求的HTTP头字段Content-Type被设置为 application/json
  • PropertyList:使用 NSPropertyListSerialization 创建参数对象的plist表示形式,根据关联的格式和写入选项值设置,并将其设置为请求体。编码请求的HTTP头字段Content-Type被设置为 application/x-plist
  • Custom:使用关联的闭包值根据现有请求和参数构造新的请求。

手动对NSURLRequest进行参数编码

let URL = NSURL(string: "https://httpbin.org/get")!
var request = NSMutableURLRequest(URL: URL)

let parameters = ["foo": "bar"]
let encoding = Alamofire.ParameterEncoding.URL
(request, _) = encoding.encode(request, parameters: parameters)

带有JSON编码参数的POST请求

let parameters = [
    "foo": [1,2,3],
    "bar": [
        "baz": "qux"
    ]
]

Alamofire_Result.request(.POST, "https://httpbin.org/post", parameters: parameters, encoding: .JSON)
// HTTP body: {"foo": [1, 2, 3], "bar": {"baz": "qux"}}

HTTP头

在全局request方法中直接支持向Request添加自定义HTTP头。这使得 attaching HTTP headers to a Request变得非常容易,这个Request可以随时改变。

对于不改变的HTTP头,建议将其设置在NSURLSessionConfiguration上,这样它们就会自动应用于底层NSURLSession创建的任何NSURLSessionTask

let headers = [
    "Authorization": "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==",
    "Content-Type": "application/x-www-form-urlencoded"
]

Alamofire_Result.request(.GET, "https://httpbin.org/get", headers: headers)
         .responseJSON { response in
             debugPrint(response)
         }

缓存

缓存由系统框架级别的 NSURLCache 处理。

文件上载

支持的上载类型

  • 文件
  • 数据
  • MultipartFormData

上传文件

let fileURL = NSBundle.mainBundle().URLForResource("Default", withExtension: "png")
Alamofire_Result.upload(.POST, "https://httpbin.org/post", file: fileURL)

带有进度的上传

Alamofire_Result.upload(.POST, "https://httpbin.org/post", file: fileURL)
         .progress { bytesWritten, totalBytesWritten, totalBytesExpectedToWrite in
             print(totalBytesWritten)

             // This closure is NOT called on the main queue for performance
             // reasons. To update your ui, dispatch to the main queue.
             dispatch_async(dispatch_get_main_queue()) {
                 print("Total bytes written on main queue: \(totalBytesWritten)")
             }
         }
         .responseJSON { response in
             debugPrint(response)
         }

MultipartFormData 上传

Alamofire_Result.upload(
    .POST,
    "https://httpbin.org/post",
    multipartFormData: { multipartFormData in
        multipartFormData.appendBodyPart(fileURL: unicornImageURL, name: "unicorn")
        multipartFormData.appendBodyPart(fileURL: rainbowImageURL, name: "rainbow")
    },
    encodingCompletion: { encodingResult in
        switch encodingResult {
        case .Success(let upload, _, _):
            upload.responseJSON { response in
                debugPrint(response)
            }
        case .Failure(let encodingError):
            print(encodingError)
        }
    }
)

下载

支持的下载类型

  • 请求
  • 恢复数据

下载文件

Alamofire_Result.download(.GET, "https://httpbin.org/stream/100") { temporaryURL, response in
    let fileManager = NSFileManager.defaultManager()
    let directoryURL = fileManager.URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)[0]
    let pathComponent = response.suggestedFilename

    return directoryURL.URLByAppendingPathComponent(pathComponent!)
}

使用默认下载位置

let destination = Alamofire_Result.Request.suggestedDownloadDestination(directory: .DocumentDirectory, domain: .UserDomainMask)
Alamofire_Result.download(.GET, "https://httpbin.org/stream/100", destination: destination)

带有进度的文件下载

Alamofire_Result.download(.GET, "https://httpbin.org/stream/100", destination: destination)
         .progress { bytesRead, totalBytesRead, totalBytesExpectedToRead in
             print(totalBytesRead)

             // This closure is NOT called on the main queue for performance
             // reasons. To update your ui, dispatch to the main queue.
             dispatch_async(dispatch_get_main_queue()) {
                 print("Total bytes read on main queue: \(totalBytesRead)")
             }
         }
         .response { _, _, _, error in
             if let error = error {
                 print("Failed with error: \(error)")
             } else {
                 print("Downloaded file successfully")
             }
         }

获取失败下载的恢复数据

Alamofire_Result.download(.GET, "https://httpbin.org/stream/100", destination: destination)
         .response { _, _, data, _ in
             if let
                 data = data,
                 resumeDataString = NSString(data: data, encoding: NSUTF8StringEncoding)
             {
                 print("Resume Data: \(resumeDataString)")
             } else {
                 print("Resume Data was empty")
             }
         }

如果可用,参数“data”将自动填充“resumeData”。

let download = Alamofire_Result.download(.GET, "https://httpbin.org/stream/100", destination: destination)
download.response { _, _, _, _ in
    if let
        resumeData = download.resumeData,
        resumeDataString = NSString(data: resumeData, encoding: NSUTF8StringEncoding)
    {
        print("Resume Data: \(resumeDataString)")
    } else {
        print("Resume Data was empty")
    }
}

身份验证

身份验证在系统框架级别由NSURLCredentialNSURLAuthenticationChallenge处理。

支持的身份验证方案

HTTP基本认证

在适当的场合,“Request”上的authenticate方法将自动向NSURLAuthenticationChallenge提供一个NSURLCredential

let user = "user"
let password = "password"

Alamofire_Result.request(.GET, "https://httpbin.org/basic-auth/\(user)/\(password)")
         .authenticate(user: user, password: password)
         .responseJSON { response in
             debugPrint(response)
         }

根据您的服务器实现,适当的头“Authorization”也可能被使用。

let user = "user"
let password = "password"

let credentialData = "\(user):\(password)".dataUsingEncoding(NSUTF8StringEncoding)!
let base64Credentials = credentialData.base64EncodedStringWithOptions([])

let headers = ["Authorization": "Basic \(base64Credentials)"]

Alamofire_Result.request(.GET, "https://httpbin.org/basic-auth/user/password", headers: headers)
         .responseJSON { response in
             debugPrint(response)
         }

使用NSURLCredential进行身份验证

let user = "user"
let password = "password"

let credential = NSURLCredential(user: user, password: password, persistence: .ForSession)

Alamofire_Result.request(.GET, "https://httpbin.org/basic-auth/\(user)/\(password)")
         .authenticate(usingCredential: credential)
         .responseJSON { response in
             debugPrint(response)
         }

验证

默认情况下,Alamofire会将任何完成的请求视为成功,无论响应内容如何。在响应处理程序之前调用validate会导致如果响应具有不可接受的状态代码或MIME类型,则生成错误。

手动验证

Alamofire_Result.request(.GET, "https://httpbin.org/get", parameters: ["foo": "bar"])
         .validate(statusCode: 200..<300)
         .validate(contentType: ["application/json"])
         .response { response in
             print(response)
         }

自动验证

自动验证状态码是否在200...299范围内,以及响应的Content-Type头是否与请求的Accept头匹配(如果提供)。

Alamofire_Result.request(.GET, "https://httpbin.org/get", parameters: ["foo": "bar"])
         .validate()
         .responseJSON { response in
             switch response.result {
             case .Success:
                 print("Validation Successful")
             case .Failure(let error):
                 print(error)
             }
         }

可打印

let request = Alamofire_Result.request(.GET, "https://httpbin.org/ip")

print(request)
// GET https://httpbin.org/ip (200)

调试打印

let request = Alamofire_Result.request(.GET, "https://httpbin.org/get", parameters: ["foo": "bar"])

debugPrint(request)

输出(cURL)

$ curl -i \
    -H "User-Agent: Alamofire" \
    -H "Accept-Encoding: Accept-Encoding: gzip;q=1.0,compress;q=0.5" \
    -H "Accept-Language: en;q=1.0,fr;q=0.9,de;q=0.8,zh-Hans;q=0.7,zh-Hant;q=0.6,ja;q=0.5" \
    "https://httpbin.org/get?foo=bar"


高级用法

Alamofire建立在NSURLSession和Foundation URL加载系统之上。为了充分发挥此框架的优势,建议您熟悉底层网络堆栈的概念和功能。

推荐阅读

管理器

类似于 Alamofire_Result.request 的顶级便捷方法使用 Alamofire_Result.Manager 的共享实例,该实例使用默认的 NSURLSessionConfiguration 进行配置。

因此,以下两个语句是等效的

Alamofire_Result.request(.GET, "https://httpbin.org/get")
let manager = Alamofire_Result.Manager.sharedInstance
manager.request(NSURLRequest(URL: NSURL(string: "https://httpbin.org/get")!))

应用程序可以为后台和短暂会话创建管理器,还可以创建自定义默认会话配置的新管理器,例如,用于默认头信息(HTTPAdditionalHeaders)或超时时间(timeoutIntervalForRequest)。

创建带有默认配置的管理器

let configuration = NSURLSessionConfiguration.defaultSessionConfiguration()
let manager = Alamofire_Result.Manager(configuration: configuration)

创建带有后台配置的管理器

let configuration = NSURLSessionConfiguration.backgroundSessionConfigurationWithIdentifier("com.example.app.background")
let manager = Alamofire_Result.Manager(configuration: configuration)

创建带有短暂配置的管理器

let configuration = NSURLSessionConfiguration.ephemeralSessionConfiguration()
let manager = Alamofire_Result.Manager(configuration: configuration)

修改会话配置

var defaultHeaders = Alamofire_Result.Manager.sharedInstance.session.configuration.HTTPAdditionalHeaders ?? [:]
defaultHeaders["DNT"] = "1 (Do Not Track Enabled)"

let configuration = NSURLSessionConfiguration.defaultSessionConfiguration()
configuration.HTTPAdditionalHeaders = defaultHeaders

let manager = Alamofire_Result.Manager(configuration: configuration)

这不建议用于 AuthorizationContent-Type 头信息。相反,请分别使用 URLRequestConvertibleParameterEncoding

请求

requestuploaddownload 方法的返回结果是 Alamofire_Result.Request 的实例。总是使用拥有管理器的构造方法创建请求,而不是直接初始化。

authenticatevalidateresponseData 这样的方法返回调用者,以方便链式调用。

请求可以被挂起、恢复和取消

  • suspend():挂起底层任务和派发队列
  • resume():恢复底层任务和派发队列。如果拥有管理器没有将 startRequestsImmediately 设置为 true,则请求必须调用 resume() 才能开始。
  • cancel():取消底层任务,产生一个错误,该错误传递到任何已注册的响应处理程序。

响应序列化

创建自定义响应序列化器

Alamofire 为字符串、JSON 和属性列表提供内置的响应序列化,但也可以通过在 Alamofire_Result.Request 的扩展中添加来添加其他序列化。

例如,以下是使用 Ono 的响应处理程序可能实现的方式

extension Request {
    public static func XMLResponseSerializer() -> ResponseSerializer<ONOXMLDocument, NSError> {
        return ResponseSerializer { request, response, data, error in
            guard error == nil else { return .Failure(error!) }

            guard let validData = data else {
                let failureReason = "Data could not be serialized. Input data was nil."
                let error = Error.errorWithCode(.DataSerializationFailed, failureReason: failureReason)
                return .Failure(error)
            }

            do {
                let XML = try ONOXMLDocument(data: validData)
                return .Success(XML)
            } catch {
                return .Failure(error as NSError)
            }
        }
    }

    public func responseXMLDocument(completionHandler: Response<ONOXMLDocument, NSError> -> Void) -> Self {
        return response(responseSerializer: Request.XMLResponseSerializer(), completionHandler: completionHandler)
    }
}

泛型响应对象序列化

可以使用泛型来提供自动、类型安全的响应对象序列化。

public protocol ResponseObjectSerializable {
    init?(response: NSHTTPURLResponse, representation: AnyObject)
}

extension Request {
    public func responseObject<T: ResponseObjectSerializable>(completionHandler: Response<T, NSError> -> Void) -> Self {
        let responseSerializer = ResponseSerializer<T, NSError> { request, response, data, error in
            guard error == nil else { return .Failure(error!) }

            let JSONResponseSerializer = Request.JSONResponseSerializer(options: .AllowFragments)
            let result = JSONResponseSerializer.serializeResponse(request, response, data, error)

            switch result {
            case .Success(let value):
                if let
                    response = response,
                    responseObject = T(response: response, representation: value)
                {
                    return .Success(responseObject)
                } else {
                    let failureReason = "JSON could not be serialized into response object: \(value)"
                    let error = Error.errorWithCode(.JSONSerializationFailed, failureReason: failureReason)
                    return .Failure(error)
                }
            case .Failure(let error):
                return .Failure(error)
            }
        }

        return response(responseSerializer: responseSerializer, completionHandler: completionHandler)
    }
}
final class User: ResponseObjectSerializable {
    let username: String
    let name: String

    init?(response: NSHTTPURLResponse, representation: AnyObject) {
        self.username = response.URL!.lastPathComponent!
        self.name = representation.valueForKeyPath("name") as! String
    }
}
Alamofire.request(.GET, "https://example.com/users/mattt")
         .responseObject { (response: Response<User, NSError>) in
             debugPrint(response)
         }

同样方法也可以用于处理返回对象集合表示的表达式

public protocol ResponseCollectionSerializable {
    static func collection(response response: NSHTTPURLResponse, representation: AnyObject) -> [Self]
}

extension Alamofire.Request {
    public func responseCollection<T: ResponseCollectionSerializable>(completionHandler: Response<[T], NSError> -> Void) -> Self {
        let responseSerializer = ResponseSerializer<[T], NSError> { request, response, data, error in
            guard error == nil else { return .Failure(error!) }

            let JSONSerializer = Request.JSONResponseSerializer(options: .AllowFragments)
            let result = JSONSerializer.serializeResponse(request, response, data, error)

            switch result {
            case .Success(let value):
                if let response = response {
                    return .Success(T.collection(response: response, representation: value))
                } else {
                    let failureReason = "Response collection could not be serialized due to nil response"
                    let error = Error.errorWithCode(.JSONSerializationFailed, failureReason: failureReason)
                    return .Failure(error)
                }
            case .Failure(let error):
                return .Failure(error)
            }
        }

        return response(responseSerializer: responseSerializer, completionHandler: completionHandler)
    }
}
final class User: ResponseObjectSerializable, ResponseCollectionSerializable {
    let username: String
    let name: String

    init?(response: NSHTTPURLResponse, representation: AnyObject) {
        self.username = response.URL!.lastPathComponent!
        self.name = representation.valueForKeyPath("name") as! String
    }

    static func collection(response response: NSHTTPURLResponse, representation: AnyObject) -> [User] {
        var users: [User] = []

        if let representation = representation as? [[String: AnyObject]] {
            for userRepresentation in representation {
                if let user = User(response: response, representation: userRepresentation) {
                    users.append(user)
                }
            }
        }

        return users
    }
}
Alamofire.request(.GET, "http://example.com/users")
         .responseCollection { (response: Response<[User], NSError>) in
             debugPrint(response)
         }

URLStringConvertible

采用 URLStringConvertible 协议的类型可用于构建 URL 字符串,这些字符串随后用于构建 URL 请求。默认情况下,NSStringNSURLNSURLComponentsNSURLRequest 等 符合 URLStringConvertible 协议,允许它们中的任何一个被作为 URLString 参数传递给 requestuploaddownload 方法。

let string = NSString(string: "https://httpbin.org/post")
Alamofire_Result.request(.POST, string)

let URL = NSURL(string: string)!
Alamofire_Result.request(.POST, URL)

let URLRequest = NSURLRequest(URL: URL)
Alamofire_Result.request(.POST, URLRequest) // overrides `HTTPMethod` of `URLRequest`

let URLComponents = NSURLComponents(URL: URL, resolvingAgainstBaseURL: true)
Alamofire_Result.request(.POST, URLComponents)

鼓励与网络应用有显著交互的应用,其自定义类型应遵循 URLStringConvertible,作为一种方便的方式将特定领域的模型映射到服务器资源。

类型安全路由

extension User: URLStringConvertible {
    static let baseURLString = "http://example.com"

    var URLString: String {
        return User.baseURLString + "/users/\(username)/"
    }
}
let user = User(username: "mattt")
Alamofire_Result.request(.GET, user) // http://example.com/users/mattt

URLRequestConvertible

遵循 URLRequestConvertible 协议的类型可以用来构建 URL 请求。NSURLRequest 默认遵循 URLRequestConvertible 协议,允许它直接传入 requestuploaddownload 方法(这是指定单个请求自定义 HTTP 体的推荐方式)

let URL = NSURL(string: "https://httpbin.org/post")!
let mutableURLRequest = NSMutableURLRequest(URL: URL)
mutableURLRequest.HTTPMethod = "POST"

let parameters = ["foo": "bar"]

do {
    mutableURLRequest.HTTPBody = try NSJSONSerialization.dataWithJSONObject(parameters, options: NSJSONWritingOptions())
} catch {
    // No-op
}

mutableURLRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")

Alamofire_Result.request(mutableURLRequest)

鼓励与网络应用有显著交互的应用,其自定义类型应遵循 URLRequestConvertible,以确保请求端点的统一性。这种做法可以用来自抽象服务器端的不一致性,提供类型安全的路由,并管理身份验证凭据和其他状态。

API 参数抽象

enum Router: URLRequestConvertible {
    static let baseURLString = "http://example.com"
    static let perPage = 50

    case Search(query: String, page: Int)

    // MARK: URLRequestConvertible

    var URLRequest: NSMutableURLRequest {
        let result: (path: String, parameters: [String: AnyObject]) = {
            switch self {
            case .Search(let query, let page) where page > 1:
                return ("/search", ["q": query, "offset": Router.perPage * page])
            case .Search(let query, _):
                return ("/search", ["q": query])
            }
        }()

        let URL = NSURL(string: Router.baseURLString)!
        let URLRequest = NSURLRequest(URL: URL.URLByAppendingPathComponent(result.path))
        let encoding = Alamofire.ParameterEncoding.URL

        return encoding.encode(URLRequest, parameters: result.parameters).0
    }
}
Alamofire_Result.request(Router.Search(query: "foo bar", page: 1)) // ?q=foo%20bar&offset=50

CRUD & 授权

enum Router: URLRequestConvertible {
    static let baseURLString = "http://example.com"
    static var OAuthToken: String?

    case CreateUser([String: AnyObject])
    case ReadUser(String)
    case UpdateUser(String, [String: AnyObject])
    case DestroyUser(String)

    var method: Alamofire.Method {
        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

    var URLRequest: NSMutableURLRequest {
        let URL = NSURL(string: Router.baseURLString)!
        let mutableURLRequest = NSMutableURLRequest(URL: URL.URLByAppendingPathComponent(path))
        mutableURLRequest.HTTPMethod = method.rawValue

        if let token = Router.OAuthToken {
            mutableURLRequest.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
        }

        switch self {
        case .CreateUser(let parameters):
            return Alamofire_Result.ParameterEncoding.JSON.encode(mutableURLRequest, parameters: parameters).0
        case .UpdateUser(_, let parameters):
            return Alamofire_Result.ParameterEncoding.URL.encode(mutableURLRequest, parameters: parameters).0
        default:
            return mutableURLRequest
        }
    }
}
Alamofire_Result.request(Router.ReadUser("mattt")) // GET /users/mattt

安全

在与服务器和 Web 服务通信时使用安全的 HTTPS 连接是保护敏感数据的重要步骤。默认情况下,Alamofire 将使用 Security 框架提供的 Apple 内置验证来评估服务器提供的证书链。虽然这保证了证书链是有效的,但它不能防止中间人(MITM)攻击或其他潜在漏洞。为了减轻 MITM 攻击,处理敏感客户数据或财务信息的应用程序应使用 ServerTrustPolicy 提供的证书或公钥固定。

ServerTrustPolicy

ServerTrustPolicy 枚举在通过安全 HTTPS 连接连接到服务器时,评估由 NSURLAuthenticationChallenge 提供的通用服务器信任。

let serverTrustPolicy = ServerTrustPolicy.PinCertificates(
    certificates: ServerTrustPolicy.certificatesInBundle(),
    validateCertificateChain: true,
    validateHost: true
)

有各种不同的服务器信任评估情况,让您完全控制验证过程。

  • PerformDefaultEvaluation:使用默认的服务器信任评估,同时允许您控制是否验证挑战提供的宿主。
  • PinCertificates:使用固定的证书来验证服务器信任。如果固定的证书之一与服务器证书之一匹配,则认为服务器信任有效。
  • PinPublicKeys:使用固定的公钥来验证服务器信任。如果固定的公钥之一与服务器证书的公钥之一匹配,则认为服务器信任有效。
  • DisableEvaluation:禁用所有评估,从而始终将任何服务器信任视为有效。
  • CustomEvaluation:使用关联的闭包来评估服务器信任的有效性,从而让您完全控制验证过程。请谨慎使用。

服务器信任策略管理器

ServerTrustPolicyManager 负责将服务器信任策略内部映射到特定主机。这使得 Alamofire 能够将每个主机与不同的服务器信任策略进行评估。

let serverTrustPolicies: [String: ServerTrustPolicy] = [
    "test.example.com": .PinCertificates(
        certificates: ServerTrustPolicy.certificatesInBundle(),
        validateCertificateChain: true,
        validateHost: true
    ),
    "insecure.expired-apis.com": .DisableEvaluation
]

let manager = Manager(
    serverTrustPolicyManager: ServerTrustPolicyManager(policies: serverTrustPolicies)
)

请确保保留对新 Manager 实例的引用,否则当您的 manager 被销毁时,所有请求都将被取消。

这些服务器信任策略将产生以下行为

  • test.example.com 总是使用证书固定,启用证书链和宿主验证,因此要求满足以下标准才能允许 TLS 握手成功
    • 证书链必须是有效的。
    • 证书链必须包含其中一个已固定的证书。
    • 挑战主机必须与证书链中最后一个证书中的主机匹配。

  • insecure.expired-apis.com永远不会评估证书链,并始终允许TLS握手成功。
  • 其他所有主机将使用苹果提供的默认评估。
子类化服务器信任策略管理器

如果您发现自己需要更灵活的服务器信任策略匹配行为(例如通配符域名),则可以子类化ServerTrustPolicyManager,并通过您自己的自定义实现覆盖serverTrustPolicyForHost方法。

class CustomServerTrustPolicyManager: ServerTrustPolicyManager {
    override func serverTrustPolicyForHost(host: String) -> ServerTrustPolicy? {
        var policy: ServerTrustPolicy?

        // Implement your custom domain matching behavior...

        return policy
    }
}

验证主机

服务器信任策略.PerformDefaultEvaluation.PinCertificates.PinPublicKeys都采用一个validateHost参数。将值设置为true会导致服务器信任评估验证证书中的主机名是否与挑战的主机名匹配。如果不匹配,评估将失败。将validateHost的值设置为false时,仍然会评估完整的证书链,但不会验证最后一个证书的主机名。

建议在生产环境中始终将validateHost设置为true

验证证书链

固定证书和公钥都提供使用validateCertificateChain参数验证证书链的选项。通过将此值设置为true,除了执行与固定证书或公钥的字节相等性检查外,还将评估完整的证书链。将值设置为false将跳过证书链验证,但仍然执行字节相等性检查。

在几种情况下,可能有必要禁用证书链验证。禁用验证的最常见用例是自签名证书和过期证书。在这两种情况下,评估总会失败,但字节相等性检查将仍然确保您从服务器获取的证书符合预期。

建议在生产环境中始终将validateCertificateChain设置为true

应用传输安全

在iOS 9中添加了“应用传输安全”(ATS)之后,使用具有多个ServerTrustPolicy对象的自定义ServerTrustPolicyManager可能不会产生任何效果。如果您持续看到“CFNetwork SSLHandshake failed (-9806)”错误,您可能遇到了这个问题。苹果的ATS系统会覆盖整个挑战系统,除非您在应用的plist中配置ATS设置以禁用足够的部分,以便允许您的应用评估服务器信任。

如果您遇到这个问题(自签名证书的可能性很高),您可以通过向您的Info.plist中添加以下内容来解决此问题。

<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/>
                <!-- Optional: Specify minimum TLS version -->
                <key>NSTemporaryExceptionMinimumTLSVersion</key>
                <string>TLSv1.2</string>
            </dict>
        </dict>
    </dict>
</dict>

是否需要将 NSExceptionRequiresForwardSecrecy 设置为 NO 取决于您的 TLS 连接是否使用了允许的密钥套件。在某些情况下,需要将其设置为 NO。为了允许 SessionDelegate 接收挑战回调和回调,NSExceptionAllowsInsecureHTTPLoads 必须设置为 YES。一旦开始调用挑战回调,ServerTrustPolicyManager 将接管服务器信任评估。如果您尝试连接到一个仅支持 TLS 版本小于 1.2 的主机,可能还需要指定 NSTemporaryExceptionMinimumTLSVersion

在生产环境中,建议始终使用有效的证书。


组件库

为了使 Alamofire 专注于核心网络实现,Alamofire 软件基金会创建了额外的组件库,以将更多功能带入到 Alamofire 生态系统。

  • AlamofireImage - 一组图像库,包括图像响应序列化器、UIImageUIImageView 扩展、自定义图像过滤器、自动清除的内存缓存和基于优先级的图像下载系统。

公开 Rdars

以下 Rdars 对当前 Alamofire 的实现有一定影响。

  • rdar://21349340 - 测试用例中因免费用桥接问题而发出的编译器警告

常见问题解答

Alamofire 这个名字的由来是什么?

Alamofire 以火羚羊花命名,这是一种蓝花(德克萨斯州的州花)的杂交变种。


致谢

Alamofire 由Alamofire 软件基金会拥有和维护。您可以在 Twitter 上关注他们@AlamofireSF 以获取项目更新和发布信息。

安全披露

如果您认为您已经识别出 Alamofire 中的安全漏洞,请尽快通过电子邮件报告,电子邮件地址为[email protected]。请勿将其发布到公开问题跟踪器。

许可证

Alamofire 是在 MIT 许可证下发布的。有关详细信息,请参阅 LICENSE 文件。