Tài liệu này mô tả cách tích hợp API Trình quản lý thông tin xác thực với một ứng dụng Android sử dụng WebView.
Tổng quan
Trước khi tìm hiểu kỹ về quy trình tích hợp, bạn cần nắm được quy trình giao tiếp giữa mã Android gốc (một thành phần web hiển thị trong WebView giúp quản lý tính năng xác thực của ứng dụng) và một phần phụ trợ. Quy trình này bao gồm đăng ký (tạo thông tin xác thực) và xác thực (lấy thông tin xác thực hiện có).
Đăng ký (tạo khoá truy cập)
- Phần phụ trợ tạo tệp JSON đăng ký ban đầu rồi gửi tệp này đến trang web hiển thị trong WebView.
- Trang web này sử dụng
navigator.credentials.create()
để đăng ký thông tin xác thực mới. Bạn sẽ dùng JavaScript đã chèn để ghi đè phương thức này trong bước sau nhằm gửi yêu cầu đến ứng dụng Android. - Ứng dụng Android dùng API Trình quản lý thông tin xác thực để tạo yêu cầu về thông tin xác thực và dùng yêu cầu đó để
createCredential
. - API Trình quản lý thông tin xác thực chia sẻ thông tin xác thực khoá công khai với ứng dụng.
- Ứng dụng sẽ gửi thông tin xác thực khoá công khai trở lại trang web để JavaScript được chèn có thể phân tích cú pháp các phản hồi.
- Trang web sẽ gửi khoá công khai đến phần phụ trợ để xác minh và lưu khoá công khai.

Xác thực (tạo khoá truy cập)
- Phần phụ trợ tạo tệp JSON xác thực để lấy thông tin xác thực rồi gửi thông tin này đến trang web hiển thị trong ứng dụng WebView.
- Trang web này sử dụng
navigator.credentials.get
. Hãy dùng JavaScript đã chèn để ghi đè phương thức này nhằm chuyển hướng yêu cầu đến ứng dụng Android. - Ứng dụng truy xuất thông tin xác thực thông qua API Trình quản lý thông tin xác thực bằng cách gọi
getCredential
. - API Trình quản lý thông tin xác thực trả thông tin xác thực về cho ứng dụng.
- Ứng dụng nhận chữ ký số của khoá riêng tư và gửi đến trang web để JavaScript được chèn có thể phân tích cú pháp các phản hồi.
- Sau đó, trang web gửi chữ ký này đến máy chủ để xác minh chữ ký số bằng khoá công khai.

