跳转至

iOS SDK

本指南将介绍如何将 Authany 集成到 iOS 应用中。

Authany iOS SDK 支持 iOS 11.0 及以上版本。本教程使用 Apple 的 Observation 框架(@Observable)和 Swift Concurrency(async/await)构建示例应用,这需要 iOS 17.0 及以上版本、Xcode 16 及以上版本,以及 Swift 6 语言模式。如果您需要支持更旧的 iOS 版本,可以使用 ObservableObject 和 SDK 的 completion handler 采用相同的结构实现。

按照本指南,🕐 10 分钟即可将 Authany 添加到您的 iOS 应用。

说明

本教程示例应用的完整代码,请参见 这个 GitHub 仓库

在 Authany 中设置应用

在 https://manage.authany.com/ 注册 Authany 管理台账号。您也可以使用自建部署的 Authany。

在项目列表中,创建一个新项目或选择一个已有项目。接下来,我们需要在该项目中创建一个 Authany 客户端应用。

步骤 1:在管理台中创建应用

在左侧菜单栏中,进入应用。

在顶部工具栏中点击⊕添加应用。

输入应用名称,并将应用类型选择为原生应用,然后点击"保存"。

创建应用

您会看到一系列有助于完成设置的指南,然后点击"下一步"。

步骤 2:配置应用

这里您需要定义一个自定义 URI scheme,Authany 会在身份验证完成后用它将用户重定向回您的应用。以我们的示例应用为例,自定义 URL scheme 是 com.example.authgeardemo,在此基础上构建的完整重定向 URI 是 com.example.authgeardemo://host/path。关于在 iOS 中设置自定义 URI scheme 的更多说明,请参见官方文档。

回到 Authany 管理台,将 com.example.authgeardemo://host/path 添加为重定向 URI。

点击"保存"按钮,并记下新建客户端应用的客户端 ID(Client ID)和端点(Endpoint),稍后会在 iOS 应用中用到。您之后也可以在应用列表中重新获取客户端 ID。

填写授权的重定向 URI

将 Authany 添加到您的 iOS 应用

在这一步中,我们将使用 Authany iOS SDK 和前面创建的客户端应用,为一个简单的 iOS 应用添加用户身份验证。

前置条件

为顺利完成本指南中的步骤,您需要准备以下条件:

  • Xcode 16 或更高版本
  • 一个面向 iOS 17.0 或更高版本、使用 Swift 6 语言模式的项目
  • 对 SwiftUI 和 Swift Concurrency(async/await)有一定了解

步骤 1:创建新的 iOS 项目

为便于讲解本指南,我们将在 Xcode 中创建一个新项目。如果您是将 Authany 添加到现有应用,可以跳过此步骤。

要创建新项目,打开 Xcode,进入文件(File) > 新建(New) > 项目(Project)。使用以下信息创建新项目:

  • 项目名称(Project Name):my_demo_app
  • 将界面(Interface)选择为 SwiftUI,其他字段保持默认,然后继续创建项目。

Xcode 新建项目

步骤 2:安装 Authany SDK

Authany iOS SDK 让您可以轻松在 iOS 项目中与 Authany 服务交互。

要将 Authany SDK 添加到项目中,在 Xcode 中进入文件(File) > 添加软件包依赖(Add Package Dependencies),在 Package URL 文本框中输入 https://github.com/authgear/authgear-sdk-ios.git。依赖规则选择直到下一个主版本(Up to Next Major Version),起始版本为 2.0.0。

点击添加软件包(Add Package)继续。

Xcode 包管理器

在下一个界面中,在添加到目标(Add to Target)下选择您的应用,然后点击添加软件包(Add Package)。

Xcode 添加软件包

如果您的项目使用 CocoaPods,也可以用以下方式安装 SDK:

pod 'Authgear', :git => 'https://github.com/authgear/authgear-sdk-ios.git'

步骤 3:创建身份验证模型

