Conduit 0.19.0

Conduit 0.19.0

测试已测试
语言语言 SwiftSwift
许可证 Apache-2.0
发布最后发布2019年12月
SPM支持SPM

John Hammerlund维护。



Conduit 0.19.0

  • Conduit贡献者

Conduit

Release Build Status Carthage Compatible CocoaPods Compatible Platform

Conduit是基于会话的Swift HTTP网络和身份验证库。

每个会话中,请求通过串行管道发送到网络队列之前,请求都被发送。在管道中,请求通过一系列中间件进行处理,这些中间件可以装饰请求、暂停会话管道和清空输出队列。从这个模式中,Conduit打包了用于OAuth2授权预定义中间件,适用于在RFC 6749中定义的所有主要流程,并向请求自动应用在RFC 6750中定义的令牌。

特性

  • 基于会话的网络客户端
  • 可配置的出站请求中间件
  • 强大的HTTP请求构建和序列化
  • 支持JSON、XML、SOAP、URL编码和Multipart表单序列化以及响应反序列化
  • 复杂的查询参数序列化
  • 具有上传/下载进度闭包的可取消/暂停的会话任务
  • SSL锁定/服务器信任策略
  • 网络可达性
  • OAuth2客户端管理
  • 自动令牌刷新、客户端凭据授权和令牌存储
  • 使用AES-256 CBC加密的加密令牌存储
  • 对RFC 6749中所有令牌授权的完全手动控制
  • 自动应用载体/基本令牌
  • 嵌入授权页面/授权代码授权策略
  • 支持多个网络会话/OAuth2客户端
  • 从现有的网络层迁移的接口

需求

  • iOS 8.0+ / macOS 10.10+ / tvOS 9.0+ / watchOS 2.0+
  • Xcode 8.1+
Conduit 版本 Swift 版本
0.4.x 3.x
0.5 - 0.7.x 4.0
0.8 - 0.13.x 4.1
0.14.0 - 0.17.x 4.2
0.18.0+ 5.0

安装

Carthage

Conduit 添加到您的 Cartfile

github "mindbody/Conduit"

Cocoapods

Conduit 添加到您的 Podfile

source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '9.0'
use_frameworks!

target 'MyApplicationTarget' do
    pod 'Conduit'
end

Swift Package Manager

Conduit 添加到您的 Package.swift

// swift-tools-version:4.1
import PackageDescription

let package = Package(
    dependencies: [
        .package(url: "https://github.com/mindbody/Conduit.git", from: "0.19.0")
    ]
)

Core Networking

URLSessionClient

Conduit 的核心是 URLSessionClient。每个客户端背后都有一个 URLSession;因此,URLSessionClient 使用可选的 URLSessionConfiguration 和代理队列进行初始化。

// Creates a new URLSessionClient with no persistent cache storage and that fires events on a background queue
let mySessionClient = URLSessionClient(sessionConfiguration: URLSessionConfiguration.ephemeral, delegateQueue: OperationQueue())

URLSessionClient 是一个结构体,这意味着它使用 值语义。初始化一个 URLSessionClient 后,可以直接修改任何副本,而不会影响其他副本。然而,单个客户端的多个副本将使用相同的网络管道;它们仍然是单个会话的一部分。换句话说,一个 URLSessionClient 应该在每个网络会话中只能初始化一次。

class MySessionClientManager {

    /// Lazy-loaded URLSessionClient used for interacting with the Kittn API 🐱
    static let kittnAPISessionClient: URLSessionClient = {
        return URLSessionClient()
    }()

}

/// Example usage ///

var sessionClient = MySessionClientManager.kittnAPISessionClient

// As a copy, this won't mutate the original copy or any other copies
sessionClient.middleware = [MyCustomMiddleware()]

HTTP 请求/响应

