Bagaimana untuk mengendalikan deep linking dan skema tersuai dalam Android WebView? Mengendalikan deep link dalam WebView Android memerlukan pelanjutan WebViewClient, mengatasi (override) shouldOverrideUrlLoading untuk menyemak isForMainFrame, mengesahkan skema tersuai terhadap senarai kebenaran (allowlist), dan melancarkan intent luaran yang dibenarkan dengan selamat bagi mengelakkan skema bukan web kekal dalam saluran pemuatan WebView.
Pemintasan deep link Android WebView ialah amalan pengaturcaraan untuk mengatasi panggil balik navigasi dalam WebViewClient tersuai bagi menangkap protokol bukan HTTP, seperti skema URI tersuai dan URI Intent Chrome. Dengan menilai metadata permintaan dan mengesahkan payload destinasi, aplikasi hos melaksanakan pelancaran Intent asli sambil mencegah ranap sistem akibat protokol yang tidak dikendalikan.
| Istilah | Definisi | Entiti Berkaitan | Peranan Niat Carian |
|---|---|---|---|
| WebView | Komponen paparan berasaskan Chromium terbenam yang merender kandungan web di dalam aplikasi Android. | SDK Android | Maklumat / Komersial |
| Skema URL Tersuai | Protokol URI proprietari yang membenarkan pautan web menyasarkan aplikasi asli. | Penghalaan Deep Link | Teknikal / Maklumat |
| Web ke Apl | Proses peralihan pengguna dari halaman web ke antara muka aplikasi asli. | Mobile Deep Linking | Maklumat |

Mengapa Android WebView Mengeluarkan Ralat net ERR UNKNOWN URL SCHEME
Had Protokol Chromium: Mengapa WebView Secara Nati Hanya Mengenali Skema Web Standard
Komponen WebView Android dikuasakan oleh enjin perenderan Chromium asas yang dikonfigurasikan terutamanya untuk menghurai dan memaparkan protokol sumber web standard: http://, https://, file://, data://, about:, dan javascript:.
Secara lalai, apabila tiada WebViewClient tersuai diberikan, rangka kerja Android mewakilkan URL yang tidak dikendalikan kepada Pengurus Aktiviti sistem. Walau bagaimanapun, sebaik sahaja aplikasi menyediakan WebViewClient tersuai, skema bukan HTTP(S) (seperti myapp://product/detail/1024, alipay://, atau whatsapp://) dihalakan terus ke dalam panggil balik navigasi klien. Jika aplikasi hos membiarkan skema tersuai kekal dalam saluran pemuatan WebView dan bukannya memintasnya, enjin Chromium akan cuba menyelesaikan skema tersuai itu sebagai sumber web biasa. Disebabkan protokol bukan standard tidak boleh diselesaikan terhadap lapisan pengangkutan pelayan web atau DNS, enjin tersebut akan memaparkan kegagalan navigasi tahap rendah.
Anatomi Ranap net::ERR_UNKNOWN_URL_SCHEME: Daripada Ketikan Pengguna kepada Kegagalan Pemuatan Sumber Web
Peralihan daripada interaksi pengguna kepada halaman ralat berlaku dalam urutan yang pantas dan berketentuan:
- Pencetus Pengguna: Pengguna mengetik butang seruan tindak (CTA) atau hiperpautan di dalam halaman pendaratan H5 terbenam.
- Permintaan Navigasi: Enjin web membina permintaan navigasi yang mengandungi rentetan URI sasaran (contohnya,
myapp://promo/summer). - Penilaian Saluran: Jika
shouldOverrideUrlLoading()mengembalikanfalseatau gagal memintas URI bukan HTTP(S), enjin Chromium menganggap URL tersebut sebagai pemuatan sumber web standard. - Penghantaran Ralat: Oleh kerana tidak menemui pengendali dalaman untuk protokol tersebut, enjin mencetuskan
onReceivedError()dan menggantikan kandungan web view dengan templat ralat Chromium lalai:net::ERR_UNKNOWN_URL_SCHEME.
Halaman ralat ini menghentikan perjalanan pengguna, menghalang penjejakan penukaran, dan merendahkan kestabilan aplikasi hos yang dipersepsikan.
Risiko Ranap ActivityNotFoundException yang Tidak Dikendalikan Apabila Aplikasi Sasaran Tiada
Apabila pembangun pertama kali cuba menyelesaikan net::ERR_UNKNOWN_URL_SCHEME, kesilapan biasa adalah dengan menghurai rentetan URI dan memanggil context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url))) tanpa pengendalian pengecualian yang mencukupi.
Jika pengguna tidak mempunyai aplikasi sasaran dipasang pada peranti mereka, sistem pengendalian Android tidak dapat mencari Aktiviti yang layak untuk menyelesaikan Intent tersebut. Dalam pelaksanaan Android standard, memanggil startActivity() tanpa pengendali yang sepadan akan mencetuskan android.content.ActivityNotFoundException yang tidak dikendalikan. Jika pengecualian ini tidak ditangkap dalam blok try-catch yang berstruktur, keseluruhan proses aplikasi hos akan ditamatkan secara tiba-tiba, yang mengakibatkan ranap sistem yang fatal.
Bagaimana shouldOverrideUrlLoading Memintas Protokol Deep Link
Evolusi API: Membandingkan Muatan Lampau (Overload) WebResourceRequest API 24+ dengan Panggil Balik Rentetan Legasi
WebViewClient Android menyediakan panggil balik shouldOverrideUrlLoading untuk membolehkan aplikasi hos memeriksa dan memintas permintaan navigasi sebelum enjin web melaksanakannya. Tandatangan dan keupayaan kaedah ini telah berkembang merentasi tahap API Android:
- API 24+ (Android 7.0 dan lebih tinggi): Mengatasi
shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean. Antara mukaWebResourceRequestmemberikan metadata kontekstual yang kaya, termasuk URL sasaran (request.url), kaedah permintaan HTTP (request.method), pengepala permintaan, dan bendera hierarki bingkai yang kritikal (request.isForMainFrame,request.hasGesture()). - Muatan Lampau Legasi (Ditamatkan dalam API 24): Mengatasi
shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean. Panggil balik legasi ini hanya menerima rentetan URL mentah tanpa metadata mengenai bingkai yang meminta atau keadaan interaksi pengguna.
Selaras dengan Rujukan Pembangun Android pada WebViewClient shouldOverrideUrlLoading, aplikasi moden mesti melaksanakan muatan lampau WebResourceRequest. Apabila menyokong versi Android legasi, panggil balik rentetan yang ditamatkan harus dikonfigurasikan secara konservatif untuk menghalang pintasan keselamatan.
Kritikal Keselamatan request.isForMainFrame: Menyekat Hijacking Subbingkai dan Iframe

