Conduit
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
没有序列化或反序列化的概念;相反,我们使用 HTTPRequestBuilder
和 RequestSerializer
完全构造和序列化一个 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
XMLRequestSerializer
和 XMLResponseDeserializer
使用项目定义的 XML
和 XMLNode
。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
发送时,它首先通过可能包含中间件的管道串行处理。每个中间件组件都可能修改请求、取消请求或完全冻结管道。
这可以用于日志记录、代理、身份验证以及实施严格的网络行为。
/// 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 6749和RFC 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中列出的所有授权:password
、client_credentials
、authorization_code
、refresh_token
和自定义扩展授权。
在Conduit身份验证的许多地方,都需要一个OAuth2Authorization
。OAuth2Authorization
是一个简单的结构,用于将客户端授权与用户授权以及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
致谢
Conduit归MINDBODY,Inc.所有,并由我们的贡献者持续维护。