URLSessionClient 没有与 URLRequest 相关联将数据发送到网络,那将一无所获。为了在一单个会话中应对许多不同的传输格式,URLSessionClient 没有序列化或反序列化的概念;相反,我们使用 HTTPRequestBuilderRequestSerializer 完全构造和序列化一个 URLRequest,然后使用 ResponseDeserializer 手动反序列化响应。

let requestBuilder = HTTPRequestBuilder(url: kittensRequestURL)
requestBuilder.method = .GET
// Can be serialzed via url-encoding, XML, or multipart/form-data
requestBuilder.serializer = JSONRequestSerializer()
// Powerful query string formatting options allow for complex query parameters
requestBuilder.queryStringParameters = [
    "options" : [
        "include" : [
            "fuzzy",
            "fluffy",
            "not mean"
        ],
        "2+2" : 4
    ]
]
requestBuilder.queryStringFormattingOptions.dictionaryFormat = .dotNotated
requestBuilder.queryStringFormattingOptions.arrayFormat = .commaSeparated
requestBuilder.queryStringFormattingOptions.spaceEncodingRule = .replacedWithPlus
requestBuilder.queryStringFormattingOptions.plusSymbolEncodingRule = .replacedWithDecodedPlus

let request = try requestBuilder.build()

let sessionClient = MySessionClientManager.kittnAPISessionClient
sessionClient.begin(request) { (data, response, error) in
    let deserializer = JSONResponseDeserializer()
    let responseDict = try? deserializer.deserialize(response: response, data: data) as? [String : Any]
    ...
}

MultipartFormRequestSerializer 使用预定义的 MIME 类型极大地简化了 multipart/form-data 请求的构建。

let serializer = MultipartFormRequestSerializer()
let kittenImage = UIImage(named: "jingles")
let kittenImageFormPart = FormPart(name: "kitten", filename: "mr-jingles.jpg", content: .image(kittenImage, .jpeg(compressionQuality: 0.8)))
let pdfFormPart = FormPart(name: "pedigree", filename: "pedigree.pdf", content: .pdf(pedigreePDFData))
let videoFormPart = FormPart(name: "cat-video", filename: "cats.mov", content: .video(catVideoData, .mov))

serializer.append(formPart: kittenImageFormPart)
serializer.append(formPart: pdfFormPart)
serializer.append(formPart: videoFormPart)

requestBuilder.serializer = serializer

XMLRequestSerializerXMLResponseDeserializer 使用项目定义的 XMLXMLNode。XML 数据会被自动解析成一个可索引和可遍历的树。

let requestBodyXMLString = "<?xml version=\"1.0\" encoding=\"utf-8\"?><Request>give me cats</Request>"

requestBuilder.requestSerializer = XMLRequestSerializer()
requestBuilder.method = .POST
requestBuilder.bodyParameters = XML(xmlString: requestBodyXMLString)

中间件

当请求通过 URLSessionClient 发送时,它首先通过可能包含中间件的管道串行处理。每个中间件组件都可能修改请求、取消请求或完全冻结管道。

Network Pipeline Architecture

这可以用于日志记录、代理、身份验证以及实施严格的网络行为。

/// Simple middelware example that logs each outbound request
struct LoggingRequestPipelineMiddleware: RequestPipelineMiddleware {

    public func prepareForTransport(request: URLRequest, completion: @escaping Result<Void>.Block) {
        print("Outbound request: \(request)")
    }

}

mySessionClient.middleware = [LoggingRequestPipelineMiddleware()]

SSL 锚定

ServerTrustEvaluation 直接集成在 URLSessionClient 中。一个 ServerAuthenticationPolicy 评估会话认证挑战。最常见的服务器认证请求是 TLS/SSL 连接的开始,可以使用 SSLPinningServerAuthenticationPolicy 进行验证。

由于单个会话客户端可能与断开的第三方主机交互,初始化器需要一个谓词来决定是否应该对信任链进行锚定。

let sslPinningPolicy = SSLPinningServerAuthenticationPolicy(certificates: CertificateBundle.certificatesInBundle) { challenge in
    // All challenges from other hosts will be ignored and will proceed through normal system evaluation
    return challenge.protectionSpace.host == "api.example.com"
}

