Cara Memintas Skema URL Tersuai dalam Ubah Haluan Android WebView

opoinstall
2026-09-21
5 min read

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

Skema tersuai mesti meninggalkan saluran pemuatan WebView sebelum Chromium gagal.

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:

  1. Pencetus Pengguna: Pengguna mengetik butang seruan tindak (CTA) atau hiperpautan di dalam halaman pendaratan H5 terbenam.
  2. Permintaan Navigasi: Enjin web membina permintaan navigasi yang mengandungi rentetan URI sasaran (contohnya, myapp://promo/summer).
  3. Penilaian Saluran: Jika shouldOverrideUrlLoading() mengembalikan false atau gagal memintas URI bukan HTTP(S), enjin Chromium menganggap URL tersebut sebagai pemuatan sumber web standard.
  4. 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 muka WebResourceRequest memberikan 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

Pemeriksaan bingkai utama menghalang navigasi iframe daripada melancarkan laluan aplikasi luaran.

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.isForMainFrame memastikan subbingkai dan iframe tidak boleh mencetuskan pelancaran aplikasi luaran atau intent skema tersuai. Jika subbingkai cuba melakukan navigasi, pemuatan HTTP(S) standard harus mengembalikan false untuk meneruskan perenderan dalam bingkai, manakala skema bukan HTTP(S) mesti mengembalikan true untuk menyekat pelaksanaan tanpa mengeluarkan ralat.
  • Perbezaan Gerak Isyarat Pengguna: Walaupun isForMainFrame mengesahkan bahawa permintaan tersebut terpakai pada dokumen utama, ia tidak membuktikan pengaktifan pengguna. Jika seni bina anda memerlukan pengesahan gerak isyarat pengguna, nilaikan request.hasGesture() atau kuatkan modal pengesahan peringkat aplikasi, dengan menyatakan bahawa dokumentasi Android menyerlahkan aliran pengguna sah tertentu di mana hasGesture() 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. WebView meneruskan navigasi web standard, memuatkan URL di dalam bekas Chromium dalamannya. Nilai pulangan ini wajib untuk halaman web http:// dan https:// standard yang sepatutnya dipaparkan di dalam pelayar terbenam.
  • Kembalikan true: Memberitahu rangka kerja bahawa aplikasi hos telah memintas dan mengendalikan URL tersebut. WebView serta-merta membatalkan proses pemuatan dalamannya. Nilai pulangan ini wajib apabila melancarkan Intent luaran, 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.

URI intent yang dihurai mesti disahkan dan dibina semula sebelum penghantaran Android.

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:

  1. Ekstrak Data: Huraikan URI dan ekstrak pakej sasaran yang disahkan serta URI data.
  2. Sahkan Pakej: Sahkan bahawa pakej yang diisytiharkan sepadan dengan senarai kebenaran eksplisit bagi rakan kongsi yang dibenarkan atau pakej dalaman.
  3. Bina Semula: Instankan Intent(Intent.ACTION_VIEW, validatedDataUri) yang baharu.
  4. 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 pastikan intent.component dan intent.selector kekal 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

Keterlihatan pakej menjejaskan pertanyaan pra-penerbangan manakala kegagalan pelancaran menggunakan sandaran yang disahkan.

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 WebViewClient anda melakukan pemeriksaan pra-penerbangan—seperti memanggil packageManager.resolveActivity(intent, 0) atau packageManager.queryIntentActivities(intent, 0) untuk menyemak kehadiran aplikasi sebelum melancarkan—pertanyaan tersebut akan mengembalikan null atau senarai kosong melainkan pakej atau skema sasaran diisytiharkan dalam AndroidManifest.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:

  1. Ekstrak dan Ikat URL Sandaran: Untuk URI intent://, ekstrak rentetan parameter S.browser_fallback_url dan sahkan bahawa ia menghala ke destinasi yang dibenarkan. Untuk URL Google Play, sahkan bahawa parameter id sepadan dengan nama pakej sasaran (id=targetPackage) untuk menghalang eksploitasi pengalihan semula terbuka.
  2. Penjanaan URI Pasaran: Jika tiada URL sandaran hadir, bina intent pasaran: market://details?id=com.example.app.
  3. Sandaran Web Play Store: Jika Google Play dinyahdaya atau tiada, tangkap ActivityNotFoundException pada intent pasaran dan muatkan https://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:

  • AppLinkRouteValidator mengesahkan domain HTTPS dalaman, awalan laluan yang dibenarkan, dan kunci pertanyaan yang dibenarkan (target, id, promo, utm_source), mengembalikan kelas data ValidatedAppRoute yang berstruktur.
  • CustomSchemeValidator menyemak 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?
Skema tersuai menghasilkan `net::ERR_UNKNOWN_URL_SCHEME` apabila URL kekal dalam saluran pemuatan WebView dan bukannya dipintas oleh aplikasi hos. Dengan <code>WebViewClient</code> tersuai, kembalikan <code>true</code> selepas mengendalikan atau menyekat skema bukan web dengan selamat; kembalikan <code>false</code> hanya apabila WebView sepatutnya meneruskan navigasi web biasa. Jika aplikasi sasaran tiada, memanggil <code>startActivity()</code> tanpa blok <code>try-catch</code> akan mencetuskan <code>ActivityNotFoundException</code> yang tidak dikendalikan.
Bagaimanakah saya boleh mengendalikan URL intent:// dalam WebView dengan selamat tanpa kerentanan keselamatan?
Untuk mengendalikan URI <code>intent://</code> dengan selamat, huraikan URI menggunakan <code>Intent.parseUri(url, Intent.URI_INTENT_SCHEME)</code>, sahkan bahawa pakej sasaran dan URI data sepadan dengan senarai kebenaran yang ketat, dan bina <code>Intent(Intent.ACTION_VIEW, safeDataUri)</code> baharu yang bersih. Tambahkan <code>Intent.CATEGORY_BROWSABLE</code> secara eksplisit untuk memastikan hanya aktiviti yang boleh dilayari mengendalikan intent tersebut, lucutkan komponen berbahaya, dan laksanakan pengendalian URL sandaran yang disahkan yang diikat pada pakej sasaran.
Bagaimanakah keterlihatan pakej Android 11 menjejaskan pemintasan deep link dalam WebView?
Keterlihatan pakej Android 11 tidak menghalang <code>startActivity()</code> daripada melancarkan aktiviti luaran yang sepadan. Walau bagaimanapun, jika <code>WebViewClient</code> anda melakukan pemeriksaan pra-penerbangan menggunakan <code>resolveActivity()</code> atau <code>queryIntentActivities()</code> sebelum cuba melancarkan, anda mesti mengisytiharkan nama pakej sasaran atau skema intent tersuai dalam elemen <code>&lt;queries&gt;</code> dalam fail <code>AndroidManifest.xml</code> anda.

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

Share this article