本指南介绍了如何在 Android 应用中使用 Credential Manager 请求数字凭证,该应用使用 WebView。使用 JavaScript 桥接机制促进网页与应用之间的通信。
概览
以下步骤介绍了来自 WebView 的数字凭证请求的生命周期:
- 请求生成:WebView 中的网页向应用服务器发出请求,应用服务器生成 OpenID4VP 请求 JSON 并将其发送回来。
- 请求发起:网页使用
navigator.credentials.get({ digital: ...}) 通过请求 JSON 请求数字凭证。 - 拦截:注入的
bridge.js会拦截此调用,提取特定的 OpenID4VP 请求 JSON,并将其传递给 Android 应用。 - Android 处理:Android 应用收到消息并调用
CredentialManager.getCredentialAPI。 - 用户选择:Credential Manager 会显示一个界面,提示用户分享其凭证。凭证会返回给 Android 应用。
- 桥接响应:Android 应用使用桥接将响应发送回网页。
- 完成:网页接收并处理响应。
前提条件
在 WebView 中实现数字凭证的 JavaScript 桥接之前,请确保您的项目满足以下要求:
- Android 版本兼容性:您的应用必须以 Android 6.0(API 级别 23)或更高版本为目标平台。
- 宿主应用控制:您必须控制托管
WebView的 Android 应用,以便在加载网页之前附加WebMessageListener。 - HTTPS 和顶级框架:请求凭据的网页必须通过
https://提供,并在主框架中运行(isMainFrame必须为true)。不支持子框架和嵌入式<iframe>元素。 - 实验性 API 选择启用:由于 Digital Credentials API 处于实验阶段,因此您必须使用
@OptIn(ExperimentalDigitalCredentialApi::class)为使用GetDigitalCredentialOption的类或方法添加注解。
添加依赖项
将 Credential Manager 和 AndroidX WebKit 依赖项添加到应用模块的 build.gradle.kts 文件中:
dependencies {
// Credential Manager dependencies
implementation("androidx.credentials:credentials:1.7.0-alpha03")
implementation("androidx.credentials:credentials-play-services-auth:1.7.0-alpha03")
// AndroidX WebKit (required for WebMessageListener bridge)
implementation("androidx.webkit:webkit:1.14.0")
}
在 WebView 中实现数字凭据
您需要向 WebView 添加 JavaScript,以便发出凭据请求并使用 bridge.js 拦截该请求。
创建 JavaScript 请求
如需在 WebView 中运行的 JavaScript 与 Android 代码之间进行通信,请将 JavaScript 代码注入 WebView:
// Example digital credential request to request verified email
const request = {
"providers": [
{
"protocol": "openid4vp",
"request": JSON.stringify({
"requests": [
{
"protocol": "openid4vp-v1-unsigned",
"data": {
"response_type": "vp_token",
"response_mode": "dc_api",
"nonce": "n-0S6_W_2F", // Generate dynamically
"dcql_query": {
"credentials": [
{
"id": "user_info_query",
"format": "dc+sd-jwt",
"meta": {
"vct_values": ["UserInfoCredential"]
},
"claims": [
{"path": ["email"]},
{"path": ["email_verified"]},
{"path": ["name"]}
]
}
]
}
}
}
]
})
}
]
};
try {
const credential = await navigator.credentials.get({
digital: request
});
// Process response
} catch (err) {
// Handle error
}
创建 JavaScript 桥接
创建名为 bridge.js 的文件,或使用 evaluateJavascript 注入此代码。
此脚本会替换 navigator.credentials.get,以拦截请求并将内部 OpenID4VP JSON 字符串发送到 Android 应用。
(function() {
if (!navigator.credentials) {
navigator.credentials = {};
}
const originalGet = navigator.credentials.get;
// Overrides navigator.credentials.get() to intercept W3C Digital Credential requests.
navigator.credentials.get = async function(options) {
if (options && options.digital) {
if (window.DigitalCredentialBridge) {
return new Promise((resolve, reject) => {
const digital = options.digital;
if (digital.providers && digital.providers.length > 0) {
const provider = digital.providers[0];
if (provider.protocol === "openid4vp") {
const requestJson = provider.request;
const message = {
type: "get",
request: requestJson
};
console.log("Sending message to native bridge:", JSON.stringify(message));
// Send the request string to native Android code
window.DigitalCredentialBridge.postMessage(JSON.stringify(message));
// Receive the response sent from native code using replyProxy.postMessage()
window.DigitalCredentialBridge.onmessage = function(event) {
console.log("Received response from native bridge:", event.data);
try {
const response = JSON.parse(event.data);
if (response.error) {
reject(new Error(response.error));
} else {
resolve(response);
}
} catch (e) {
// If it's not JSON, just resolve with the raw data
resolve(event.data);
}
};
} else {
reject(new Error("Unsupported protocol: " + provider.protocol));
}
} else {
reject(new Error("No providers specified in digital credential request"));
}
});
} else {
console.warn("DigitalCredentialBridge not found, falling back to original");
}
}
if (originalGet) {
return originalGet.call(navigator.credentials, options);
} else {
throw new Error("navigator.credentials.get is not supported and no bridge available");
}
};
})();
在 Android 中实现数字凭据
将您的 Android 应用配置为处理从 bridge.js 发送的凭证请求。
创建 Android 监听器
设置一个实现 WebViewCompat.WebMessageListener 的 DigitalCredentialWebListener 类。此类负责处理 JavaScript 与原生 Android 代码之间的双向消息,对 isMainFrame 和 sourceOrigin 执行安全检查,并防止请求重叠。
实现 Credential Manager 处理程序
调用 getCredential,以从设备请求数字凭证。
@OptIn(ExperimentalDigitalCredentialApi::class)
class CredentialManagerHandler(private val context: Context) {
private val credentialManager = CredentialManager.create(context)
suspend fun getDigitalCredential(requestJson: String): String {
val digitalCredentialOption = GetDigitalCredentialOption(requestJson)
val getCredRequest = GetCredentialRequest(listOf(digitalCredentialOption))
return try {
val result = credentialManager.getCredential(context, getCredRequest)
val credential = result.credential
if (credential is DigitalCredential) {
credential.credentialJson
} else {
"{\"error\": \"Unexpected credential type\"}"
}
} catch (e: GetCredentialException) {
"{\"error\": \"${e.message}\"}"
} catch (e: Exception) {
"{\"error\": \"Unknown exception\"}"
}
}
companion object {
private const val TAG = "CredManHandler"
}
}
将框架应用于您的 activity
在 Activity 或 View 中,注册 WebMessageListener 和关联的函数。
// Inside your Activity or View setup
AndroidView(
factory = { context ->
val handler = CredentialManagerHandler(context)
WebView(context).apply {
settings.javaScriptEnabled = true
// Register the bridge before loading URL
if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) {
val listener = DigitalCredentialWebListener(coroutineScope, credentialManagerHandler)
WebViewCompat.addWebMessageListener(
webView,
INTERFACE_NAME,
setOf("*"),
listener
)
loadUrl("https://your-secure-domain.com/login")
}
},
)
正在测试流程
测试该流程,以确认 WebView 与 Android 应用之间的通信。在此过程中,凭证选择器会显示在 WebView 中,完成后,所选凭证会返回到 WebView。