mySessionClient.serverAuthenticationPolicies = [sslPinningPolicy]

身份验证

Conduit实现了RFC 6749RFC 6750中规定的所有主要的OAuth2流程和复杂性。这使得Conduit成为OAuth2基于API SDK的理想基础解决方案。

配置

每个身份验证会话需要一个客户端配置,这反过来又需要一个OAuth2服务器环境。

guard let tokenGrantURL = URL(string: "https://api.example.com/oauth2/issue/token") else {
    return
}

let scope = "cats dogs giraffes"
let serverEnvironment = OAuth2ServerEnvironment(scope: scope, tokenGrantURL: tokenGrantURL)

let clientID = "my_oauth2_client"
let clientSecret = "shhhh"

let clientConfiguration = OAuth2ClientConfiguration(clientIdentifier: clientID, clientSecret: clientSecret, environment: serverEnvironment)

// Only for convenience for single-client applications; can be managed elsewhere
Auth.defaultClientConfiguration = clientConfiguration

令牌存储

Oauth2令牌存储允许在令牌授权流程中自动检索/更新。

// Stores user and client tokens to the keychain
let keychainStore = OAuth2KeychainStore(serviceName: "com.company.app-name.oauth-token", accessGroup: "com.company.shared-access-group")

// Stores user and client tokens to UserDefaults or a defined storage location
let diskStore = OAuth2TokenDiskStore(storageMethod: .userDefaults)

// Stores user and client tokens to memory; useful for tests/debugging
let memoryStore = OAuth2TokenMemoryStore()

// Only for convenience for single-client applications; can be managed elsewhere
Auth.defaultTokenStore = keychainStore

令牌授权

OAuth2令牌授权通过策略进行处理。Conduit支持RFC 6749中列出的所有授权:passwordclient_credentialsauthorization_coderefresh_token和自定义扩展授权。

在Conduit身份验证的许多地方,都需要一个OAuth2AuthorizationOAuth2Authorization是一个简单的结构,用于将客户端授权与用户授权以及Bearer凭据与Basic凭据分开。虽然某些OAuth2服务器可能不会真正将这些视为不同角色或身份,但它允许对用户敏感数据与客户端敏感数据进行清晰的管理。

当手动创建和使用OAuth2TokenGrantStrategy(通常用于资源拥有者流程)时,也必须手动存储令牌。

// This token grant is most-likely issued on behalf of a user, so the authorization level is "user", and the authorization type is "bearer"
let tokenGrantStrategy = OAuth2PasswordTokenGrantStrategy(username: "[email protected]", password: "hunter2", clientConfiguration: Auth.defaultClientConfiguration)
tokenGrantStrategy.issueToken { result in
    guard case .value(let token) = result else {
        // Handle failure
        return
    }
    let userBearerAuthorization = OAuth2Authorization(type: .bearer, level: .user)
    Auth.defaultTokenStore.store(token: token, for: Auth.defaultClientConfiguration, with: userBearerAuthorization)
    // Handle success
}
// This token grant is issued on behalf of a client, so the authorization level is "client"
let tokenGrantStrategy = OAuth2ClientCredentialsTokenGrantStrategy(clientConfiguration: Auth.defaultClientConfiguration)
tokenGrantStrategy.issueToken { result in
    guard case .value(let token) = result else {
        // Handle failure
        return
    }
    let clientBearerAuthorization = OAuth2Authorization(type: .bearer, level: .client)
    Auth.defaultTokenStore.store(token: token, for: Auth.defaultClientConfiguration, with: clientBearerAuthorization)
    // Handle success
}

对于授权代码流程,存在OAuth2AuthorizationStrategy。目前,实现仅限于iOS Safari。

// AppDelegate.swift
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    OAuth2AuthorizationRedirectHandler.default.authorizationURLScheme = "x-my-custom-scheme"
    // Other setup
    return true
}

