Bu belgede, Kimlik Bilgisi Yöneticisi API'sinin WebView kullanan bir Android uygulamasıyla nasıl entegre edileceği açıklanmaktadır.
Genel Bakış
Entegrasyon sürecine başlamadan önce, yerel Android kodu, uygulamanızın kimlik doğrulamasını yöneten bir Web Görünümü'nde oluşturulan bir web bileşeni ve arka uç arasındaki iletişim akışını anlamanız önemlidir. Bu akış, kayıt (kimlik bilgisi oluşturma) ve kimlik doğrulama (mevcut kimlik bilgilerini alma) işlemlerini içerir.
Kayıt (geçiş anahtarı oluşturma)
- Arka uç, ilk kayıt JSON'unu oluşturur ve WebView içinde oluşturulan web sayfasına gönderir.
- Web sayfası yeni kimlik bilgilerini kaydetmek için
navigator.credentials.create()
adresini kullanır. İsteği Android uygulamasına göndermek amacıyla sonraki bir adımda bu yöntemi geçersiz kılmak için yerleştirilen JavaScript'i kullanacaksınız. - Android uygulaması, kimlik bilgisi isteğini oluşturmak ve
createCredential
için kullanmak üzere Kimlik Bilgisi Yöneticisi API'sini kullanır. - Kimlik Bilgisi Yöneticisi API'si, ortak anahtar kimlik bilgisini uygulamayla paylaşır.
- Uygulama, ortak anahtar kimlik bilgisini web sayfasına geri gönderir. Böylece, eklenen JavaScript yanıtları ayrıştırabilir.
- Web sayfası, ortak anahtarı arka uca göndererek ortak anahtarı doğrular ve kaydeder.
Kimlik doğrulama (geçiş anahtarı alın)
- Arka uç, kimlik bilgisini almak için kimlik doğrulama JSON'u oluşturur ve bunu WebView istemcisinde oluşturulan web sayfasına gönderir.
- Web sayfasında
navigator.credentials.get
kullanılıyor. İsteği Android uygulamasına yönlendirmek için bu yöntemi geçersiz kılmak üzere yerleştirilen JavaScript'i kullanın. - Uygulama,
getCredential
yöntemini çağırarak Credential Manager API'yi kullanarak kimlik bilgisini alır. - Kimlik Bilgisi Yöneticisi API'si, kimlik bilgisini uygulamaya döndürür.
- Uygulama, özel anahtarın dijital imzasını alır ve yerleştirilen JavaScript'in yanıtları ayrıştırabilmesi için bunu web sayfasına gönderir.
- Ardından web sayfası, dijital imzayı ortak anahtarla doğrulayan sunucuya gönderir.
Aynı akış, şifreler veya birleşik kimlik sistemleri için de kullanılabilir.
Ön koşullar
Kimlik Bilgisi Yöneticisi API'sini kullanmak için Kimlik Bilgisi Yöneticisi kılavuzunun ön koşullar bölümünde belirtilen adımları tamamlayın ve aşağıdakileri yaptığınızdan emin olun:
- Gerekli bağımlılıkları ekleyin.
- ProGuard dosyasında sınıfları koruma.
- Digital Asset Links desteği eklendi.
JavaScript iletişimi
Web Görünümünde JavaScript'in ve yerel Android kodunun birbiriyle konuşmasına izin vermek için mesaj göndermeniz ve iki ortam arasında istekleri işlemeniz gerekir. Bunu yapmak için bir WebView'e özel JavaScript kodu ekleyin. Bu sayede web içeriğinin davranışını değiştirebilir ve yerel Android koduyla etkileşimde bulunabilirsiniz.
JavaScript ekleme
Aşağıdaki JavaScript kodu, WebView ile Android uygulaması arasında iletişim kurar. Daha önce açıklanan kayıt ve kimlik doğrulama akışları için WebAuthn API tarafından kullanılan navigator.credentials.create()
ve navigator.credentials.get()
yöntemlerini geçersiz kılar.
Uygulamanızda bu JavaScript kodunun sıkıştırılmış sürümünü kullanın.
Geçiş anahtarları için işleyici oluşturma
JavaScript ile iletişimi yöneten bir PasskeyWebListener
sınıfı oluşturun. Bu sınıf, WebViewCompat.WebMessageListener
sınıfından devralınmalıdır. Bu sınıf, JavaScript'ten mesaj alır ve Android uygulamasında gerekli işlemleri gerçekleştirir.
Kotlin
// The class talking to Javascript should inherit:
class PasskeyWebListener(
private val activity: Activity,
private val coroutineScope: CoroutineScope,
private val credentialManagerHandler: CredentialManagerHandler
) : WebViewCompat.WebMessageListener
// ... Implementation details
Java
// The class talking to Javascript should inherit:
class PasskeyWebListener implements WebViewCompat.WebMessageListener {
// Implementation details
private Activity activity;
// Handles get/create methods meant for Java:
private CredentialManagerHandler credentialManagerHandler;
public PasskeyWebListener(
Activity activity,
CredentialManagerHandler credentialManagerHandler
) {
this.activity = activity;
this.credentialManagerHandler = credentialManagerHandler;
}
// ... Implementation details
}
PasskeyWebListener
içinde, aşağıdaki bölümlerde açıklandığı gibi istek ve yanıt mantığını uygulayın.
Kimlik doğrulama isteğini işleme
JavaScript kodu Android uygulamasına bir mesaj gönderdiğinde, WebAuthn navigator.credentials.create()
veya navigator.credentials.get()
işlemlerinin isteklerini işlemek için PasskeyWebListener
sınıfının onPostMessage
yöntemi çağrılır:
Kotlin
class PasskeyWebListener(...)... {
// ...
/** 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 {
const val TYPE_KEY = "type"
const val REQUEST_KEY = "request"
const val CREATE_UNIQUE_KEY = "create"
const val GET_UNIQUE_KEY = "get"
}
}
Java
class PasskeyWebListener implements ... {
// ...
/**
* 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
public void onPostMessage(
@NonNull WebView view,
@NonNull WebMessageCompat message,
@NonNull Uri sourceOrigin,
Boolean isMainFrame,
@NonNull JavaScriptReplyProxy replyProxy,
) {
if (messageData == null) {
return;
}
onRequest(
messageData,
sourceOrigin,
isMainFrame,
JavaScriptReplyChannel(replyProxy)
)
}
private void onRequest(
String msg,
Uri sourceOrigin,
boolean isMainFrame,
ReplyChannel reply
) {
if (msg != null) {
try {
JSONObject jsonObj = new JSONObject(msg);
String type = jsonObj.getString(TYPE_KEY);
String message = jsonObj.getString(REQUEST_KEY);
boolean isCreate = type.equals(CREATE_UNIQUE_KEY);
boolean isGet = type.equals(GET_UNIQUE_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;
}
String originScheme = sourceOrigin.getScheme();
if (originScheme == null || !originScheme.toLowerCase().equals("https")) {
reportFailure("WebAuthn not permitted for current URL", type);
return;
}
// Verify that origin belongs to your website,
// Requests of unknown origin may gain access to 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.
ReplyChannel replyCurrent = replyChannel;
if (replyCurrent == null) {
Log.i(TAG, "The reply channel was null, cannot continue");
return;
}
if (isCreate) {
handleCreateFlow(credentialManagerHandler, message, replyCurrent));
} else if (isGet) {
handleGetFlow(credentialManagerHandler, message, replyCurrent));
} else {
Log.i(TAG, "Incorrect request json");
}
} catch (JSONException e) {
e.printStackTrace();
}
}
}
}
handleCreateFlow
ve handleGetFlow
için GitHub'daki örneğe bakın.
Yanıtı işleme
Yerel uygulamadan web sayfasına gönderilen yanıtları işlemek için JavaScriptReplyChannel
içine JavaScriptReplyProxy
ekleyin.
Kotlin
class PasskeyWebListener(...)... {
// ...
// 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?)
}
}
Java
class PasskeyWebListener implements ... {
// ...
// The setup for the reply channel allows communication with JavaScript.
private static class JavaScriptReplyChannel implements ReplyChannel {
private final JavaScriptReplyProxy reply;
JavaScriptReplyChannel(JavaScriptReplyProxy reply) {
this.reply = reply;
}
@Override
public void send(String message) {
reply.postMessage(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 {
void send(String message);
}
}
Yerel uygulamadaki hataları yakalayıp JavaScript tarafına geri gönderdiğinizden emin olun.
WebView ile entegrasyon
Bu bölümde, WebView entegrasyonunuzu nasıl ayarlayacağınız açıklanmaktadır.
WebView'u başlatma
Android uygulamanızın etkinliğinde bir WebView
başlatın ve eşlik eden bir WebViewClient
oluşturun. WebViewClient
, WebView
içine yerleştirilen JavaScript koduyla iletişimi yönetir.
Web Görünümü'nü ayarlayın ve Kimlik Bilgisi Yöneticisi'ni arayın:
Kotlin
val credentialManagerHandler = CredentialManagerHandler(this)
// ...
AndroidView(factory = {
WebView(it).apply {
settings.javaScriptEnabled = true
// Test URL:
val url = "https://credman-web-test.glitch.me/"
val listenerSupported = WebViewFeature.isFeatureSupported(
WebViewFeature.WEB_MESSAGE_LISTENER
)
if (listenerSupported) {
// Inject local JavaScript that calls Credential Manager.
hookWebAuthnWithListener(this, this@MainActivity,
coroutineScope, credentialManagerHandler)
} else {
// Fallback routine for unsupported API levels.
}
loadUrl(url)
}
}
)
Java
// Example shown in the onCreate method of an Activity
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
WebView webView = findViewById(R.id.web_view);
// Test URL:
String url = "https://credman-web-test.glitch.me/";
Boolean listenerSupported = WebViewFeature.isFeatureSupported(
WebViewFeature.WEB_MESSAGE_LISTENER
);
if (listenerSupported) {
// Inject local JavaScript that calls Credential Manager.
hookWebAuthnWithListener(webView, this,
coroutineScope, credentialManagerHandler)
} else {
// Fallback routine for unsupported API levels.
}
webView.loadUrl(url);
}
Yeni bir WebView istemci nesnesi oluşturun ve web sayfasına JavaScript ekleyin:
Kotlin
// This is an example call into hookWebAuthnWithListener
val passkeyWebListener = PasskeyWebListener(
activity, coroutineScope, credentialManagerHandler
)
val webViewClient = object : WebViewClient() {
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
super.onPageStarted(view, url, favicon)
// Handle page load events
passkeyWebListener.onPageStarted();
webView.evaluateJavascript(PasskeyWebListener.INJECTED_VAL, null)
}
}
webView.webViewClient = webViewClient
Java
// This is an example call into hookWebAuthnWithListener
PasskeyWebListener passkeyWebListener = new PasskeyWebListener(
activity, credentialManagerHandler
)
WebViewClient webiewClient = new WebViewClient() {
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
super.onPageStarted(view, url, favicon);
// Handle page load events
passkeyWebListener.onPageStarted();
webView.evaulateJavascript(PasskeyWebListener.INJECTED_VAL, null);
}
};
webView.setWebViewClient(webViewClient);
Web mesajı dinleyicisi oluşturma
JavaScript ile Android uygulaması arasında mesajların gönderilmesine izin vermek için WebViewCompat.addWebMessageListener
yöntemiyle bir web mesajı dinleyici ayarlayın.
Kotlin
val rules = setOf("*")
if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) {
WebViewCompat.addWebMessageListener(
webView, PasskeyWebListener.INTERFACE_NAME, rules, passkeyWebListener
)
}
Java
Set<String> rules = new HashSet<>(Arrays.asList("*"));
if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) {
WebViewCompat.addWebMessageListener(
webView, PasskeyWebListener.INTERFACE_NAME, rules, passkeyWebListener
)
}
Web entegrasyonu
Web entegrasyonunu nasıl oluşturacağınızı öğrenmek için Şifresiz girişler için geçiş anahtarı oluşturma ve Form otomatik doldurma özelliğiyle geçiş anahtarıyla oturum açma başlıklı makaleleri inceleyin.
Test ve dağıtım
Android uygulaması, web sayfası ve arka uç arasında düzgün iletişim sağlamak için akışın tamamını kontrollü bir ortamda ayrıntılı olarak test edin.
Entegre çözümü üretime dağıtın. Bu işlem sırasında arka uçta gelen kayıt ve kimlik doğrulama isteklerinin işlendiğinden emin olun. Arka uç kodu, kayıt (oluşturma) ve kimlik doğrulama (alma) işlemleri için ilk JSON'u oluşturmalıdır. Ayrıca, web sayfasından alınan yanıtların doğrulanması ve doğrulanması da bu işlev tarafından yapılmalıdır.
Uygulamanın kullanıcı deneyimi önerilerine uygun olduğunu doğrulayın.
Önemli notlar
navigator.credentials.create()
venavigator.credentials.get()
işlemlerini işlemek için sağlanan JavaScript kodunu kullanın.PasskeyWebListener
sınıfı, Android uygulaması ile Web Görünümü'ndeki JavaScript kodu arasındaki köprüdür. Mesaj iletme, iletişim ve gerekli işlemlerin yürütülmesini sağlar.- Sağlanan kod snippet'lerini projenizin yapısına, adlandırma kurallarına ve olası tüm gereksinimlerinize uyacak şekilde uyarlayın.
- Yerel uygulama tarafındaki hataları yakalayıp JavaScript tarafına geri gönderin.
Bu kılavuzu uygulayarak ve Kimlik Bilgisi Yöneticisi API'yi WebView kullanan Android uygulamanıza entegre ederek kullanıcılarınıza kimlik bilgilerini etkili bir şekilde yönetirken geçiş anahtarı özellikli güvenli ve sorunsuz bir giriş deneyimi sunabilirsiniz.