Conduit 是一个基于会话的 Swift HTTP 网络和身份验证库。
在每个会话中,请求在被分发到网络队列之前,会通过一个串行管道发送。 在管道内,请求通过一系列中间件进行处理,这些中间件可以修饰请求、暂停会话管道以及清空传出队列。 基于这种模式,Conduit 为 OAuth2 授权许可捆绑了预定义的中间件,支持 RFC 6749 中定义的所有主要流程,并按照 RFC 6750 中的定义自动将令牌应用于请求。
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 |
将 Conduit
添加到你的 Package.swift
// swift-tools-version:5.0
import PackageDescription
let package = Package(
dependencies: [
.package(url: "https://github.com/mindbody/Conduit.git", from: "1.0.0")
]
)
将 Conduit
添加到你的 Podfile
source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '9.0'
use_frameworks!
target 'MyApplicationTarget' do
pod 'Conduit'
end
URLSessionClient
是 Conduit 的核心。 每个客户端都由一个 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()]
如果没有 URLRequest
发送到网络,URLSessionClient
将毫无用处。 为了在单个会话中扩展以适应许多不同的可能传输格式,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()]
服务器信任评估内置于 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 Auth 的许多地方,都需要 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: "user@example.com", 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
的强大功能
refresh_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. 所有,并由我们的贡献者持续维护。