Selaras dengan Panduan Pembangun Android pada WebResourceRequest, panggil balik shouldOverrideUrlLoading boleh dicetuskan untuk subbingkai, iframe, dan pengalihan semula sumber latar belakang serta navigasi dokumen peringkat teratas.
Menguatkuasakan request.isForMainFrame adalah penting untuk keselamatan aplikasi:
- Pengasingan Sempadan Bingkai: Menyemak
request.isForMainFramememastikan subbingkai dan iframe tidak boleh mencetuskan pelancaran aplikasi luaran atau intent skema tersuai. Jika subbingkai cuba melakukan navigasi, pemuatan HTTP(S) standard harus mengembalikanfalseuntuk meneruskan perenderan dalam bingkai, manakala skema bukan HTTP(S) mesti mengembalikantrueuntuk menyekat pelaksanaan tanpa mengeluarkan ralat. - Perbezaan Gerak Isyarat Pengguna: Walaupun
isForMainFramemengesahkan bahawa permintaan tersebut terpakai pada dokumen utama, ia tidak membuktikan pengaktifan pengguna. Jika seni bina anda memerlukan pengesahan gerak isyarat pengguna, nilaikanrequest.hasGesture()atau kuatkan modal pengesahan peringkat aplikasi, dengan menyatakan bahawa dokumentasi Android menyerlahkan aliran pengguna sah tertentu di manahasGesture()mungkin mengembalikan false.
Membezakan Navigasi Web Biasa daripada Handoff Deep Link: Bila untuk Mengembalikan false lwn true
Nilai pulangan boolean bagi shouldOverrideUrlLoading mengawal kitaran hayat perenderan WebView:
- Kembalikan
false: Memberitahu rangka kerja Android bahawa aplikasi hos tidak mengendalikan URL tersebut.WebViewmeneruskan navigasi web standard, memuatkan URL di dalam bekas Chromium dalamannya. Nilai pulangan ini wajib untuk halaman webhttp://danhttps://standard yang sepatutnya dipaparkan di dalam pelayar terbenam. - Kembalikan
true: Memberitahu rangka kerja bahawa aplikasi hos telah memintas dan mengendalikan URL tersebut.WebViewserta-merta membatalkan proses pemuatan dalamannya. Nilai pulangan ini wajib apabila melancarkanIntentluaran, menghala ke aktiviti asli, atau menyekat skema yang tidak dibenarkan.
Mengendalikan Pautan Aplikasi (App Links) Hos yang Disahkan dalam Bekas Terbenam
Apabila pautan di dalam WebView menghala ke domain HTTPS aplikasi hos sendiri yang disahkan (seperti https://app.example.com/product/1024), melaksanakan navigasi lalai akan menyebabkan WebView memuatkan versi web halaman produk tersebut.
Untuk menyampaikan pengalaman asli, WebViewClient harus memeriksa URL HTTPS yang masuk. Jika hos sepadan dengan domain yang dikaitkan dengan aplikasi, klien memintas permintaan tersebut, mengesahkan parameter laluan dan pertanyaan terhadap pengesah laluan dalaman (AppLinkRouteValidator), dan menghantar data terus ke penghala asli (internalRouter(validatedRoute)), sambil mengembalikan true untuk membatalkan perenderan web yang berlebihan.
Menyusun Pengendali Intent Selamat untuk Skema Tersuai dan URI Intent
Membedah Sintaks URI Intent Chrome: Skema, Pakej, dan S.browser_fallback_url
Pada Android, halaman web kerap menggunakan URI Intent Chrome untuk menyusun deep link asli dengan mekanisme sandaran pelayar terbina dalam. URI Intent menggunakan skema intent:// dan mengekodkan parameter penghalaan, sasaran pakej, dan URL sandaran ke dalam rentetan bersiri:
intent://product/detail/1024#Intent;scheme=myapp;package=com.example.app;S.browser_fallback_url=https%3A%2F%2Fplay.google.com%2Fstore%2Fapps%2Fdetails%3Fid%3Dcom.example.app;end
Sebagaimana didokumenkan dalam Panduan Pembangun Chrome pada URI Intent Android, URI tersebut dipecahkan kepada komponen yang berbeza:
- Hos dan Laluan (
product/detail/1024): Menentukan destinasi aplikasi dalaman. scheme=myapp: Menentukan protokol tersuai yang mendasari.package=com.example.app: Mengehadkan penyelesaian intent secara eksklusif kepada pakej Android yang ditentukan.S.browser_fallback_url=...: Parameter tambahan String pilihan yang menentukan URL web atau gedung eksplisit untuk dimuatkan jika aplikasi sasaran tidak dipasang.
Bahaya Melaksanakan Intent Dihurai Mentah: Mengurangkan Kerentanan Suntikan Intent
Corak anti-pelaksanaan yang biasa ialah menghurai URI masuk dengan Intent.parseUri(url, Intent.URI_INTENT_SCHEME) dan menghantar Intent yang terhasil terus ke context.startActivity(intent).
Selaras dengan Panduan Ujian Keselamatan Aplikasi Mudah Alih OWASP pada Deep Link Tidak Selamat, melaksanakan intent dihurai mentah mendedahkan aplikasi kepada serangan Suntikan Intent. Penyerang yang mengawal halaman web boleh mencipta URI Intent yang mengandungi sasaran komponen eksplisit berniat jahat (component=com.example.app/.InternalAdminActivity), tindakan sensitif, atau tambahan intent istimewa (S.is_admin=true). WebView akan menyahsiri parameter ini dan melancarkan aktiviti yang tidak dieksport atau dalaman, sekali gus memintas halangan pengesahan.

Pembinaan Semula Intent Selamat: Melucutkan Komponen dan Mengehadkan Tindakan kepada ACTION_VIEW
Untuk menghapuskan kerentanan suntikan Intent, aplikasi hos tidak boleh melancarkan Intent yang disahsiri secara mentah secara terus. Sebaliknya, WebViewClient mesti menghurai URI, mengekstrak hanya payload data yang disahkan, dan membina semula Intent bersih yang khusus:
- Ekstrak Data: Huraikan URI dan ekstrak pakej sasaran yang disahkan serta URI data.
- Sahkan Pakej: Sahkan bahawa pakej yang diisytiharkan sepadan dengan senarai kebenaran eksplisit bagi rakan kongsi yang dibenarkan atau pakej dalaman.
- Bina Semula: Instankan
Intent(Intent.ACTION_VIEW, validatedDataUri)yang baharu. - Asingkan: Gunakan
intent.addCategory(Intent.CATEGORY_BROWSABLE)untuk memastikan hanya aktiviti yang boleh dilayari boleh menyelesaikan permintaan, tetapkan bendera eksplisit (FLAG_ACTIVITY_NEW_TASK), dan pastikanintent.componentdanintent.selectorkekal null.
Menguatkuasakan Senarai Kebenaran Skema, Hos, Laluan, dan Parameter Pertanyaan yang Ketat
Kedua-dua skema tersuai (myapp://) dan URI Intent yang dibina semula mesti melalui pengesah yang ketat sebelum pelaksanaan.
Pelaksanaan rujukan ini secara khusus mengendalikan skema myapp:// aplikasi hos sendiri, yang secara eksplisit menyematkan pelaksanaan intent kepada com.example.app. Untuk ekosistem rakan kongsi berbilang aplikasi, pembangun harus mengekalkan pemetaan skema-ke-pakej yang ketat atau mengutamakan Pautan Aplikasi HTTPS yang disahkan.
Pengesah menyemak:
- Skema: Sepadan dengan skema tersuai yang dibenarkan (
myapp). - Hos: Sepadan dengan kategori penghalaan yang telah ditetapkan (
open,product,event). - Awalan Laluan: Mematuhi struktur laluan yang dijangkakan (
/detail/,/view/,/main/). - Parameter Pertanyaan: Menguatkuasakan kunci pertanyaan yang dibenarkan (
target,id,promo,utm_source) dan mengesahkan bahawa nilai parameter mematuhi had panjang (contohnya, ≤128 aksara) dan set aksara alfanumerik.
[Pengguna Mengetik Pautan Dalam Android WebView]
│
▼
[WebViewClient.shouldOverrideUrlLoading]
│
┌─────────────┴─────────────┐
▼ ▼
[!request.isForMainFrame] [request.isForMainFrame]
│ │
▼ ▼
[Semak Skema Subbingkai] [Periksa Skema URL]
├─ HTTP(S) -> pulangkan false │
└─ Bukan-Web -> pulangkan true ┌────┴───────────────────────────┐
(Sekat Ralat) ▼ ▼
[HTTP / HTTPS] [Skema Bukan-HTTP(S)]
│ │
▼ ▼
[Semak Domain Hos] [CustomSchemeValidator]
├─ Pautan Aplikasi Dalaman ├─ Sah -> Lancarkan Intent Disemat
│ -> Penghala Dalaman └─ Intent -> Bina Semula & Sandaran
└─ Web Luaran
-> Pulangkan false
Cara Mengendalikan Keterlihatan Pakej Android 11 dan Sandaran Gedung Aplikasi

Peralihan Keterlihatan Pakej Android 11: Mengapa Pertanyaan Pra-penerbangan Memerlukan Pengisytiharan
Bermula dengan Android 11 (tahap API 30), Google memperkenalkan penapisan Keterlihatan Pakej untuk meningkatkan privasi pengguna. Di bawah rangka kerja ini, pandangan aplikasi terhadap aplikasi lain yang dipasang adalah terhad secara lalai.
Adalah kritikal untuk memahami perbezaan antara pelancaran intent dan pertanyaan intent:
- Pelancaran Intent Terus: Panggilan standard kepada
context.startActivity(intent)tidak memerlukan pengisytiharan keterlihatan pakej. Jika aplikasi sasaran dipasang, sistem pengendalian menghala dan melancarkannya dengan berjaya tidak kira sama ada ia boleh dilihat oleh pertanyaan pakej atau tidak. - Pertanyaan Intent Pra-penerbangan: Jika seni bina
WebViewClientanda melakukan pemeriksaan pra-penerbangan—seperti memanggilpackageManager.resolveActivity(intent, 0)ataupackageManager.queryIntentActivities(intent, 0)untuk menyemak kehadiran aplikasi sebelum melancarkan—pertanyaan tersebut akan mengembalikannullatau senarai kosong melainkan pakej atau skema sasaran diisytiharkan dalamAndroidManifest.xml.
Mengisytiharkan Elemen <queries> dalam AndroidManifest.xml untuk Skema Tersuai dan Sasaran Pakej
Selaras dengan Panduan Pembangun Android pada Keterlihatan Pakej dalam Android 11, aplikasi yang melakukan pemeriksaan pra-penerbangan sebelum pelaksanaan intent harus mengisytiharkan pakej sasaran atau tandatangan intent mereka dalam manifes:
<manifest package="com.example.app">
<!-- Pengisytiharan pertanyaan pra-penerbangan pilihan untuk penemuan pakej eksplisit -->
<queries>
<package android:name="com.example.partner" />
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="myapp" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="https" android:host="app.example.com" />
</intent>
</queries>
</manifest>
Jika seni bina anda lebih suka corak pelancaran standard, melaksanakan startActivity() dalam blok try-catch yang menangkap ActivityNotFoundException menyediakan pelaksanaan yang berdaya tahan dan bebas pertanyaan.
Melaksanakan Rantaian Sandaran: Penghalaan Gedung yang Anggun Apabila Aplikasi Sasaran Tiada
Apabila skema tersuai atau URI Intent yang dipintas tidak dapat dilancarkan kerana aplikasi sasaran tidak dipasang pada peranti pengguna, WebViewClient mesti melaksanakan rantaian sandaran eksplisit:
- Ekstrak dan Ikat URL Sandaran: Untuk URI
intent://, ekstrak rentetan parameterS.browser_fallback_urldan sahkan bahawa ia menghala ke destinasi yang dibenarkan. Untuk URL Google Play, sahkan bahawa parameteridsepadan dengan nama pakej sasaran (id=targetPackage) untuk menghalang eksploitasi pengalihan semula terbuka. - Penjanaan URI Pasaran: Jika tiada URL sandaran hadir, bina intent pasaran:
market://details?id=com.example.app. - Sandaran Web Play Store: Jika Google Play dinyahdaya atau tiada, tangkap
ActivityNotFoundExceptionpada intent pasaran dan muatkanhttps://play.google.com/store/apps/details?id=com.example.app.
Mengikat Parameter Kontekstual Merentasi Pengalihan Semula Sandaran dengan OpoInstall
Apabila pengguna yang tidak memasang aplikasi dihalakan ke Google Play, pautan gedung statik akan menggugurkan parameter dinamik. OpoInstall, platform atribusi mudah alih dan deep linking, merapatkan jurang ini dengan menangkap token rujukan dan konteks kempen pada bahagian belakang atribusi sebelum pengalihan semula gedung. Sahkan gelagat pengambilan parameter tertunda dan API permulaan terhadap versi SDK Android OpoInstall yang digunakan dalam pengeluaran.
Selepas pemasangan seterusnya dan pelancaran aplikasi awal, SDK Android OpoInstall akan mendapatkan semula parameter yang dicache, yang membolehkan aplikasi hos memulihkan adegan sasaran pengguna. Semak dokumentasi penyepaduan SDK untuk arahan mengenai cara menerima parameter tertunda selepas pemasangan.
Pelaksanaan WebViewClient Pengeluaran untuk Pemintasan Skema yang Selamat
Menyusun Seni Bina WebViewClient Tersuai dalam Kotlin
WebViewClient pengeluaran menyelaraskan pengesahan bingkai, penyenaraian skema yang dibenarkan, pembinaan semula intent, pengekstrakan sandaran, dan penghalaan dalaman. Pelaksanaan ini merangkumkan peraturan pengesahan dalam objek pengesah khusus (AppLinkRouteValidator dan CustomSchemeValidator) untuk memastikan panggil balik navigasi bersih dan boleh diselenggara.
Melaksanakan Model AppLinkRouteValidator dan CustomSchemeValidator
Objek pengesah menguatkuasakan keselamatan gagal-tutup (fail-closed) yang ketat:
AppLinkRouteValidatormengesahkan domain HTTPS dalaman, awalan laluan yang dibenarkan, dan kunci pertanyaan yang dibenarkan (target,id,promo,utm_source), mengembalikan kelas dataValidatedAppRouteyang berstruktur.CustomSchemeValidatormenyemak skema tersuai yang dibenarkan (myapp), mengesahkan hos yang dibenarkan (open,product,event), menguatkuasakan awalan laluan, dan membersihkan nilai parameter pertanyaan terhadap set aksara alfanumerik serta had panjang (≤128 aksara).
Pelaksanaan kod di bawah menunjukkan cara mengkonfigurasi pengisytiharan keterlihatan pakej AndroidManifest.xml pilihan dan melaksanakan CustomWebViewClient yang selamat dan selamat-bebenang dalam Kotlin dengan pengendalian sandaran automatik. Binari SDK yang disahkan dan pembungkus penyepaduan boleh diperolehi daripada pusat muat turun SDK OpoInstall.
<!-- AndroidManifest.xml: Pengisytiharan Keterlihatan Pakej Pilihan untuk Pertanyaan Intent Pra-penerbangan (Android 11+) -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.app">
<!-- Isytiharkan pakej luaran dan skema tersuai hanya jika menggunakan pertanyaan resolveActivity pra-penerbangan -->
<queries>
<!-- Benarkan penemuan pra-penerbangan untuk rakan kongsi atau pakej sasaran tertentu -->
<package android:name="com.example.partner" />
<!-- Benarkan penyelesaian pra-penerbangan skema deep linking tersuai -->
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="myapp" />
</intent>
<!-- Benarkan penyelesaian pra-penerbangan domain Pautan Aplikasi HTTPS yang disahkan -->
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="https" android:host="app.example.com" />
</intent>
</queries>
<application
android:allowBackup="true"
android:label="@string/app_name"
android:theme="@style/Theme.AppCompat.Light.NoActionBar">
<!-- Komponen aplikasi -->
</application>
</manifest>
// Android: WebViewClient Tersuai dengan Keselamatan Sempadan Bingkai, Pengesahan Senarai Kebenaran, dan Penghalaan Sandaran
// Contoh penyepaduan rujukan; sahkan tandatangan kaedah terhadap seni bina Android yang digunakan.
package com.example.app.webview
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.util.Log
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
data class ValidatedAppRoute(val path: String, val queryParams: Map<String, String>)
object AppLinkRouteValidator {
private val allowedPrefixes = setOf("/open/", "/product/", "/promo/")
private val allowedQueryKeys = setOf("target", "id", "promo", "utm_source")
fun validate(uri: Uri): ValidatedAppRoute? {
val path = uri.path ?: ""
if (allowedPrefixes.none { path.startsWith(it) }) {
return null
}
val sanitizedParams = mutableMapOf<String, String>()
for (name in uri.queryParameterNames) {
// Postur keselamatan ketat: tolak permintaan jika kunci pertanyaan tidak diketahui wujud
if (name !in allowedQueryKeys) {
return null
}
val value = uri.getQueryParameter(name) ?: ""
if (value.length <= 64 && value.matches(Regex("^[A-Za-z0-9_-]+$"))) {
sanitizedParams[name] = value
} else {
return null
}
}
return ValidatedAppRoute(path, sanitizedParams)
}
}
object CustomSchemeValidator {
private val allowedSchemes = setOf("myapp")
private val allowedHosts = setOf("open", "product", "event")
private val allowedPathPrefixes = setOf("/detail/", "/view/", "/main/")
private val allowedQueryKeys = setOf("target", "id", "promo", "utm_source")
fun validate(uri: Uri): Uri? {
val scheme = uri.scheme?.lowercase() ?: return null
val host = uri.host?.lowercase() ?: return null
val path = uri.path ?: ""
if (!allowedSchemes.contains(scheme) || !allowedHosts.contains(host)) {
return null
}
if (allowedPathPrefixes.none { path.startsWith(it) }) {
return null
}
for (name in uri.queryParameterNames) {
if (name !in allowedQueryKeys) return null
val value = uri.getQueryParameter(name) ?: ""
if (value.length > 128 || !value.matches(Regex("^[A-Za-z0-9_-]+$"))) {
return null
}
}
return uri
}
}
class CustomWebViewClient(
private val context: Context,
private val internalRouter: (ValidatedAppRoute) -> Unit
) : WebViewClient() {
override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
request ?: return false
// Keperluan keselamatan: Hadkan penghalaan aplikasi luaran kepada navigasi bingkai utama sahaja
if (!request.isForMainFrame) {
val scheme = request.url.scheme?.lowercase()
// Benarkan pemuatan subbingkai HTTP(S) standard, tetapi sekat skema bukan web untuk mencegah hijacking iframe & ralat
return if (scheme == "http" || scheme == "https") {
false
} else {
Log.w("WebViewClient", "Menyekat skema bukan web dalam subbingkai/iframe: $scheme")
true // Gunakan/batalkan tanpa melancarkan intent luaran atau memaparkan ralat Chromium
}
}
val url = request.url.toString()
return handleUrlNavigation(view, url)
}
@Deprecated("Ditamatkan dalam Java")
override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean {
if (url == null) return false
val uri = Uri.parse(url)
val scheme = uri.scheme?.lowercase()
// Panggil balik legasi tidak mempunyai metadata isForMainFrame yang boleh dipercayai; jangan halakan skema luaran
return if (scheme == "http" || scheme == "https") {
false // Benarkan pemuatan kandungan web standard
} else {
Log.w("WebViewClient", "Menyekat skema luaran pada panggil balik legasi: $scheme")
true // Sekat ralat skema tidak dikendalikan tanpa melancarkan intent luaran
}
}
private fun handleUrlNavigation(view: WebView?, url: String): Boolean {
val uri = Uri.parse(url)
val scheme = uri.scheme?.lowercase() ?: return false
// 1. Kendalikan HTTP / HTTPS dan Pautan Aplikasi Dalaman
if (scheme == "http" || scheme == "https") {
// Semak sama ada URL sepadan dengan domain Pautan Aplikasi apl hos sendiri
if (scheme == "https" && uri.host?.lowercase() == "app.example.com") {
val validatedRoute = AppLinkRouteValidator.validate(uri)
if (validatedRoute != null) {
Log.i("WebViewClient", "Laluan HTTPS dalaman disahkan. Menghantar ke penghala dalaman.")
internalRouter(validatedRoute)
} else {
Log.w("WebViewClient", "Menolak Pautan Aplikasi dalaman yang salah bentuk atau tidak dibenarkan: $uri")
}
return true // Dikendalikan secara dalaman; batalkan pemuatan webview
}
return false // Benarkan WebView memuatkan kandungan web standard secara asli
}
// 2. Sahkan skema tersuai dengan senarai kebenaran yang ketat dan penyematan pakej untuk mengelakkan hijacking
val validatedUri = CustomSchemeValidator.validate(uri)
if (validatedUri != null) {
val pinnedIntent = Intent(Intent.ACTION_VIEW, validatedUri).apply {
`package` = "com.example.app" // Sematkan pakej apl hos secara eksplisit
addCategory(Intent.CATEGORY_BROWSABLE)
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
return launchSafeIntent(pinnedIntent, fallbackAction = {
handleStoreFallback(view, "com.example.app")
})
}
// 3. Kendalikan URI Intent Chrome dengan pembinaan semula selamat dan pengekstrakan sandaran
if (scheme == "intent") {
return try {
val parsed = Intent.parseUri(url, Intent.URI_INTENT_SCHEME)
val targetPackage = parsed.`package`
val allowedPackages = setOf("com.example.app", "com.example.partner")
val fallbackUrl = parsed.getStringExtra("browser_fallback_url")
if (targetPackage != null && allowedPackages.contains(targetPackage)) {
val rawDataUri = parsed.data
if (rawDataUri != null) {
val validatedDataUri = CustomSchemeValidator.validate(rawDataUri)
if (validatedDataUri != null) {
// Bina semula Intent ACTION_VIEW yang bersih dan khusus dan bukannya melaksanakan objek dihurai mentah
val safeIntent = Intent(Intent.ACTION_VIEW, validatedDataUri).apply {
`package` = targetPackage
addCategory(Intent.CATEGORY_BROWSABLE)
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
launchSafeIntent(safeIntent, fallbackAction = {
handleIntentFallback(view, fallbackUrl, targetPackage)
})
} else {
Log.w("WebViewClient", "Menolak intent dengan URI data tidak sah: $rawDataUri")
true
}
} else {
Log.w("WebViewClient", "Menolak intent yang kekurangan payload URI data")
true
}
} else {
Log.w("WebViewClient", "Menolak intent yang menyasarkan pakej tidak disahkan: $targetPackage")
true // Batalkan intent pakej tidak disahkan
}
} catch (e: Exception) {
Log.e("WebViewClient", "Gagal menghurai URI intent: $url", e)
true // Sekat ralat skema tidak dikendalikan
}
}
// Tolak skema tidak diketahui untuk mengelakkan ralat net::ERR_UNKNOWN_URL_SCHEME
Log.w("WebViewClient", "Menyekat navigasi skema tidak disahkan: $scheme")
return true
}
private fun launchSafeIntent(intent: Intent, fallbackAction: () -> Unit): Boolean {
return try {
context.startActivity(intent)
true
} catch (e: ActivityNotFoundException) {
Log.w("WebViewClient", "Aplikasi sasaran tidak dipasang: ${intent.data}. Mencetuskan sandaran.")
fallbackAction()
true
} catch (e: SecurityException) {
Log.e("WebViewClient", "Pengecualian keselamatan semasa melancarkan intent: ${intent.data}", e)
true
}
}
private fun handleIntentFallback(view: WebView?, fallbackUrl: String?, targetPackage: String) {
if (!fallbackUrl.isNullOrBlank() && isAuthorizedFallbackUrl(fallbackUrl, targetPackage)) {
Log.i("WebViewClient", "Menghala ke URL sandaran pelayar yang dibenarkan: $fallbackUrl")
view?.loadUrl(fallbackUrl)
} else {
handleStoreFallback(view, targetPackage)
}
}
private fun handleStoreFallback(view: WebView?, packageName: String) {
val marketUri = Uri.parse("market://details?id=$packageName")
val marketIntent = Intent(Intent.ACTION_VIEW, marketUri).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
try {
context.startActivity(marketIntent)
} catch (e: ActivityNotFoundException) {
val webStoreUrl = "https://play.google.com/store/apps/details?id=$packageName"
Log.i("WebViewClient", "Aplikasi Play Store tiada. Memuatkan URL gedung web: $webStoreUrl")
view?.loadUrl(webStoreUrl)
}
}
private fun isAuthorizedFallbackUrl(url: String, targetPackage: String): Boolean {
val uri = Uri.parse(url)
val scheme = uri.scheme?.lowercase()
val host = uri.host?.lowercase()
if (scheme != "https" || host == null) return false
return when (host) {
"play.google.com" -> {
// Menguatkuasakan bahawa URL sandaran Google Play menghala secara khusus kepada pakej sasaran
uri.path == "/store/apps/details" && uri.getQueryParameter("id") == targetPackage
}
"app.example.com", "www.example.com" -> {
// Mengesahkan laluan sandaran domain milik sendiri
AppLinkRouteValidator.validate(uri) != null
}
else -> false
}
}
}
Pelaksanaan Selamat-Bebenang: Menghantar Laluan Dalaman dan Menguruskan Keadaan Kitaran Hayat
Apabila AppLinkRouteValidator mengesahkan laluan dalaman yang sah, peralihan UI mesti dilaksanakan pada utas UI utama Android. Jika internalRouter mencetuskan fragmen, peralihan aktiviti, atau navigasi Compose, bungkus panggilan penghantaran dalam runOnUiThread atau korutin kitaran hayat utas-utama untuk mengelakkan ranap sistem akibat utas.
Matriks Penyelesaian Ralat dan Diagnostik Pengalihan Semula Android WebView
Panduan Penyelesaian Masalah Deep Linking Android WebView yang Komprehensif
Matriks di bawah memperincikan tandatangan ralat biasa yang ditemui apabila mengendalikan skema tersuai dalam WebView Android, bersama-sama punca utama dan pemulihan kejuruteraan:
| Tandatangan Ralat / Gejala | Punca Utama | Tahap API Terjejas | Titik Semak Diagnostik | Pemulihan Disyorkan |
|---|---|---|---|---|
net::ERR_UNKNOWN_URL_SCHEME |
Skema bukan web tidak dikendalikan dalam pemuat WebView | Semua tahap API | shouldOverrideUrlLoading hilang atau mengembalikan false |
Atasi delegat, pintas skema, kembalikan true |
ActivityNotFoundException |
Apl sasaran tiada pada peranti | Semua tahap API | context.startActivity() tidak dikendalikan |
Tangkap pengecualian dan laksanakan S.browser_fallback_url atau laluan Play Store |
| Hijacking Intent Iframe | Subbingkai mencetuskan intent luaran | API 24+ | request.isForMainFrame tidak disemak |
Kawal dengan if (!request.isForMainFrame) dan sekat skema bukan web |
| Pertanyaan Pra-penerbangan Mengembalikan Null | Penapis keterlihatan pakej Android 11+ | API 30+ (Android 11+) | Memanggil resolveActivity tanpa pengisytiharan manifes |
Isytiharkan <queries> dalam manifes atau gunakan startActivity try-catch secara terus |
| Gelung Muat Semula Pautan Aplikasi Dalaman | Pautan Aplikasi Hos dihalakan semula ke WebView | Semua tahap API | Domain hos tidak dikendalikan dalam delegat | Pintas domain sendiri, sahkan laluan, hantar ke penghala asli |
Soalan Lazim (FAQ)
Mengapa Android WebView saya ranap apabila membuka skema URL tersuai?
Bagaimanakah saya boleh mengendalikan URL intent:// dalam WebView dengan selamat tanpa kerentanan keselamatan?
Bagaimanakah keterlihatan pakej Android 11 menjejaskan pemintasan deep link dalam WebView?
Ringkasan dan Rangka Kerja Keputusan
Mengendalikan deep linking dan skema tersuai dalam WebView Android memerlukan merapatkan jurang antara kotak pasir perenderan web dan penghalaan intent sistem pengendalian asli. Bergantung pada gelagat WebView lalai akan mengakibatkan ralat protokol yang tidak dikendalikan, pengalaman pengguna yang lemah, dan potensi kerentanan keselamatan.
Dengan melaksanakan WebViewClient yang mantap yang mengesahkan sempadan bingkai, mengesahkan skema tersuai terhadap senarai kebenaran eksplisit, membina semula intent ACTION_VIEW yang selamat, dan mengendalikan sandaran gedung yang anggun, pasukan kejuruteraan mengurangkan kegagalan pengendalian skema WebView dan menyokong navigasi yang lebih berdaya tahan. Menyepadukan penghalaan sisi-klien yang boleh dipercayai dengan rangka kerja atribusi mudah alih yang komprehensif mengurangkan geseran penghalaan merentas aliran atribusi dan menyokong perjalanan pengguna yang lebih konsisten.
Untuk meneroka seni bina deep linking Android asli dan penghalaan parameter, rujuk dokumentasi penyepaduan SDK, muat turun pustaka klien daripada pusat muat turun SDK OpoInstall, teroka rujukan pelaksanaan atribusi mudah alih, atau daftarkan aplikasi anda pada konsol pembangun OpoInstall.
Bahan Berkaitan
- Konsep: Navigasi WebView Android, Pemintasan Skema URL Tersuai, Pembinaan Semula URI Intent, Keterlihatan Pakej
- Teknologi: WebViewClient Android, Enjin Web Chromium, Android Debug Bridge, SDK Android OpoInstall
- Piawaian: IETF RFC 3986 Uniform Resource Identifier, Spesifikasi Intent Android, Panduan Ujian Keselamatan Aplikasi Mudah Alih OWASP (MASTG)
- API:
shouldOverrideUrlLoadingAndroid,WebResourceRequest.isForMainFrame,Intent.parseUri,PackageManager.queryIntentActivities - Dokumentasi Rasmi & Rujukan:
- Rujukan Pembangun Android pada WebViewClient shouldOverrideUrlLoading
- Panduan Pembangun Android pada WebResourceRequest
- Panduan Pembangun Android pada Keterlihatan Pakej dalam Android 11
- Panduan Pembangun Chrome pada URI Intent Android
- Panduan Ujian Keselamatan Aplikasi Mudah Alih OWASP pada Deep Link Tidak Selamat
Share this article