我们会把所有身份验证逻辑集中放在一处:一个 @Observable 模型,持有一个 Authgear 实例,并暴露一个简洁、便于界面使用的状态。视图(下一步会创建)只负责渲染该状态并调用模型的方法。

新建一个名为 AuthenticationModel.swift 的 Swift 文件,添加以下内容:

//  AuthenticationModel.swift

import Authgear
import Observation

// Sendable snapshots produced *inside* the SDK's completion handlers so that
// no non-Sendable Authgear type crosses the `await` boundary.
private enum AuthOutcome: Sendable {
    case signedIn(userID: String)
    case cancelled
    case failed(message: String)
}

private enum VoidOutcome: Sendable {
    case ok
    case failed(message: String)
}

@MainActor
@Observable
final class AuthenticationModel {
    enum State: Equatable {
        case loading
        case signedOut
        case signedIn(userID: String)
    }

    private(set) var state: State = .loading

    // Non-nil drives an error alert in the view.
    var errorMessage: String?

    private let authgear: Authgear
    private var didConfigure = false

    init() {
        authgear = Authgear(
            clientId: "<CLIENT_ID>",
            endpoint: "<AUTHGEAR_ENDPOINT>"
        )
    }

    // Configure the SDK once and restore any existing session.
    func configure() async {
        guard !didConfigure else { return }
        didConfigure = true

        let outcome: VoidOutcome = await withCheckedContinuation { continuation in
            authgear.configure { result in
                switch result {
                case .success:
                    continuation.resume(returning: .ok)
                case let .failure(error):
                    continuation.resume(returning: .failed(message: error.localizedDescription))
                }
            }
        }

        switch outcome {
        case .ok:
            // Refresh the session if the user has an existing one.
            if authgear.sessionState == .authenticated {
                await refreshCurrentUser()
            } else {
                state = .signedOut
            }
        case let .failed(message):
            errorMessage = message
            state = .signedOut
        }
    }

    // Start the interactive login flow.
    func logIn() async {
        state = .loading

        let outcome: AuthOutcome = await withCheckedContinuation { continuation in
            authgear.authenticate(redirectURI: "com.example.authgeardemo://host/path") { result in
                switch result {
                case let .success(userInfo):
                    continuation.resume(returning: .signedIn(userID: userInfo.sub))
                case let .failure(error):
                    if let authgearError = error as? AuthgearError,
                       case .cancel = authgearError {
                        continuation.resume(returning: .cancelled)
                    } else {
                        continuation.resume(returning: .failed(message: error.localizedDescription))
                    }
                }
            }
        }

        apply(outcome)
    }

    // Log out and clear the local session.
    func logOut() async {
        state = .loading

        let outcome: VoidOutcome = await withCheckedContinuation { continuation in
            authgear.logout { result in
                switch result {
                case .success:
                    continuation.resume(returning: .ok)
                case let .failure(error):
                    continuation.resume(returning: .failed(message: error.localizedDescription))
                }
            }
        }

        switch outcome {
        case .ok:
            state = .signedOut
        case let .failed(message):
            errorMessage = message
            // The SDK's session state is authoritative after a failed logout.
            if authgear.sessionState == .authenticated {
                await refreshCurrentUser()
            } else {
                state = .signedOut
            }
        }
    }

    // Open the pre-built user settings page.
    func openUserSettings() {
        authgear.open(page: .settings)
    }

    // Refresh the signed-in user's info; downgrades to signed-out if the
    // refresh token is no longer valid.
    private func refreshCurrentUser() async {
        let outcome: AuthOutcome = await withCheckedContinuation { continuation in
            authgear.fetchUserInfo { result in
                switch result {
                case let .success(userInfo):
                    continuation.resume(returning: .signedIn(userID: userInfo.sub))
                case let .failure(error):
                    continuation.resume(returning: .failed(message: error.localizedDescription))
                }
            }
        }

        apply(outcome)
    }