func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool {
    if OAuth2AuthorizationRedirectHandler.default.handleOpen(url) {
        return true
    }
    ...
}
// SampleAuthManager.swift

guard let authorizationBaseURL = URL(string: "https://api.example.com/oauth2/authorize"),
    let redirectURI = URL(string: "x-my-custom-scheme://authorize") else {
    return
}
let authorizationStrategy = OAuth2SafariAuthorizationStrategy(presentingViewController: visibleViewController, authorizationRequestEndpoint: authorizationBaseURL)

var authorizationRequest = OAuth2AuthorizationRequest(clientIdentifier: "my_oauth2_client")
authorizationRequest.redirectURI = redirectURI
authorizationRequest.scope = "cats dogs giraffes"
authorizationRequest.state = "abc123"
authorizationRequest.additionalParameters = [
    "custom_param_1" : "value"
]

authorizationStrategy.authorize(request: authorizationRequest) { result in
    guard case .value(let response) = result else {
        // Handle failure
        return
    }
    if response.state != authorizationRequest.state {
        // We've been attacked! 👽
        return
    }
    let tokenGrantStrategy = OAuth2AuthorizationCodeTokenGrantStrategy(code: response.code, redirectURI: redirectURI, clientConfiguration: Auth.defaultClientConfiguration)

    tokenGrantStrategy.issueToken { result in
        // Store token
        // Handle success/failure
    }
}

身份验证中间件

将这些功能结合起来,Conduit提供了中间件来处理与OAuth2客户端相关的所有繁琐工作。以下是OAuth2RequestPipelineMiddleware强大功能的简要总结。

  • 如果存在且有效,将自动应用于给定的OAuth2客户端的存储的Bearer token
  • 暂停/清空出站网络队列,如果存在刷新令牌,则尝试为过期token获取refresh_token
  • 如果token过期或不存在,则尝试为客户端承载授权获取client_credentials
  • 自动应用于客户端基本授权的基本认证

当完全利用时,Conduit使服务操作从所需参数/编码到所需的确切授权类型和级别,都非常易于阅读和理解

let requestBuilder = HTTPRequestBuilder(url: protectedKittensRequestURL)
requestBuilder.method = .GET
requestBuilder.serializer = JSONRequestSerializer()
requestBuilder.queryStringParameters = [
    "options" : [
        "include" : [
            "fuzzy",
            "fluffy",
            "not mean"
        ],
        "2+2" : 4
    ]
]
requestBuilder.queryStringFormattingOptions.dictionaryFormat = .dotNotated
requestBuilder.queryStringFormattingOptions.arrayFormat = .commaSeparated
requestBuilder.queryStringFormattingOptions.spaceEncodingRule = .replacedWithPlus
requestBuilder.queryStringFormattingOptions.plusSymbolEncodingRule = .replacedWithDecodedPlus

let request = try requestBuilder.build()

let bearerUserAuthorization = OAuth2Authorization(type: .bearer, level: .user)
let authMiddleware = OAuth2RequestPipelineMiddleware(clientConfiguration: Auth.defaultClientConfiguration, authorization: userBearerAuthorization, tokenStorage: Auth.defaultTokenStore)

var sessionClient = MySessionClientManager.kittnAPISessionClient
// Again, this is a copy, so we're free to mutate the middleware within the copy
sessionClient.middleware.append(authMiddleware)

sessionClient.begin(request) { (data, response, error) in
    let deserializer = JSONResponseDeserializer()
    let responseDict = try? deserializer.deserialize(response: response, data: data) as? [String : Any]
    ...
}

示例

本仓库包含一个iOS示例,该示例附加到Conduit.xcworkspace

许可协议

Apache 2.0 许可下发布。有关详细信息,请参阅LICENSE

致谢

mindbody-logo

Conduit归MINDBODY,Inc.所有,并由我们的贡献者持续维护。