此框架专为 Trifork Identity Manager 而设计。
请在此处查看我们完整实现的示例 (SwiftUI)
https://github.com/trifork/TIM-Example-iOS
将此仓库添加到您的 SPM 📦
https://github.com/trifork/TIM-iOS
在使用 TIM
中的任何函数或属性之前,您必须通过调用 configure
方法来配置框架(通常您希望在应用程序启动时执行此操作)
import TIM
import AppAuth // Required for scopes
let config = TIMConfiguration(
timBaseUrl: URL(string: "<TIM base URL>")!,
realm: "<realm>",
clientId: "<clientId>",
redirectUri: URL(string:"<urlScheme>:/")!,
scopes: [ OIDScopeOpenID, OIDScopeProfile ],
encryptionMethod: .aesGcm
)
TIM.configure(configuration: config)
要使用依赖项的自定义实现来配置 TIM,请参阅自定义设置说明。
设置您的 URL scheme 或通用链接以接收登录重定向:Apple 文档
根据您的生命周期处理,您应该在以下回调之一中处理 URL 请求
.onOpenURL(perform:)
scene(_:, openURLContexts:)
application(_:, open:, options:) -> Bool
SceneDelegate
示例
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
for url: URL in URLContexts.map({ $0.url }) {
TIM.auth.handleRedirect(url: url)
}
}
如果您正在使用 TIM
的生物识别功能,请不要忘记在您的 Info.plist 中设置 NSFaceIDUsageDescription
Apple 文档 键。
以下示例使用 TIM
的 Combine
接口,它返回 Future
类。 如果您正在开发部署目标低于 iOS 13 的应用程序,则存在相同的接口,但使用完成闭包(尽管这些接口已从 iOS 13 中弃用)。
所有用户都必须通过 OpenID Connect 登录进行注册。 这是通过以下方式完成的
TIM.auth.performOpenIDConnectLogin(presentingViewController: topViewController)
.sink { (completion) in
switch completion {
case .failure(let error):
print("Failed to perform OpenID Connect login: \(error.localizedDescription)")
case .finished:
break
}
} receiveValue: { (accessToken) in
print("Successfully logged in, access and refresh token is now available. \nAT:\n\(accessToken)")
}
.store(in: &futureStorage)
为了避免用户每次需要有效会话时都进行 OpenID Connect 登录,您可以提供密码,这将允许您保存刷新令牌的加密版本,这样用户只需提供密码即可获得有效的访问令牌。
用户必须先成功执行 OpenID Connect 登录才能设置密码,因为刷新令牌必须可用。
guard let refreshToken = TIM.auth.refreshToken else {
return
}
// UserId can be retrieved from the refresh token: `refreshToken.userId`
TIM.storage.storeRefreshToken(refreshToken, withNewPassword: password)
.sink { (completion) in
switch completion {
case .failure(let error):
print("Failed to store refresh token: \(error.localizedDescription)")
case .finished:
break
}
} receiveValue: { (keyId) in
// TIM has saved the keyId for the userId of the refresh token - you don't need to do anything with the keyId at this point unless you are doing something custom work with TIMEncryptedStorage.
print("Saved refresh token for keyId: \(keyId)")
}
.store(in: &futureStore)
在用户创建密码后,您可以启用生物识别访问以进行登录。 您将需要用户的密码和来自刷新令牌的 userId 才能执行此操作。
userId
可以从刷新令牌中检索:TIM.auth.refreshToken?.userId
TIM.storage.enableBiometricAccessForRefreshToken(password: password, userId: userId)
.sink(
receiveCompletion: { (completion} in
switch result {
case .finished:
print("Successfully enabled biometric login for user.")
case .failure(let error):
print("Whoops, something went wrong: \(error.localizedDescription)")
}
},
receiveValue: { _ in }
)
.store(in: &futureStore)
您必须提供希望登录的用户的用户 ID(这允许多个用户在同一设备上登录)。
如果之前启用了生物识别,用户可以使用生物识别,否则您必须提供密码。 您可以设置 storeNewRefreshToken
来控制系统是否应在成功登录时更新刷新令牌。 强烈建议 存储新的刷新令牌,因为它会在用户每次登录时保持续订用户的会话。 虽然,如果您有不想更新它的情况,您可以将其设置为 false。
密码和生物识别可以使用相同的完成处理,如下例所示。
// Login with password
TIM.auth.loginWithPassword(userId: userId, password: password, storeNewRefreshToken: true)
.sink(
receiveCompletion: handleResultCompletion,
receiveValue: { _ in })
.store(in: &futureStore)
// Login with biometrics
TIM.auth.loginWithBiometricId(
userId: userId,
storeNewRefreshToken: true,
willBeginNetworkRequests: {
// BioID succeeded, show loading indicator -> TIM will initiate network requests right now.
})
.sink(
receiveCompletion: handleResultCompletion,
receiveValue: { _ in })
.store(in: &futureStore)
// Completion handling
func handleResultCompletion(_ completion: Subscribers.Completion<TIMError>) {
switch completion {
case .failure(let error):
print("Failed to login: \(error.localizedDescription)")
switch error {
case .storage(let storageError):
switch storageError {
case .incompleteUserDataSet:
// Reset user! We cannot recover from this state!
break
case .encryptedStorageFailed:
// Note that this is a simplified error handling, which uses the Bool extensions to avoid huge switch statements.
// If you want to handle errors the right way, you should look into all error cases and decide which you need specific
// error handling for. The ones you see here are the most common ones, which are very likely to happen.
if storageError.isWrongPassword() {
// Handle wrong password
} else if storageError.isKeyLocked() {
// Handle key locked (three wrong password logins)
} else if storageError.isBiometricFailedError() {
// Bio failed or was cancelled, do nothing.
} else if storageError.isKeyServiceError() {
// Something went wrong while communicating with the key service (possible network failure)
} else {
// Something failed - please try again.
}
}
case .auth(let authError):
if case TIMAuthError.refreshTokenExpired = authError {
// Refresh Token has expired.
}
}
case .finished:
print("Successfully logged in!")
}
}
令牌类型为 JWT
,它只是 String
的 typealias
。 该框架具有 JWT
的扩展,允许您直接从令牌获取以下数据
token.expireTimestamp
token.userId
该框架会跟踪已创建密码并存储加密刷新令牌的用户。
TIM.storage.availableUserIds
将返回可用刷新令牌(sub
字段)中的标识符列表。 与用户相关的任何其他数据以及 ID 与用户数据之间的映射都由您负责。 TIM
将仅跟踪令牌中的标识符。
在大多数情况下,您不必担心您的刷新令牌,因为 TIM
方法会为您处理此问题。 如果您处于需要它的情况下,可以从 storage
访问它
storage.getStoredRefreshToken(userId: userId, password: password)
.sink(
receiveCompletion: { _ in },
receiveValue: { (rt) in
//Valid refresh token!
})
.store(in: &futureStorage)
TIM
确保您的访问令牌始终有效并自动刷新。 这也是为什么 TIM.auth.accessToken()
是一个异步函数的原因。
大多数情况下,当令牌可用时,TIM
会立即完成调用,而在需要更新令牌时会稍慢一些。
您应该避免将访问令牌的值分配给属性,而是在需要时始终使用此函数,以确保令牌有效。
TIM.auth.accessToken()
.sink(
receiveCompletion: { _ in },
receiveValue: { [weak self] (at) in
//Valid access token!
})
.store(in: &futureStorage)
您可以注销用户,这将丢弃当前的访问令牌和刷新令牌,这样您将必须通过登录再次加载它。
TIM.auth.logout()
您可以删除为用户标识符存储的所有数据,这样刷新令牌将不再可用,并且该用户将不再存在于 availableUserIds
集中。 通常,在这种情况下您也希望注销
TIM.auth.logout() // Logout of current session
TIM.storage.clear(userId: theUserId) // Delete the stored user data
您可以配置 TIM 以监控应用程序在后台的时间,并在超过所需持续时间时使其自动注销。
1. The user logs in (background monitor timeout is set to 5 minutes)
2. The user sends the app to the background
3. The user opens the app after 6 minutes
4. TIM automatically calls logout, which invalidates the current session and invokes the timeout callback.
TIM.auth.enableBackgroundTimeout(durationSeconds: 60 * 5) {
// Handle the user log out event, e.g. present login screen.
}
由于不同的依赖项,TIM
可能会抛出大量错误。 所有错误的共同点是,它们都包装在 TIMError.auth()
或 TIMError.storage()
类型中,具体取决于抛出错误的区域。 这些错误将包含来自框架内部的其他错误,并且其中有几个级别。
大多数错误都在帮助您作为开发人员找出您可能配置错误的地方。 一旦一切都在设置中正确配置,则只有一小部分错误,这些错误需要作为特定错误处理
// Refresh token has expired
TIMError.auth(TIMAuthError.refreshTokenExpired)
// The user pressed cancel in the safari view controller during the OpenID Connect login
TIMError.auth(TIMAuthError.safariViewControllerCancelled)
TIMError.storage(
TIMStorageError.encryptedStorageFailed(
TIMEncryptedStorageError.keyServiceFailed(TIMKeyServiceError.badPassword)
)
)
TIMError.storage(
TIMStorageError.encryptedStorageFailed(
TIMEncryptedStorageError.keyServiceFailed(TIMKeyServiceError.keyLocked)
)
)
由于 TIMKeyServiceError
深入到错误结构中,因此在 TIMStorageError
类型上有一些简写
if storageError.isKeyLocked() {
// Handle key locked (happens on wrong password three times in a row)
}
if storageError.isWrongPassword() {
// Handle wrong password
}
if storageError.isKeyServiceError() {
// The communication with the KeyService failed. E.g. no internet connection.
}
if storageError.isBiometricFailedError() {
// Handle biometric failed/was cancelled scenario.
}
当然,应该仍然处理其他错误,但可以以更通用的方式处理它们,因为它们可能是由网络问题、服务器更新或其他不可预测的情况引起的。
您可以通过注入您自己定义的协议实现,使用自定义依赖项配置 TIM。 这适用于在 TIM 的定制版本上运行的特定项目。
免责声明: 这不 适用于与 TIM 托管产品一起使用。 如果您走这条路,最好知道自己在做什么 😅
以下是如何使用默认实现对 TIM 依赖项进行自定义配置的示例。 通过实现所需的协议,您可以将默认实现替换为您自己的实现。
let keyServiceConfig = TIMKeyServiceConfiguration(
realmBaseUrl: "<TIM base URL>/auth/realms/<realm>",
version: .v1
)
let openIdConfiguration = TIMOpenIDConfiguration(
issuer: URL(string: "<TIM base URL>/auth/realms/<realm>")!,
clientId: "<clientId>",
redirectUri: URL(string: "<urlScheme>:/")!,
scopes: [OIDScopeOpenID, OIDScopeProfile]
)
let encryptedStorage = TIMEncryptedStorage(
secureStorage: TIMKeychain(),
keyService: TIMKeyService(
configuration: keyServiceConfig),
encryptionMethod: .aesGcm
)
let dataStorage = TIMDataStorageDefault(encryptedStorage: encryptedStorage)
let auth = TIMAuthDefault(
dataStorage: dataStorage,
openIdController: AppAuthController(openIdConfiguration),
backgroundMonitor: TIMAppBackgroundMonitorDefault()
)
TIM.configure(
dataStorage: dataStorage,
auth: auth,
customLogger: TIMLogger()
)
上述自定义配置等效于此默认配置
let config = TIMConfiguration(
timBaseUrl: URL(string: "<TIM base URL>")!,
realm: "<realm>",
clientId: "<clientId>",
redirectUri: URL(string: "<urlScheme>:/")!,
scopes: [OIDScopeOpenID, OIDScopeProfile],
encryptionMethod: .aesGcm
)
TIM.configure(configuration: config)
TIM
依赖于 AppAuth
和 TIMEncryptedStorage
,并将其用法包装用于常见用例(请参阅以上部分),以便可以轻松管理注册、登录和加密存储。
TIM.storage: TIMDataStorage
处理所有存储操作,包括加密和原始数据到安全存储(默认为 iOS Keychain)。
这在很大程度上取决于 TIMEncryptedStorage
包,该包与 TIM KeyService 通信,以处理基于用户选择的密码和生物识别访问(如果启用)的加密。
TIM.auth: TIMAuth
通过 AppAuth
框架处理所有 OpenID Connect 操作。 其主要目的是处理访问令牌和刷新令牌以及两者的续订。 TIMAuth
依赖于 TIMDataStorage
来存储新的刷新令牌。
TIM
依赖于 TIMEncryptedStorage
进行加密数据存储以及通过 TouchID/FaceID 进行访问:https://github.com/trifork/TIMEncryptedStorage-iOS
TIM
依赖于 AppAuth
进行 OpenID Connect 操作:https://github.com/openid/AppAuth-iOS
TIM
被设计为可测试的,这样您就可以模拟您想要模拟的框架部分。 该框架包含一个自定义的 configure
方法,允许您完全自定义框架的内部实现
TIM.configure(dataStorage: TIMDataStorage, auth: TIMAuth, customLogger: TIMLoggerProtocol?)
TIM
中的每个依赖项都构建在协议之上,这样您就可以实现自己的模拟类进行测试。
configure
方法允许您更改 TIM
行为。 我们强烈建议您仅将上述 configure
方法用于测试!