    private func apply(_ outcome: AuthOutcome) {
        switch outcome {
        case let .signedIn(userID):
            state = .signedIn(userID: userID)
        case .cancelled:
            state = .signedOut
        case let .failed(message):
            errorMessage = message
            state = .signedOut
        }
    }
}

将"<CLIENT_ID>"和"<AUTHANY_ENDPOINT>"替换为您之前创建的客户端项目配置页面中的客户端 ID 和端点。

说明

为什么要用 AuthOutcome / VoidOutcome 枚举?Authany SDK 提供的是 completion handler 风格的 API,其结果类型(UserInfo、SessionState、AuthgearError)并不是 Sendable 的。为了在 Swift 6 语言模式下保持代码整洁,每次调用都用 withCheckedContinuation 包装,completion 闭包只捕获 continuation,并在闭包内部提取出一个较小的 Sendable 值来 resume。这样就不会有非 Sendable 的 SDK 类型跨越 await 边界,模型也就能在严格并发检查下编译通过,而无需额外的注解。

在示例应用仓库中,客户端 ID、端点和重定向 URI 被提取到一个较小的 Constants.swift 枚举中,而不是硬编码在初始化方法里;这是实际项目中值得采用的做法。

以下是各个方法的作用:

  • configure():只初始化一次 SDK(通过 didConfigure 加以保护),如果存在此前的会话,会刷新会话,让回访用户直接进入已登录界面。
  • logIn():执行交互式身份验证流程。用户主动取消与真正的错误会分开处理,因此取消登录弹窗不会触发错误提示。
  • logOut():结束当前会话。如果调用失败,会以 SDK 的 sessionState 为准,而不是直接认定用户已登出。
  • openUserSettings():打开 Authany 预置的用户设置页面,用户可以在其中查看和修改个人资料属性及安全设置。
  • refreshCurrentUser():调用 fetchUserInfo 来更新 sessionState 并获取用户的 sub(唯一用户 ID)。它还能检测到已被远程撤销的会话:此时 sessionState 会变为 .noSession,模型也会回到未登录状态。

步骤 4:构建 ContentView

现在将 ContentView.swift 的内容替换为一个观察该模型的、状态驱动的视图。它会在视图首次出现时通过 .task 触发 configure(),将异步操作包装在 Task {} 中,并通过 .alert 展示任何 errorMessage:

//  ContentView.swift

import SwiftUI

struct ContentView: View {
    @State private var model = AuthenticationModel()

    var body: some View {
        VStack(spacing: 16) {
            switch model.state {
            case .loading:
                ProgressView()
            case .signedOut:
                signedOutView
            case let .signedIn(userID):
                signedInView(userID: userID)
            }
        }
        .padding()
        .task {
            await model.configure()
        }
        .alert(
            "Something went wrong",
            isPresented: Binding(
                get: { model.errorMessage != nil },
                set: { isPresented in
                    if !isPresented { model.errorMessage = nil }
                }
            ),
            presenting: model.errorMessage
        ) { _ in
            Button("OK", role: .cancel) {}
        } message: { message in
            Text(message)
        }
    }

    private var signedOutView: some View {
        VStack(spacing: 16) {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundStyle(.tint)
            Text("My Demo App")
            Button("Login") {
                Task { await model.logIn() }
            }
        }
    }

    private func signedInView(userID: String) -> some View {
        VStack(spacing: 16) {
            Text("Welcome user \(userID)")
            Button("User Settings") {
                model.openUserSettings()
            }
            Button("Logout") {
                Task { await model.logOut() }
            }
        }
    }
}

#Preview {
    ContentView()
}

该视图没有直接调用 SDK,只读取 model.state 并调用模型的方法。未登录时显示登录(Login)按钮;操作进行中显示 ProgressView;登录成功后会问候用户,并提供用户设置(User Settings)和登出(Logout)选项。

检查点

现在运行您的应用,它应该会启动并停留在登录(Login)界面。点击登录会尝试打开身份验证页面,但暂时还不会返回您的应用。接下来我们会注册重定向 URI scheme。

步骤 5:为重定向 URI 注册 URI Scheme