Có thể sử dụng cùng một quy trình cho mật khẩu hoặc hệ thống nhận dạng được liên kết.
Điều kiện tiên quyết
Để sử dụng API Trình quản lý thông tin xác thực, hãy hoàn thành các bước nêu trong phần điều kiện tiên quyết của hướng dẫn về Trình quản lý thông tin xác thực và đảm bảo bạn làm như sau:
- Thêm các phần phụ thuộc bắt buộc.
- Duy trì các lớp trong tệp ProGuard.
- Thêm tính năng hỗ trợ cho Digital Asset Links (Đường liên kết đến tài sản kỹ thuật số).
Hoạt động giao tiếp của JavaScript
Để cho phép JavaScript trong WebView và mã Android gốc giao tiếp với nhau, bạn cần gửi thông báo và xử lý các yêu cầu giữa hai môi trường. Để làm như vậy, hãy chèn mã JavaScript tuỳ chỉnh vào một WebView. Việc này cho phép bạn sửa đổi hành vi của nội dung web và tương tác với mã Android gốc.
Chèn JavaScript
Mã JavaScript sau đây thiết lập hoạt động giao tiếp giữa WebView và ứng dụng Android. Thao tác này sẽ ghi đè các phương thức navigator.credentials.create()
và navigator.credentials.get()
mà API WebAuthn sử dụng cho các quy trình đăng ký và xác thực đã mô tả trước đó.
Dùng phiên bản rút gọn của mã JavaScript này trong ứng dụng của bạn.
Tạo trình nghe cho khoá truy cập
Thiết lập một lớp PasskeyWebListener
xử lý việc giao tiếp bằng JavaScript. Lớp này cần phải kế thừa từ WebViewCompat.WebMessageListener
. Lớp này nhận thông báo từ JavaScript và thực hiện các thao tác cần thiết trong ứng dụng Android.
Các phần sau đây mô tả cấu trúc của lớp PasskeyWebListener
, cũng như cách xử lý các yêu cầu và phản hồi.
Xử lý yêu cầu xác thực
Để xử lý yêu cầu cho các thao tác navigator.credentials.create()
hoặc navigator.credentials.get()
của WebAuthn, phương thức onPostMessage
của lớp PasskeyWebListener
sẽ được gọi khi mã JavaScript gửi thông báo đến ứng dụng Android:
// The class talking to Javascript should inherit:
class PasskeyWebListener(
private val activity: Activity,
private val coroutineScope: CoroutineScope,
private val credentialManagerHandler: CredentialManagerHandler
) : WebViewCompat.WebMessageListener {
/** havePendingRequest is true if there is an outstanding WebAuthn request.
There is only ever one request outstanding at a time. */
private var havePendingRequest = false
/** pendingRequestIsDoomed is true if the WebView has navigated since
starting a request. The FIDO module cannot be canceled, but the response
will never be delivered in this case. */
private var pendingRequestIsDoomed = false
/** replyChannel is the port that the page is listening for a response on.
It is valid if havePendingRequest is true. */
private var replyChannel: ReplyChannel? = null
/**
* Called by the page during a WebAuthn request.
*
* @param view Creates the WebView.
* @param message The message sent from the client using injected JavaScript.
* @param sourceOrigin The origin of the HTTPS request. Should not be null.
* @param isMainFrame Should be set to true. Embedded frames are not
supported.
* @param replyProxy Passed in by JavaScript. Allows replying when wrapped in
the Channel.
* @return The message response.
*/
@UiThread
override fun onPostMessage(
view: WebView,
message: WebMessageCompat,
sourceOrigin: Uri,
isMainFrame: Boolean,
replyProxy: JavaScriptReplyProxy,
) {
val messageData = message.data ?: return
onRequest(
messageData,
sourceOrigin,
isMainFrame,
JavaScriptReplyChannel(replyProxy)
)
}
private fun onRequest(
msg: String,
sourceOrigin: Uri,
isMainFrame: Boolean,
reply: ReplyChannel,
) {
msg?.let {
val jsonObj = JSONObject(msg);
val type = jsonObj.getString(TYPE_KEY)
val message = jsonObj.getString(REQUEST_KEY)
if (havePendingRequest) {
postErrorMessage(reply, "The request already in progress", type)
return
}
replyChannel = reply
if (!isMainFrame) {
reportFailure("Requests from subframes are not supported", type)
return
}
val originScheme = sourceOrigin.scheme
if (originScheme == null || originScheme.lowercase() != "https") {
reportFailure("WebAuthn not permitted for current URL", type)
return
}
// Verify that origin belongs to your website,
// it's because the unknown origin may gain credential info.
// if (isUnknownOrigin(originScheme)) {
// return
// }
havePendingRequest = true
pendingRequestIsDoomed = false
// Use a temporary "replyCurrent" variable to send the data back, while
// resetting the main "replyChannel" variable to null so it’s ready for
// the next request.
val replyCurrent = replyChannel
if (replyCurrent == null) {
Log.i(TAG, "The reply channel was null, cannot continue")
return;
}
when (type) {
CREATE_UNIQUE_KEY ->
this.coroutineScope.launch {
handleCreateFlow(credentialManagerHandler, message, replyCurrent)
}
GET_UNIQUE_KEY -> this.coroutineScope.launch {
handleGetFlow(credentialManagerHandler, message, replyCurrent)
}
else -> Log.i(TAG, "Incorrect request json")
}
}
}
private suspend fun handleCreateFlow(
credentialManagerHandler: CredentialManagerHandler,
message: String,
reply: ReplyChannel,
) {
try {
havePendingRequest = false
pendingRequestIsDoomed = false
val response = credentialManagerHandler.createPasskey(message)
val successArray = ArrayList<Any>();
successArray.add("success");
successArray.add(JSONObject(response.registrationResponseJson));
successArray.add(CREATE_UNIQUE_KEY);
reply.send(JSONArray(successArray).toString())
replyChannel = null // setting initial replyChannel for the next request
} catch (e: CreateCredentialException) {
reportFailure(
"Error: ${e.errorMessage} w type: ${e.type} w obj: $e",
CREATE_UNIQUE_KEY
)
} catch (t: Throwable) {
reportFailure("Error: ${t.message}", CREATE_UNIQUE_KEY)
}
}
companion object {
/** INTERFACE_NAME is the name of the MessagePort that must be injected into pages. */
const val INTERFACE_NAME = "__webauthn_interface__"
const val TYPE_KEY = "type"
const val REQUEST_KEY = "request"
const val CREATE_UNIQUE_KEY = "create"
const val GET_UNIQUE_KEY = "get"
/** INJECTED_VAL is the minified version of the JavaScript code described at this class
* heading. The non minified form is found at credmanweb/javascript/encode.js.*/
const val INJECTED_VAL = """
var __webauthn_interface__,__webauthn_hooks__;!function(e){console.log("In the hook."),__webauthn_interface__.addEventListener("message",function e(n){var r=JSON.parse(n.data),t=r[2];"get"===t?o(r):"create"===t?u(r):console.log("Incorrect response format for reply")});var n=null,r=null,t=null,a=null;function o(e){if(null!==n&&null!==t){if("success"!=e[0]){var r=t;n=null,t=null,r(new DOMException(e[1],"NotAllowedError"));return}var a=i(e[1]),o=n;n=null,t=null,o(a)}}function l(e){var n=e.length%4;return Uint8Array.from(atob(e.replace(/-/g,"+").replace(/_/g,"/").padEnd(e.length+(0===n?0:4-n),"=")),function(e){return e.charCodeAt(0)}).buffer}function s(e){return btoa(Array.from(new Uint8Array(e),function(e){return String.fromCharCode(e)}).join("")).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+${'$'}/,"")}function u(e){if(null===r||null===a){console.log("Here: "+r+" and reject: "+a);return}if(console.log("Output back: "+e),"success"!=e[0]){var n=a;r=null,a=null,n(new DOMException(e[1],"NotAllowedError"));return}var t=i(e[1]),o=r;r=null,a=null,o(t)}function i(e){return console.log("Here is the response from credential manager: "+e),e.rawId=l(e.rawId),e.response.clientDataJSON=l(e.response.clientDataJSON),e.response.hasOwnProperty("attestationObject")&&(e.response.attestationObject=l(e.response.attestationObject)),e.response.hasOwnProperty("authenticatorData")&&(e.response.authenticatorData=l(e.response.authenticatorData)),e.response.hasOwnProperty("signature")&&(e.response.signature=l(e.response.signature)),e.response.hasOwnProperty("userHandle")&&(e.response.userHandle=l(e.response.userHandle)),e.getClientExtensionResults=function e(){return{}},e}e.create=function n(t){if(!("publicKey"in t))return e.originalCreateFunction(t);var o=new Promise(function(e,n){r=e,a=n}),l=t.publicKey;if(l.hasOwnProperty("challenge")){var u=s(l.challenge);l.challenge=u}if(l.hasOwnProperty("user")&&l.user.hasOwnProperty("id")){var i=s(l.user.id);l.user.id=i}var c=JSON.stringify({type:"create",request:l});return __webauthn_interface__.postMessage(c),o},e.get=function r(a){if(!("publicKey"in a))return e.originalGetFunction(a);var o=new Promise(function(e,r){n=e,t=r}),l=a.publicKey;if(l.hasOwnProperty("challenge")){var u=s(l.challenge);l.challenge=u}var i=JSON.stringify({type:"get",request:l});return __webauthn_interface__.postMessage(i),o},e.onReplyGet=o,e.CM_base64url_decode=l,e.CM_base64url_encode=s,e.onReplyCreate=u}(__webauthn_hooks__||(__webauthn_hooks__={})),__webauthn_hooks__.originalGetFunction=navigator.credentials.get,__webauthn_hooks__.originalCreateFunction=navigator.credentials.create,navigator.credentials.get=__webauthn_hooks__.get,navigator.credentials.create=__webauthn_hooks__.create,window.PublicKeyCredential=function(){},window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable=function(){return Promise.resolve(!1)};
"""
}
Đối với handleCreateFlow
và handleGetFlow
, hãy tham khảo ví dụ này trên GitHub.
Xử lý phản hồi
Để xử lý các phản hồi được gửi từ ứng dụng gốc đến trang web, hãy thêm JavaScriptReplyProxy
trong JavaScriptReplyChannel
.
// The setup for the reply channel allows communication with JavaScript.
private class JavaScriptReplyChannel(private val reply: JavaScriptReplyProxy) :
ReplyChannel {
override fun send(message: String?) {
try {
reply.postMessage(message!!)
} catch (t: Throwable) {
Log.i(TAG, "Reply failure due to: " + t.message);
}
}
}
// ReplyChannel is the interface where replies to the embedded site are
// sent. This allows for testing since AndroidX bans mocking its objects.
interface ReplyChannel {
fun send(message: String?)
}
Hãy nhớ phát hiện mọi lỗi từ ứng dụng gốc và gửi lại các lỗi đó về phía JavaScript.
Tích hợp với WebView
Phần này mô tả cách thiết lập quá trình tích hợp WebView.
Khởi chạy WebView
Trong hoạt động của ứng dụng Android, hãy khởi chạy WebView
và thiết lập một WebViewClient
đi kèm. WebViewClient
xử lý hoạt động giao tiếp với mã JavaScript được chèn vào WebView
.
Thiết lập WebView và gọi Trình quản lý thông tin xác thực:
val credentialManagerHandler = CredentialManagerHandler(this)
setContent {
val coroutineScope = rememberCoroutineScope()
AndroidView(factory = {
WebView(it).apply {
settings.javaScriptEnabled = true
// Test URL:
val url = "https://passkeys-codelab.glitch.me/"
val listenerSupported = WebViewFeature.isFeatureSupported(
WebViewFeature.WEB_MESSAGE_LISTENER
)
if (listenerSupported) {
// Inject local JavaScript that calls Credential Manager.
hookWebAuthnWithListener(
this, this@WebViewMainActivity,
coroutineScope, credentialManagerHandler
)
} else {
// Fallback routine for unsupported API levels.
}
loadUrl(url)
}
}
)
}
Tạo một đối tượng ứng dụng WebView mới và chèn JavaScript vào trang web:
val passkeyWebListener = PasskeyWebListener(activity, coroutineScope, credentialManagerHandler)
val webViewClient = object : WebViewClient() {
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
super.onPageStarted(view, url, favicon)
webView.evaluateJavascript(PasskeyWebListener.INJECTED_VAL, null)
}
}
webView.webViewClient = webViewClient
Thiết lập trình nghe thông báo trên web
Để cho phép đăng thông báo giữa JavaScript và ứng dụng Android, hãy thiết lập trình nghe thông báo trên web bằng phương thức WebViewCompat.addWebMessageListener
.
val rules = setOf("*")
if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) {
WebViewCompat.addWebMessageListener(webView, PasskeyWebListener.INTERFACE_NAME,
rules, passkeyWebListener)
}
Tích hợp web
Để tìm hiểu cách tạo quy trình tích hợp web, hãy xem bài viết Tạo khoá truy cập cho hoạt động đăng nhập không cần mật khẩu và Đăng nhập bằng khoá truy cập thông qua tính năng tự động điền biểu mẫu.
Kiểm thử và triển khai
Kiểm thử kỹ toàn bộ quy trình trong một môi trường được kiểm soát để đảm bảo hoạt động giao tiếp đúng cách giữa ứng dụng Android, trang web và phần phụ trợ.
Triển khai giải pháp tích hợp vào giai đoạn sản xuất, đảm bảo rằng phần phụ trợ có thể xử lý các yêu cầu đăng ký và xác thực sắp tới. Mã phụ trợ phải tạo JSON ban đầu cho các quy trình đăng ký (tạo) và xác thực (nhận). Thao tác này cũng sẽ xử lý việc xác thực và xác minh các phản hồi nhận được từ trang web.
Xác minh rằng phương thức triển khai này phù hợp với các đề xuất về trải nghiệm người dùng.
Lưu ý quan trọng
- Sử dụng mã JavaScript được cung cấp để xử lý các thao tác
navigator.credentials.create()
vànavigator.credentials.get()
. - Lớp
PasskeyWebListener
là cầu nối giữa ứng dụng Android và mã JavaScript trong WebView. Thư viện này xử lý việc truyền thông báo, giao tiếp và thực thi các hành động bắt buộc. - Điều chỉnh các đoạn mã được cung cấp cho phù hợp với cấu trúc, quy ước đặt tên của dự án và mọi yêu cầu cụ thể mà bạn có thể có.
- Phát hiện lỗi ở phía ứng dụng gốc và gửi lại phía JavaScript.
Bằng cách làm theo hướng dẫn này và tích hợp API Trình quản lý thông tin xác thực vào một ứng dụng Android sử dụng WebView, bạn có thể mang đến cho người dùng trải nghiệm đăng nhập an toàn và liền mạch bằng khoá truy cập, đồng thời quản lý hiệu quả thông tin xác thực của họ.