W tym dokumencie opisujemy, jak zintegrować interfejs Credential Manager API z aplikacją na Androida, która korzysta z komponentu WebView.
Omówienie
Zanim przejdziesz do procesu integracji, musisz poznać przepływ komunikacji między natywnym kodem Androida, komponentem internetowym renderowanym w widoku WebView, który zarządza uwierzytelnianiem aplikacji, a backendem. Proces obejmuje rejestrację (tworzenie danych logowania) i uwierzytelnianie (uzyskiwanie istniejących danych logowania).
Rejestracja (tworzenie klucza dostępu)
- Backend generuje początkowy plik JSON rejestracji i wysyła go na stronę internetową renderowaną w widoku WebView.
- Strona internetowa używa
navigator.credentials.create()
do rejestrowania nowych danych logowania. W późniejszym kroku użyjesz wstrzykniętego kodu JavaScript, aby zastąpić tę metodę i wysłać żądanie do aplikacji na Androida. - Aplikacja na Androida używa interfejsu Credential Manager API do tworzenia żądania danych logowania i używa go do
createCredential
. - Interfejs Credential Manager API udostępnia aplikacji dane logowania klucza publicznego.
- Aplikacja odsyła dane logowania klucza publicznego do strony internetowej, aby wstrzyknięty kod JavaScript mógł analizować odpowiedzi.
- Strona internetowa wysyła klucz publiczny do backendu, który go weryfikuje i zapisuje.

Uwierzytelnianie (uzyskiwanie klucza dostępu)
- Backend generuje uwierzytelniający plik JSON, aby uzyskać dane logowania, i wysyła go na stronę internetową renderowaną w klientach WebView.
- Strona internetowa używa metody
navigator.credentials.get
. Użyj wstrzykniętego kodu JavaScript, aby zastąpić tę metodę i przekierować żądanie do aplikacji na Androida. - Aplikacja pobiera dane logowania za pomocą interfejsu Credential Manager API, wywołując
getCredential
. - Interfejs Credential Manager API zwraca dane logowania do aplikacji.
- Aplikacja pobiera podpis cyfrowy klucza prywatnego i wysyła go na stronę internetową, aby wstrzyknięty kod JavaScript mógł analizować odpowiedzi.
- Następnie strona internetowa wysyła go na serwer, który weryfikuje podpis cyfrowy za pomocą klucza publicznego.

Tego samego procesu można używać w przypadku haseł lub systemów tożsamości sfederowanej.
Wymagania wstępne
Aby korzystać z interfejsu Credential Manager API, wykonaj czynności opisane w sekcji Wymagania wstępne w przewodniku po Credential Managerze i upewnij się, że:
Komunikacja w JavaScript
Aby umożliwić komunikację między JavaScriptem w WebView a natywnym kodem Androida, musisz wysyłać wiadomości i obsługiwać żądania między tymi dwoma środowiskami. W tym celu wstrzyknij niestandardowy kod JavaScript do komponentu WebView. Umożliwia to modyfikowanie działania treści internetowych i interakcję z natywnym kodem Androida.
Wstrzykiwanie kodu JavaScript
Poniższy kod JavaScript nawiązuje komunikację między komponentem WebView a aplikacją na Androida. Zastępuje metody navigator.credentials.create()
i navigator.credentials.get()
, które są używane przez WebAuthn API w procesach rejestracji i uwierzytelniania opisanych wcześniej.
W aplikacji użyj zminimalizowanej wersji tego kodu JavaScript.
Tworzenie odbiornika kluczy dostępu
Skonfiguruj PasskeyWebListener
klasę, która obsługuje komunikację z JavaScriptem. Ta klasa powinna dziedziczyć po klasie WebViewCompat.WebMessageListener
. Ta klasa odbiera wiadomości z JavaScriptu i wykonuje niezbędne działania w aplikacji na Androida.
W sekcjach poniżej opisujemy strukturę klasy PasskeyWebListener
oraz obsługę żądań i odpowiedzi.
Obsługa prośby o uwierzytelnienie
Aby obsługiwać żądania operacji WebAuthn navigator.credentials.create()
lub navigator.credentials.get()
, wywoływana jest metoda onPostMessage
klasy PasskeyWebListener
, gdy kod JavaScript wysyła wiadomość do aplikacji na Androida:
// 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)}; """ }
W przypadku handleCreateFlow
i handleGetFlow
zapoznaj się z przykładem w GitHubie.
Obsługa odpowiedzi
Aby obsługiwać odpowiedzi wysyłane z aplikacji natywnej na stronę internetową, dodaj element JavaScriptReplyProxy
w elemencie 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?) }
Pamiętaj, aby wyłapywać wszelkie błędy z aplikacji natywnej i przesyłać je z powrotem do kodu JavaScript.
Integracja z WebView
W tej sekcji opisujemy, jak skonfigurować integrację z WebView.
Inicjowanie WebView
W aktywności aplikacji na Androida zainicjuj WebView
i skonfiguruj powiązany element WebViewClient
. WebViewClient
obsługuje komunikację z kodem JavaScript wstrzykniętym do WebView
.
Skonfiguruj WebView i wywołaj Menedżera danych logowania:
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) } } ) }
Utwórz nowy obiekt klienta WebView i wstrzyknij JavaScript na stronę internetową:
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
Konfigurowanie odbiornika wiadomości internetowych
Aby umożliwić publikowanie wiadomości między JavaScriptem a aplikacją na Androida, skonfiguruj odbiornik wiadomości internetowych za pomocą metody WebViewCompat.addWebMessageListener
.
val rules = setOf("*") if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) { WebViewCompat.addWebMessageListener(webView, PasskeyWebListener.INTERFACE_NAME, rules, passkeyWebListener) }
Integracja z witryną
Aby dowiedzieć się, jak utworzyć integrację z internetem, przeczytaj artykuły Tworzenie klucza dostępu do logowania bez hasła i Logowanie się przy użyciu klucza dostępu przez automatyczne wypełnianie formularza.
Testowanie i wdrażanie
Dokładnie przetestuj cały proces w kontrolowanym środowisku, aby zapewnić prawidłową komunikację między aplikacją na Androida, stroną internetową i backendem.
Wdróż zintegrowane rozwiązanie w środowisku produkcyjnym, aby mieć pewność, że backend może obsługiwać przychodzące żądania rejestracji i uwierzytelniania. Kod backendu powinien generować początkowy plik JSON na potrzeby rejestracji (tworzenia) i uwierzytelniania (pobierania). Powinien też obsługiwać sprawdzanie i weryfikację odpowiedzi otrzymanych ze strony internetowej.
Sprawdź, czy implementacja jest zgodna z zaleceniami dotyczącymi UX.
Ważne uwagi
- Użyj podanego kodu JavaScript do obsługi operacji
navigator.credentials.create()
inavigator.credentials.get()
. - Klasa
PasskeyWebListener
jest pomostem między aplikacją na Androida a kodem JavaScript w komponencie WebView. Odpowiada za przekazywanie wiadomości, komunikację i wykonywanie wymaganych działań. - Dostosuj podane fragmenty kodu do struktury projektu, konwencji nazewnictwa i wszelkich wymagań, które mogą być potrzebne.
- Wyłapywanie błędów po stronie aplikacji natywnej i przesyłanie ich z powrotem do JavaScriptu.
Postępując zgodnie z tym przewodnikiem i integrując interfejs Credential Manager API z aplikacją na Androida, która korzysta z komponentu WebView, możesz zapewnić użytkownikom bezpieczne i płynne logowanie za pomocą kluczy dostępu, a zarazem skutecznie zarządzać ich danymi logowania.