为了让 Authany 在身份验证完成后能重定向回您的应用,需要注册自定义 URL 的scheme,也就是重定向 URI 中 :// 之前的部分。以我们的重定向 URI com.example.authgeardemo://host/path 为例,scheme 就是 com.example.authgeardemo。

在 Xcode 中打开项目的 Info.plist 或项目设置界面,添加以下内容:

进入目标(Targets) > {您的项目} > 信息(Info),展开 URL Types 部分。

添加一个新的 URL scheme,具体信息如下:

标识符(Identifier):CFBundleURLTypes

URL Schemes:com.example.authgeardemo

角色(Role):Editor

Xcode 项目属性

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
    <dict>
        <!-- Other entries -->
        <key>CFBundleURLTypes</key>
        <array>
            <dict>
                <key>CFBundleTypeRole</key>
                <string>Editor</string>
                <key>CFBundleURLName</key>
                <string>CFBundleURLTypes</string>
                <key>CFBundleURLSchemes</key>
                <array>
                    <string>com.example.authgeardemo</string>
                </array>
            </dict>
        </array>
    </dict>
</plist>

注意

请在 CFBundleURLSchemes 中注册裸 scheme com.example.authgeardemo,不要注册完整的重定向 URI com.example.authgeardemo://host/path。CFBundleURLSchemes 只需要 scheme 本身(即 :// 之前的部分)。完整的重定向 URI 是您在 Authany 管理台中注册、并传给 authenticate(redirectURI:) 的内容。

步骤 6:运行并测试

再次运行您的应用并尝试登录。由于重定向 scheme 已经注册,Authany 会在身份验证完成后重定向回您的应用,视图也会切换到已登录界面。

完整体验一下整个流程:

  • 登录(Login):完成身份验证,并显示 "Welcome user <sub>"。
  • 用户设置(User Settings):打开 Authany 预置的设置页面。
  • 登出(Logout):结束会话并返回登录界面。

如果配置失败(例如端点填写错误)或身份验证出错,您在步骤 4 中添加的 .alert 会显示错误信息,而不是静默失败。

理解会话状态

您可能需要判断用户是否已登录(例如只在用户未登录时显示登录按钮)。

sessionState 反映的是用户在 SDK 本地状态中的登录状态。即使 sessionState 为 .authenticated,如果会话已被远程撤销,也可能已经失效。因此,在初始化 SDK 之后,应在合适的时机调用 fetchUserInfo 来更新 sessionState;上面模型中的 configure() → refreshCurrentUser() 正是这样做的。

sessionState 的取值可以是 .unknown、.noSession 或 .authenticated。初始值为 .unknown。调用 authgear.configure 后,如果找到此前的会话,则变为 .authenticated;如果不存在这样的会话,则变为 .noSession。

在 HTTP 请求中使用访问令牌

每次使用访问令牌前都要调用 refreshAccessTokenIfNeeded;它只有在访问令牌已过期时才会发起网络请求进行检查。然后将访问令牌放入请求的 Authorization 请求头中。和其他 SDK 调用一样,您可以用 continuation 把 completion handler 桥接成 async/await:

func callProtectedAPI() async throws {
    try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
        authgear.refreshAccessTokenIfNeeded { result in
            continuation.resume(with: result)
        }
    }

    // The access token is ready to use. It can be empty if the user is not
    // logged in or the session is invalid.
    guard let accessToken = authgear.accessToken else {
        // The user is not logged in, or the token is expired.
        return
    }

    // Example only — use your own networking library.
    var urlRequest = URLRequest(url: URL(string: "YOUR_SERVER_URL")!)
    urlRequest.setValue("Bearer \(accessToken)", forHTTPHeaderField: "authorization")
    // ... continue making your request
}

后续步骤

为了保护您的应用服务器免受未经授权的访问,您需要将后端与 Authany 集成。

后端/API 集成

iOS SDK 参考

关于 iOS SDK 的详细文档,请访问 iOS SDK 参考。