移动应用
本指南介绍将 k-ID 年龄验证集成到移动应用程序中的最佳实践。在 Web 上,k-ID 界面通常嵌入在 iframe 中,但移动应用需要不同的方法来有效显示验证 URL。
概述
在移动端,受支持的 Web 嵌入是托管 URL,它们呈现一个感知司法管辖区的验证界面(AgeKeys、面部年龄估计、ID 验证和其他方法)。支持两种类型:
- 年龄验证 URL:由
/age-verification/perform-access-age-verification端点返回的url(AgeKit+ 独立验证) - 年龄保证挑战 URL:由
/age-gate/check返回的CHALLENGE_AGE_GATE_AGE_ASSURANCE挑战的challenge.url(当启用自动年龄保证时),或由/session/upgrade返回的CHALLENGE_SESSION_UPGRADE_BY_AGE_ASSURANCE挑战的challenge.url
本指南中的示例使用 /age-verification/perform-access-age-verification 端点,但相同的显示方法和结果处理适用于上述任何一种 URL。
在移动应用程序中嵌入这些 URL 之一时,您有多种显示选项,每种选项都有不同的功能和权衡。主要考虑因素包括:
- AgeKeys 支持:用户是否可以为未来的验证创建和使用 AgeKeys(基于 FIDO 的通行密钥)
- 结果通信:验证结果如何传递回您的应用
- 用户体验:集成级别和原生感
- 设备方向:验证在竖屏方向下效果最佳。应用内浏览器界面会继承您应用的方向,因此如果您的应用锁定为横屏,请改用默认外部浏览器,让用户可以旋转到竖屏。请参阅设备方向
移动端实现方法
Android 选项
Android 提供了三种显示验证 URL 的主要方法:
-
Custom Tabs ⭐ 推荐 - 在保持应用品牌的自定义 Chrome 浏览器标签页中打开验证 URL。这提供了完整的浏览器功能,同时将用户保持在应用的上下文中。Custom Tabs 与 Chrome 共享 Cookie 和身份验证状态,实现无缝体验。
-
WebView - 使用 Android 的原生 WebView 组件在应用内直接嵌入 Web 内容。虽然实现简单,但 WebView 对现代 Web 标准的支持有限,无法访问某些浏览器功能。
WebView 不支持 AgeKeys 所需的 WebAuthn。使用 WebView 嵌入验证 URL 时,用户将无法创建或使用 AgeKeys。要获得具有完整 AgeKeys 支持的最佳用户体验,请改用 Custom Tabs。
- Trusted Web Activity (TWA) - 以全屏模式显示 Web 内容,主要为 Progressive Web Apps 设计。TWA 需要在应用和 k-ID 域之间建立数字资产链接。当未检测到数字资产链接时,TWA 会自动回退到 Custom Tabs。
Trusted Web Activities 不支持嵌入验证 URL。需要在 k-ID 的域上配置数字资产链接,但这是不可用的。由于在这种情况下 TWA 会回退到 Custom Tabs,建议直接使用 Custom Tabs 以获得更简单的实现。
iOS 选项
iOS 提供了三种显示验证 URL 的主要方法:
-
ASWebAuthenticationSession ⭐ 推荐 - 专为安全身份验证流程设计,此方法在系统管理的浏览器视图中显示 Web 内容。它与 Safari 共享 Cookie 并提供对现代 Web 功能(如 WebAuthn)的访问,使其成为验证流程的理想选择。
-
SFSafariViewController - 在类似 Safari 的界面中显示 Web 内容,与 Safari 共享 Cookie 和身份验证状态。这提供了熟悉的浏览体验,同时保持应用上下文。
-
WKWebView - Apple 的现代 Web 视图组件,可在应用内嵌入 Web 内容。与 Android 的 WebView 类似,WKWebView 对某些 Web 标准有限制,无法访问所有浏览器功能。
WKWebView 不支持 AgeKeys 所需的 WebAuthn。使用 WKWebView 嵌入验证 URL 时,用户将无法创建或使用 AgeKeys。要获得具有完整 AgeKeys 支持的最佳用户体验,请改用 ASWebAuthenticationSession。
默认浏览器(Android 和 iOS)
默认外部浏览器在 Android 和 iOS 上均可使用。您不是在应用内浏览器界面中呈现验证 URL,而是将用户的默认浏览器作为独立应用启动(Android 上使用 Intent.ACTION_VIEW,iOS 上使用 UIApplication.open)。
- 完全支持 AgeKeys - 浏览器提供 WebAuthn,因此用户可以创建和使用 AgeKeys。
- 独立的方向 - 由于浏览器是独立应用,它管理自己的方向。这使其成为锁定横屏方向应用的推荐方法,因为用户可以旋转到竖屏以获得最佳验证体验。参见设备方向。
- 需要回调 URL - 用户离开您的应用,因此结果通过
redirectUrl回调传递,该回调还会在流程完成时将焦点返回到您的应用。DOM 消息不可用。
AgeKeys 支持限制
AgeKeys 是基于 FIDO 和 WebAuthn 标准的可重用匿名年龄证明凭据。它们允许用户验证一次年龄,并在不同服务之间重复使用该验证,而无需透露个人信息。
AgeKeys 需要 WebAuthn 支持,这在 Android WebView 或 iOS WKWebView 中不可用。如果您使用这些组件嵌入验证 URL,用户在验证期间将看不到 AgeKeys 作为选项,并且在验证成功后无法创建 AgeKeys。
要为您的用户启用 AgeKeys,必须使用以下方法之一:
- Android: Custom Tabs
- iOS: ASWebAuthenticationSession 或 SFSafariViewController
设备方向
年龄验证在竖屏方向下效果最佳。面部年龄估计和 ID 文档拍摄等方法在设备竖直时更容易完成,并且验证界面是为竖屏布局的。
应用内浏览器界面(Custom Tabs、ASWebAuthenticationSession、SFSafariViewController 和 WebView/WKWebView)会继承您应用的方向约束。如果您的游戏或应用锁定为横屏,验证界面也会被强制为横屏,这会降低体验质量。
如果您的应用锁定了横屏方向,请在设备的默认外部浏览器中打开验证 URL,而不是应用内浏览器界面。外部浏览器作为独立应用运行并管理自己的方向,因此用户可以将设备旋转到竖屏以获得最佳体验。
在生成验证 URL 时设置 redirectUrl 回调。当用户完成验证流程时,浏览器会重定向到您的深度链接,从而将焦点返回到您的应用或游戏。
外部浏览器完全支持 AgeKeys(WebAuthn 可用)。与 Custom Tabs 和 ASWebAuthenticationSession 一样,DOM 消息不可用,因此您必须使用回调 URL 来接收结果。
按如下方式在默认浏览器中打开验证 URL:
- iOS (Swift)
- Android (Kotlin)
import UIKit
func displayVerificationInBrowser(verificationUrl: URL) {
// 打开系统默认浏览器(一个独立应用),无论您应用的锁定方向如何,
// 它都会管理自己的方向。
UIApplication.shared.open(verificationUrl)
}
import android.app.Activity
import android.content.Intent
import android.net.Uri
fun displayVerificationInBrowser(activity: Activity, verificationUrl: String?) {
if (verificationUrl == null) {
// 处理错误:生成验证 URL 失败
return
}
// ACTION_VIEW 将用户的默认浏览器作为独立应用启动,
// 无论您应用的锁定如何,它都会管理自己的方向。
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(verificationUrl))
activity.startActivity(intent)
}
按照步骤 4 中所示的方式处理返回的深度链接。
接收验证结果
移动应用需要在用户完成验证流程后接收结果。有两种方法,每种方法都有不同的可用性:
有关分析验证结果的详细信息,包括字段存在规则、状态类型和实现指南,请参阅验证事件契约。
回调 URL(通用方法)
所有实现方法的推荐方法是使用回调 URL。当您调用 API 生成验证 URL 时,包含 redirectUrl 参数。验证流程完成后,它会将结果作为查询参数重定向到此 URL。
回调 URL 的优势:
- 适用于所有实现方法
- 比 DOM 消息更可靠
- 移动应用的标准深度链接模式
- 即使应用移至后台,结果也会始终传递
回调 URL 的工作原理
- 在应用中注册深度链接处理程序(例如,
myapp://verification-complete) - 调用 API 时将深度链接作为
redirectUrl包含 - 验证页面完成后重定向到您的深度链接
- 应用处理深度链接并提取结果
仅当验证 URL 在浏览器或 Web 视图中直接打开时才会发生重定向,而不是在 iframe 中嵌入时。
回调 URL 参数
当验证页面重定向到您的回调 URL 时,它包含与流程相关的查询参数。例如:
- 年龄验证包括
verificationId和result - 当 URL 来自会话升级时,它还可以包含
sessionId和状态信息
回调 URL 示例:
myapp://verification-complete?verificationId=7854909b-9124-4bed-9282-24b44c4a3c97&result=PASS
实现回调 URL
调用年龄验证 API 时,在请求中包含 redirectUrl。URL 可以是:
- HTTPS URL:
https://example.com/verification-complete - 自定义深度链接:
myapp://verification-complete
DOM 消息(仅限 WebView/WKWebView)
使用 Android WebView 或 iOS WKWebView 时,可以监听从验证页面发送的 JavaScript 消息。这允许您:
- 实时接收验证结果
- 控制 Web 视图何时关闭
- 根据验证事件更新应用的 UI
年龄验证界面发出 Verification.Result 事件,其中包含 status(例如 PASS 或 FAIL),并在成功时包含解析出的 ageCategory。
DOM 消息作为 postMessage 事件发送,您可以在本机代码中拦截。有关可用事件的详细信息,请参阅 DOM 事件概述。
DOM 消息仅适用于 WebView 和 WKWebView,它们不支持 AgeKeys。它们不适用于 Custom Tabs、Trusted Web Activity、ASWebAuthenticationSession 或 SFSafariViewController。由于这些组件无法创建 AgeKeys,请改用回调 URL 加推荐的显示方法。
对于 iOS WKWebView,您可以通过注册名为 kid 的消息处理程序在本机接收 k-ID 事件。验证页面会自动检测此处理程序并直接向其发送事件。但是,Android WebView 没有接收 postMessage 事件的本机机制。您必须注入 JavaScript 来监听消息,并通过 JavaScript 接口将其转发到您的本机代码。
第三方应用验证流程
某些验证方法(如 ConnectID)需要在验证过程中将用户重定向到第三方移动应用。例如,ConnectID 会打开用户的银行应用来完成身份验证。
第三方应用流程的工作原理
使用涉及第三方应用的验证方法时,流程会经过多个应用程序,然后返回到您的应用:
逐步说明:
- 您的应用向服务器请求验证 URL
- 您的服务器使用 API 密钥调用 k-ID API,包含应用的
redirectUrl - k-ID 将包含验证界面的 URL 返回给您的服务器
- 您的服务器将 URL 返回给应用
- 您的应用在 Web 组件(ASWebAuthenticationSession 或 Custom Tabs)中打开 URL
- 当用户选择 ConnectID 等验证方法时,k-ID UI 深度链接到第三方验证应用(如银行应用)
- 验证完成后,第三方应用重定向到设备原生浏览器中的 k-ID 结果页面
- k-ID 检索您存储的
redirectUrl并将用户重定向回您的应用,同时携带验证结果
第三方应用流程的关键注意事项
Web 组件要求:这些验证方法必须在系统浏览器上下文(ASWebAuthenticationSession、Custom Tabs 或 SFSafariViewController)中打开,而不是在嵌入式 WebView 中。第三方应用重定向流程需要完整的浏览器上下文才能正常工作。
原生浏览器切换:第三方应用完成验证后,它会重定向到在设备原生浏览器中打开的 k-ID URL,而不是直接返回到原来的 Web 组件。这是移动设备上应用间重定向工作方式的平台限制。
回调 URL 至关重要:由于验证流程经过多个应用和浏览器,redirectUrl 参数对于在完成后将用户返回到您的应用至关重要。在启动可能使用第三方应用方法的验证时,请始终包含 redirectUrl。
测试第三方应用流程
以下验证方法使用第三方应用重定向:
- ConnectID: 包括用于在移动应用程序中验证重定向流程的测试应用。
方法比较
| 方法 | 平台 | AgeKeys | DOM 消息 | 回调 URL | 最适合 |
|---|---|---|---|---|---|
| WebView | Android | ❌ | ✅ | ✅ | 不推荐(无 AgeKeys 支持) |
| Custom Tabs | Android | ✅ | ❌ | ✅ | 大多数用例(推荐) |
| Trusted Web Activity | Android | ✅ | ❌ | ✅ | 不推荐(需要数字资产链接) |
| WKWebView | iOS | ❌ | ✅ | ✅ | 不推荐(无 AgeKeys 支持) |
| ASWebAuthenticationSession | iOS | ✅ | ❌ | ✅ | 大多数用例(推荐) |
| SFSafariViewController | iOS | ✅ | ❌ | ✅ | 类似 Safari 的体验 |
| 默认外部浏览器 | Android 和 iOS | ✅ | ❌ | ✅ | 锁定横屏的应用(允许旋转到竖屏) |
推荐实现
Android: Custom Tabs
使用 Custom Tabs 和回调 URL,以获得功能与用户体验的最佳平衡。
选择 Custom Tabs 的原因:
- 通过 WebAuthn 完全支持 AgeKeys
- 访问所有现代 Web 功能
- 应用品牌化的无缝用户体验
- 可靠的回调机制
- 与 Chrome 共享身份验证状态
实现步骤:
- 为回调 URL 注册深度链接处理程序
- 在 API 请求中包含
redirectUrl - 使用 Custom Tabs 打开验证 URL
- 使用验证结果处理深度链接回调
iOS: ASWebAuthenticationSession
使用 ASWebAuthenticationSession 和回调 URL,实现安全、原生感的合规流程。
选择 ASWebAuthenticationSession 的原因:
- 通过 WebAuthn 完全支持 AgeKeys
- 访问所有现代 Web 功能
- 系统管理的安全 UI
- 与 Safari 共享 Cookie
- 可靠的回调机制
实现步骤:
- 为回调 URL 注册 URL 方案处理程序
- 在 API 请求中包含
redirectUrl - 使用 ASWebAuthenticationSession 显示验证 URL
- 使用验证结果处理 URL 方案回调
完整实现示例
以下是实现推荐方法的逐步示例,包含完整的代码示例:
步骤 1:注册深度链接处理程序
- iOS (Swift)
- Android (Kotlin)
在 Info.plist 中注册 URL 方案:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>myapp</string>
</array>
</dict>
</array>
在 AndroidManifest.xml 中添加意图过滤器:
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="myapp" android:host="verification-complete" />
</intent-filter>
</activity>
步骤 2:从您的服务器生成验证 URL
验证 URL 必须从您的服务器生成,而不是直接从移动应用生成。这可以保护您的 API 密钥不被暴露在客户端代码中。您的移动应用应调用您自己的服务器 API,然后由该 API 进行到 k-ID 的服务器到服务器调用。
架构概述
服务器实现
您的服务器使用 API 密钥调用 /age-verification/perform-access-age-verification 端点,并包含 redirectUrl 深度链接:
POST https://game-api.k-id.com/api/v1/age-verification/perform-access-age-verification
Content-Type: application/json
Authorization: Bearer YOUR_API_KEY
{
"jurisdiction": "US-CA",
"criteria": {
"ageCategory": "DIGITAL_YOUTH_OR_ADULT"
},
"options": {
"redirectUrl": "myapp://verification-complete"
}
}
测试时,请使用测试环境端点:https://game-api.test.k-id.com/api/v1/age-verification/perform-access-age-verification
响应:
{
"id": "7854909b-9124-4bed-9282-24b44c4a3c97",
"url": "https://family.k-id.com/verify?token=eyJhbGciOiJFUzM4NCIs...",
"shortUrl": "https://family.k-id.com/v/7854909b-9124-4bed-9282-24b44c4a3c97?pid=42&s=qr"
}
相同的显示和结果处理适用于年龄保证挑战 URL:来自 /age-gate/check 的 CHALLENGE_AGE_GATE_AGE_ASSURANCE 挑战的 challenge.url(当为产品启用自动年龄保证时),以及来自 /session/upgrade 的 CHALLENGE_SESSION_UPGRADE_BY_AGE_ASSURANCE 挑战的 challenge.url。只有生成该 URL 的端点不同。
移动客户端实现
您的移动应用调用您的服务器以获取验证 URL:
- iOS (Swift)
- Android (Kotlin)
import Foundation
func fetchVerificationUrl(completion: @escaping (URL?) -> Void) {
// 调用您自己的服务器端点,而不是直接调用 k-ID
guard let url = URL(string: "https://your-server.com/api/generate-verification-url") else {
completion(nil)
return
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
// 添加您自己的身份验证(会话令牌等)
request.setValue("Bearer USER_SESSION_TOKEN", forHTTPHeaderField: "Authorization")
let requestBody: [String: Any] = [
"jurisdiction": "US-CA",
"criteria": [
"ageCategory": "DIGITAL_YOUTH_OR_ADULT"
],
"options": [
"redirectUrl": "myapp://verification-complete"
]
]
guard let httpBody = try? JSONSerialization.data(withJSONObject: requestBody) else {
completion(nil)
return
}
request.httpBody = httpBody
URLSession.shared.dataTask(with: request) { data, response, error in
guard error == nil,
let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode),
let data = data,
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let verificationUrlString = json["url"] as? String,
let verificationUrl = URL(string: verificationUrlString) else {
completion(nil)
return
}
completion(verificationUrl)
}.resume()
}
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONObject
import java.io.IOException
fun fetchVerificationUrl(callback: (String?) -> Unit) {
val client = OkHttpClient()
val mediaType = "application/json".toMediaType()
val requestBody = JSONObject().apply {
put("jurisdiction", "US-CA")
put("criteria", JSONObject().apply {
put("ageCategory", "DIGITAL_YOUTH_OR_ADULT")
})
put("options", JSONObject().apply {
put("redirectUrl", "myapp://verification-complete")
})
}.toString().toRequestBody(mediaType)
// 调用您自己的服务器端点,而不是直接调用 k-ID
val request = Request.Builder()
.url("https://your-server.com/api/generate-verification-url")
.post(requestBody)
// 添加您自己的身份验证(会话令牌等)
.addHeader("Authorization", "Bearer USER_SESSION_TOKEN")
.addHeader("Content-Type", "application/json")
.build()
client.newCall(request).enqueue(object : Callback {
override fun onResponse(call: Call, response: Response) {
if (!response.isSuccessful) {
callback(null)
return
}
response.body?.use { body ->
try {
val jsonResponse = JSONObject(body.string())
val verificationUrl = jsonResponse.optString("url", null)
callback(verificationUrl)
} catch (e: Exception) {
callback(null)
}
} ?: callback(null)
}
override fun onFailure(call: Call, e: IOException) {
callback(null)
}
})
}
步骤 3:显示验证 URL
- iOS (Swift)
- Android (Kotlin)
import AuthenticationServices
// 将会话存储为属性以防止释放
var authSession: ASWebAuthenticationSession?
func displayVerification(verificationUrl: URL) {
authSession = ASWebAuthenticationSession(
url: verificationUrl,
callbackURLScheme: "myapp"
) { callbackURL, error in
if let error = error {
// 处理错误(用户取消等)
return
}
if let callbackURL = callbackURL {
handleVerificationCallback(callbackURL)
}
}
authSession?.presentationContextProvider = self
authSession?.start()
}
import android.app.Activity
import android.net.Uri
import androidx.browser.customtabs.CustomTabsIntent
fun displayVerification(activity: Activity, verificationUrl: String?) {
if (verificationUrl == null) {
// 处理错误:生成验证 URL 失败
return
}
val builder = CustomTabsIntent.Builder()
val customTabsIntent = builder.build()
val uri = Uri.parse(verificationUrl)
customTabsIntent.launchUrl(activity, uri)
}
步骤 4:处理回调
- iOS (Swift)
- Android (Kotlin)
func handleVerificationCallback(_ callbackURL: URL) {
// 检查这是否是我们期望的回调 URL
guard callbackURL.scheme == "myapp",
callbackURL.host == "verification-complete",
let components = URLComponents(url: callbackURL, resolvingAgainstBaseURL: false),
let queryItems = components.queryItems else {
return
}
let verificationId = queryItems.first(where: { $0.name == "verificationId" })?.value
let result = queryItems.first(where: { $0.name == "result" })?.value
// 仅当验证 URL 来自会话升级时才存在
let sessionId = queryItems.first(where: { $0.name == "sessionId" })?.value
// 根据验证结果更新 UI
if result == "PASS" {
// 处理验证成功
} else if result == "FAIL" {
// 处理验证失败
}
// 可选:使用 API 端点在服务器端验证
if let verificationId = verificationId {
verifyResultServerSide(verificationId: verificationId)
} else if let sessionId = sessionId {
verifyResultServerSide(sessionId: sessionId)
}
}
import android.content.Intent
import android.net.Uri
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent) // 重要:确保使用新意图
val data: Uri? = intent.data
if (data != null && data.scheme == "myapp" && data.host == "verification-complete") {
val verificationId = data.getQueryParameter("verificationId")
val result = data.getQueryParameter("result")
// 仅当验证 URL 来自会话升级时才存在
val sessionId = data.getQueryParameter("sessionId")
// 根据验证结果更新 UI
if (result == "PASS") {
// 处理验证成功
} else {
// 处理验证失败
}
// 可选:使用 API 端点在服务器端验证
if (verificationId != null) {
verifyResultServerSide(verificationId)
} else if (sessionId != null) {
verifyResultServerSide(sessionId)
}
}
}
为了安全性和数据完整性,始终使用适当的 API 端点在服务器端验证结果,而不是仅依赖客户端数据。对于验证,使用 /age-verification/get-status 端点;当验证 URL 来自会话升级时,使用 /session/get 端点。有关分析验证结果的详细信息,包括字段存在规则、状态类型和实现指南,请参阅验证事件契约。如果您使用 Webhook,请参阅投递、重试与恢复了解重试策略和错过事件的恢复方法。
受支持的移动 Web 嵌入
在移动端,仅嵌入以下托管 URL。所有 URL 的显示和处理方式相同:
- 来自访问年龄验证(
/age-verification/perform-access-age-verification)的年龄验证 URL:无会话的独立年龄验证(AgeKit+)。支持瀑布流和单方法流程。 - 来自
/age-gate/check的CHALLENGE_AGE_GATE_AGE_ASSURANCE挑战 URL:当为产品启用自动年龄保证且玩家必须证明所声明的年龄时返回的challenge.url。 - 来自
/session/upgrade的CHALLENGE_SESSION_UPGRADE_BY_AGE_ASSURANCE挑战 URL:当权限需要年龄保证时返回的challenge.url。
对于年龄门控和同意流程本身,请使用自定义工作流原生构建 UX 并遵循 CDK UX 指南,因为年龄门控和端到端小部件不推荐用于移动端。