จะจัดการ deep linking และ custom scheme ใน Android WebView ได้อย่างไร? การจัดการ deep link ใน Android WebView จำเป็นต้องขยายคลาส WebViewClient, ทำการ override เมธอด shouldOverrideUrlLoading เพื่อตรวจสอบ isForMainFrame, ตรวจสอบ custom scheme กับรายการที่อนุญาต (allowlist), และสั่งเปิด intent ภายนอกที่ได้รับอนุญาตอย่างปลอดภัย เพื่อป้องกันไม่ให้ scheme ที่ไม่ใช่เว็บค้างอยู่ในกระบวนการโหลดของ WebView
การดักจับ deep link ใน Android WebView คือการเขียนโปรแกรมเพื่อ override callback ของการนำทางภายใน WebViewClient ที่กำหนดเอง เพื่อจับโปรโตคอลที่ไม่ใช่ HTTP เช่น custom URI scheme และ Chrome Intent URI การประเมินข้อมูลเมตาของคำขอและการตรวจสอบความถูกต้องของปลายทางจะช่วยให้แอปพลิเคชันหลักเปิดใช้งาน Native Intent ได้อย่างราบรื่น ในขณะที่ป้องกันการเกิดข้อผิดพลาดจากโปรโตคอลที่ไม่ได้รับการจัดการ
| คำศัพท์ | คำนิยาม | เอนทิตีที่เกี่ยวข้อง | บทบาทของ Search Intent |
|---|---|---|---|
| WebView | คอมโพเนนต์มุมมองที่ใช้ Chromium เป็นพื้นฐานในการแสดงผลเนื้อหาเว็บภายในแอป Android | Android SDK | ข้อมูล / เชิงพาณิชย์ |
| Custom URL Scheme | โปรโตคอล URI เฉพาะที่อนุญาตให้ลิงก์เว็บเชื่อมต่อไปยังแอปพลิเคชันเนทีฟได้ | การกำหนดเส้นทาง Deep Link | เทคนิค / ข้อมูล |
| Web to App | กระบวนการนำผู้ใช้เปลี่ยนผ่านจากหน้าเว็บไปยังอินเทอร์เฟซของแอปพลิเคชันเนทีฟ | Mobile Deep Linking | ข้อมูล |

สาเหตุที่ Android WebView แสดงข้อผิดพลาด net ERR UNKNOWN URL SCHEME
ข้อจำกัดของโปรโตคอล Chromium: เหตุใด WebView จึงจดจำเฉพาะมาตรฐานเว็บทั่วไปเท่านั้น
คอมโพเนนต์ WebView ของ Android ขับเคลื่อนโดยเอ็นจิ้นการแสดงผล Chromium ซึ่งถูกกำหนดค่าให้แยกวิเคราะห์และแสดงผลโปรโตคอลทรัพยากรเว็บมาตรฐานเป็นหลัก ได้แก่: http://, https://, file://, data://, about:, และ javascript:
โดยปกติแล้ว เมื่อไม่มีการกำหนด WebViewClient แบบกำหนดเอง เฟรมเวิร์กของ Android จะส่งต่อ URL ที่ไม่ได้จัดการไปยัง Activity Manager ของระบบ อย่างไรก็ตาม เมื่อแอปพลิเคชันมีการระบุ WebViewClient แบบกำหนดเอง Scheme ที่ไม่ใช่ HTTP(S) (เช่น myapp://product/detail/1024, alipay://, หรือ whatsapp://) จะถูกส่งตรงไปยัง callback การนำทางของไคลเอ็นต์ หากแอปพลิเคชันปล่อยให้ custom scheme ค้างอยู่ในกระบวนการโหลดของ WebView แทนที่จะทำการดักจับ เอ็นจิ้น Chromium จะพยายามจัดการ custom scheme นั้นเหมือนทรัพยากรเว็บทั่วไป เนื่องจากโปรโตคอลที่ไม่ใช่มาตรฐานไม่สามารถแก้ไขผ่าน DNS หรือเลเยอร์การขนส่งของเซิร์ฟเวอร์เว็บได้ เอ็นจิ้นจึงแสดงข้อผิดพลาดการนำทางระดับต่ำออกมา
กายวิภาคของความผิดพลาด net::ERR_UNKNOWN_URL_SCHEME: จากการกดของผู้ใช้สู่ความล้มเหลวในการโหลดทรัพยากรเว็บ
การเปลี่ยนจากปฏิสัมพันธ์ของผู้ใช้ไปสู่หน้าข้อผิดพลาดเกิดขึ้นตามลำดับที่ชัดเจน:
- User Trigger: ผู้ใช้กดปุ่ม Call-to-action หรือไฮเปอร์ลิงก์ภายในหน้า Landing Page แบบ H5 ที่ฝังอยู่
- Navigation Request: เอ็นจิ้นเว็บสร้างคำขอการนำทางที่มีสตริง URI ปลายทาง (เช่น
myapp://promo/summer) - Pipeline Evaluation: หาก
shouldOverrideUrlLoading()คืนค่าเป็นfalseหรือไม่สามารถดักจับ URI ที่ไม่ใช่ HTTP(S) ได้ เอ็นจิ้น Chromium จะถือว่า URL นั้นเป็นโหลดทรัพยากรเว็บมาตรฐาน - Error Dispatch: เมื่อไม่พบตัวจัดการสำหรับโปรโตคอลนั้น เอ็นจิ้นจะทริกเกอร์
onReceivedError()และแทนที่เนื้อหาของ WebView ด้วยเทมเพลตข้อผิดพลาดเริ่มต้นของ Chromium:net::ERR_UNKNOWN_URL_SCHEME
หน้าข้อผิดพลาดนี้หยุดประสบการณ์ใช้งานของผู้ใช้ ป้องกันการติดตามคอนเวอร์ชัน และลดความเชื่อมั่นในเสถียรภาพของแอปพลิเคชันลง
ความเสี่ยงของข้อผิดพลาด ActivityNotFoundException เมื่อไม่มีแอปพลิเคชันเป้าหมาย
เมื่อนักพัฒนาพยายามแก้ไข net::ERR_UNKNOWN_URL_SCHEME เป็นครั้งแรก ข้อผิดพลาดทั่วไปคือการแยกสตริง URI แล้วเรียกใช้ context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url))) โดยไม่มีการจัดการข้อยกเว้นที่เหมาะสม
หากผู้ใช้ไม่ได้ติดตั้งแอปพลิเคชันเป้าหมายไว้ในอุปกรณ์ ระบบปฏิบัติการ Android จะไม่พบ Activity ที่เหมาะสมในการแก้ไข Intent นั้น ในการทำงานมาตรฐานของ Android การเรียก startActivity() โดยไม่มีตัวจัดการที่ตรงกันจะทำให้เกิด android.content.ActivityNotFoundException ที่ไม่ได้จัดการ หากข้อยกเว้นนี้ไม่ถูกดักจับภายในบล็อก try-catch ที่มีโครงสร้าง แอปพลิเคชันทั้งหมดจะปิดตัวลงทันที ส่งผลให้เกิดแอปแครช
shouldOverrideUrlLoading ดักจับโปรโตคอล Deep Link อย่างไร
วิวัฒนาการของ API: การเปรียบเทียบการ Overload WebResourceRequest ใน API 24+ กับ Legacy String Callbacks
WebViewClient ของ Android มี callback shouldOverrideUrlLoading เพื่อให้แอปพลิเคชันสามารถตรวจสอบและดักจับคำขอการนำทางก่อนที่เอ็นจิ้นเว็บจะดำเนินการ ซึ่งลายเซ็นและความสามารถของเมธอดนี้มีการพัฒนามาตามระดับ API ของ Android:
- API 24+ (Android 7.0 ขึ้นไป): Overrides
shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Booleanโดยอินเทอร์เฟซWebResourceRequestจะให้ข้อมูลเมตาบริบทที่สมบูรณ์ รวมถึง URL เป้าหมาย (request.url), วิธีการขอ HTTP (request.method), เฮดเดอร์ของคำขอ, และสถานะการลำดับชั้นของเฟรม (request.isForMainFrame,request.hasGesture()) - Legacy Overload (เลิกใช้งานใน API 24): Overrides
shouldOverrideUrlLoading(view: WebView?, url: String?): Booleanโดย callback แบบดั้งเดิมนี้จะรับเฉพาะสตริง URL ดิบโดยไม่มีข้อมูลเมตาเกี่ยวกับเฟรมที่ขอหรือสถานะการโต้ตอบของผู้ใช้
เพื่อให้สอดคล้องกับ Android Developers Reference บน WebViewClient shouldOverrideUrlLoading แอปพลิเคชันสมัยใหม่ต้องใช้การ Overload แบบ WebResourceRequest และในการรองรับ Android เวอร์ชันเก่า ควรตั้งค่า callback แบบสตริงแบบอนุรักษ์นิยมเพื่อป้องกันการบายพาสความปลอดภัย
ความสำคัญด้านความปลอดภัยของ request.isForMainFrame: การบล็อกการจี้ Subframe และ Iframe

เพื่อให้สอดคล้องกับ Android Developers Guide บน WebResourceRequest callback shouldOverrideUrlLoading สามารถถูกทริกเกอร์สำหรับ subframe, iframe, และการเปลี่ยนเส้นทางทรัพยากรเบื้องหลัง รวมถึงการนำทางเอกสารระดับบนสุด
การบังคับใช้ request.isForMainFrame เป็นสิ่งจำเป็นสำหรับความปลอดภัยของแอปพลิเคชัน:
- การแยกขอบเขตเฟรม (Frame Boundary Isolation): การตรวจสอบ
request.isForMainFrameช่วยให้มั่นใจได้ว่า subframe และ iframe ไม่สามารถทริกเกอร์การเปิดแอปพลิเคชันภายนอกหรือ intent แบบ custom scheme ได้ หาก subframe พยายามนำทาง การโหลด HTTP(S) มาตรฐานควรคืนค่าfalseเพื่อให้แสดงผลในเฟรมต่อไป ในขณะที่ scheme ที่ไม่ใช่ HTTP(S) ต้องคืนค่าtrueเพื่อระงับการทำงานโดยไม่ทำให้เกิดข้อผิดพลาด - การจำแนกท่าทางของผู้ใช้ (User Gesture Distinction): แม้
isForMainFrameจะยืนยันว่าคำขอนั้นใช้กับเอกสารหลัก แต่ก็ไม่ได้พิสูจน์ถึงการกระตุ้นโดยผู้ใช้ หากสถาปัตยกรรมของคุณต้องการการยืนยันท่าทางของผู้ใช้ ให้ประเมินrequest.hasGesture()หรือบังคับใช้โมดัลยืนยันระดับแอปพลิเคชัน โดยสังเกตว่าเอกสารของ Android เน้นย้ำบางโฟลว์การใช้งานจริงที่hasGesture()อาจคืนค่า false
การแยกความแตกต่างระหว่างการนำทางเว็บปกติกับ Deep Link Handoff: เมื่อใดควรคืนค่า false เทียบกับ true
ค่าส่งคืนแบบบูลีนของ shouldOverrideUrlLoading จะควบคุมวงจรการแสดงผลของ WebView:
- คืนค่า
false: แจ้งเฟรมเวิร์ก Android ว่าแอปพลิเคชัน ไม่ได้ จัดการ URL นั้นWebViewจะทำการนำทางเว็บตามมาตรฐานโดยโหลด URL ภายในคอนเทนเนอร์ Chromium ของตัวเอง ค่าส่งคืนนี้เป็นข้อบังคับสำหรับหน้าเว็บhttp://และhttps://มาตรฐานที่ควรแสดงผลภายในเบราว์เซอร์ที่ฝังอยู่ - คืนค่า
true: แจ้งเฟรมเวิร์กว่าแอปพลิเคชัน ได้ดักจับและจัดการ URL นั้นแล้วWebViewจะยกเลิกกระบวนการโหลดภายในทันที ค่าส่งคืนนี้เป็นข้อบังคับเมื่อมีการเปิดIntentภายนอก, การกำหนดเส้นทางไปยัง native activity, หรือการระงับ scheme ที่ไม่ได้รับอนุญาต
การจัดการ App Links ของแอปพลิเคชันที่ผ่านการยืนยันภายในคอนเทนเนอร์ที่ฝังอยู่
เมื่อลิงก์ภายใน WebView ชี้ไปยังโดเมน HTTPS ที่ผ่านการยืนยันของแอปพลิเคชันเอง (เช่น https://app.example.com/product/1024) การเรียกใช้การนำทางเริ่มต้นจะทำให้ WebView โหลดเวอร์ชันเว็บของหน้าผลิตภัณฑ์นั้น
เพื่อมอบประสบการณ์แบบเนทีฟ WebViewClient ควรตรวจสอบ URL HTTPS ที่เข้ามา หากโฮสต์ตรงกับโดเมนที่เกี่ยวข้องของแอปพลิเคชัน ไคลเอ็นต์จะทำการดักจับคำขอ ตรวจสอบเส้นทางและพารามิเตอร์ของ Query กับตัวตรวจสอบเส้นทางภายใน (AppLinkRouteValidator) และส่งข้อมูลไปยังเราเตอร์เนทีฟโดยตรง (internalRouter(validatedRoute)) พร้อมส่งค่าคืนเป็น true เพื่อยกเลิกการแสดงผลบนเว็บที่ซ้ำซ้อน
การสร้างตัวจัดการ Intent ที่ปลอดภัยสำหรับ Custom Schemes และ Intent URIs
การแยกส่วนประกอบไวยากรณ์ Chrome Intent URI: Scheme, Package, และ S.browser_fallback_url
บน Android หน้าเว็บมักใช้ Chrome Intent URI เพื่อจัดโครงสร้าง deep link เนทีฟพร้อมกลไกสำรองเบราว์เซอร์ในตัว Intent URI จะใช้ scheme intent:// และเข้ารหัสพารามิเตอร์การกำหนดเส้นทาง เป้าหมายของแพ็กเกจ และ URL สำรอง ไว้ในสตริงที่ถูกทำให้เป็นอนุกรม (serialized):
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
ตามที่ระบุไว้ใน Chrome for Developers Guide บน Android Intent URIs URI จะถูกแบ่งออกเป็นส่วนประกอบต่างๆ ดังนี้:
- Host และ Path (
product/detail/1024): กำหนดปลายทางภายในแอปพลิเคชัน scheme=myapp: ระบุโปรโตคอลกำหนดเองพื้นฐานpackage=com.example.app: จำกัดการแก้ไข Intent ให้เฉพาะเจาะจงกับ Android แพ็กเกจที่ระบุเท่านั้นS.browser_fallback_url=...: พารามิเตอร์พิเศษแบบ String ทางเลือกที่กำหนด URL เว็บหรือร้านค้าที่จะโหลดหากไม่ได้ติดตั้งแอปพลิเคชันเป้าหมาย
อันตรายของการดำเนินการ Raw Parsed Intent: การลดช่องโหว่ Intent Injection
รูปแบบที่ไม่ควรปฏิบัติทั่วไปในการดำเนินการคือการแยก URI ที่เข้ามาด้วย Intent.parseUri(url, Intent.URI_INTENT_SCHEME) และส่ง Intent ที่ได้ไปยัง context.startActivity(intent) โดยตรง
เพื่อให้สอดคล้องกับ OWASP Mobile Application Security Testing Guide คำแนะนำเกี่ยวกับ Insecure Deep Links การสั่งเรียกใช้ Intent ที่แยกส่วนประกอบแบบดิบจะทำให้แอปพลิเคชันเสี่ยงต่อการโจมตีแบบ Intent Injection ผู้โจมตีที่ควบคุมหน้าเว็บอาจสร้าง Intent URI ที่มีเป้าหมายส่วนประกอบที่ชัดเจนและเป็นอันตราย (component=com.example.app/.InternalAdminActivity), การกระทำที่ละเอียดอ่อน, หรือพารามิเตอร์เสริมของ Intent ที่ได้รับสิทธิ์สูง (S.is_admin=true) โดย WebView จะ deserialize พารามิเตอร์เหล่านี้และเปิด activity ที่ไม่ได้ส่งออก (unexported) หรือ activity ภายใน ซึ่งเป็นการข้ามการป้องกันการตรวจสอบสิทธิ์

การสร้าง Intent ขึ้นใหม่อย่างปลอดภัย: การลบส่วนประกอบและการจำกัดการกระทำไว้ที่ ACTION_VIEW
เพื่อกำจัดช่องโหว่ Intent injection แอปพลิเคชันต้องไม่เรียกใช้ Intent ที่ผ่านการ deserialized โดยตรง แต่ WebViewClient จะต้องแยกวิเคราะห์ URI ดึงข้อมูลเฉพาะข้อมูลที่ตรวจสอบแล้ว และสร้าง Intent ใหม่ที่สะอาดและเฉพาะเจาะจง:
- Extract Data: แยกวิเคราะห์ URI และดึงแพ็กเกจเป้าหมายและ URI ข้อมูลที่ยืนยันแล้ว
- Validate Package: ตรวจสอบว่าแพ็กเกจที่ประกาศนั้นตรงกับรายการที่อนุญาต (allowlist) ของพันธมิตรที่ได้รับอนุญาตหรือแพ็กเกจภายใน
- Reconstruct: สร้าง
Intent(Intent.ACTION_VIEW, validatedDataUri)ใหม่ - Isolate: ใช้
intent.addCategory(Intent.CATEGORY_BROWSABLE)เพื่อให้แน่ใจว่าเฉพาะกิจกรรมที่เรียกดูได้เท่านั้นที่สามารถจัดการคำขอได้ ตั้งค่า Flag ที่ชัดเจน (FLAG_ACTIVITY_NEW_TASK), และตรวจสอบให้แน่ใจว่าintent.componentและintent.selectorยังคงเป็น null
การบังคับใช้รายการที่อนุญาต (Allowlist) สำหรับ Scheme, Host, Path, และ Query Parameter
ทั้ง custom scheme (myapp://) และ Intent URI ที่ถูกสร้างขึ้นใหม่จะต้องผ่านตัวตรวจสอบที่เข้มงวดก่อนดำเนินการ
ตัวอย่างการนำไปใช้นี้จัดการ scheme myapp:// ของแอปพลิเคชันเองโดยเฉพาะ และยึดการดำเนินการ Intent ไว้ที่ com.example.app สำหรับระบบนิเวศของพันธมิตรหลายแอป นักพัฒนาควรจัดการการจับคู่ scheme กับแพ็กเกจที่เข้มงวด หรือให้ความสำคัญกับ HTTPS App Links ที่ผ่านการยืนยันแล้ว
ตัวตรวจสอบจะตรวจสอบ:
- Scheme: ตรงกับ custom scheme ที่ได้รับอนุญาต (
myapp) - Host: ตรงกับหมวดหมู่การกำหนดเส้นทางที่กำหนดไว้ล่วงหน้า (
open,product,event) - Path Prefix: เป็นไปตามโครงสร้างเส้นทางที่คาดหวัง (
/detail/,/view/,/main/) - Query Parameters: บังคับใช้คีย์ Query ที่อยู่ในรายการที่อนุญาต (
target,id,promo,utm_source) และตรวจสอบว่าค่าพารามิเตอร์เป็นไปตามข้อจำกัดความยาว (เช่นตัวอักษร) และชุดอักขระตัวเลข
[ผู้ใช้กดลิงก์ภายใน Android WebView]
│
▼
[WebViewClient.shouldOverrideUrlLoading]
│
┌─────────────┴─────────────┐
▼ ▼
[!request.isForMainFrame] [request.isForMainFrame]
│ │
▼ ▼
[ตรวจสอบ Subframe Scheme] [ตรวจสอบ URL Scheme]
├─ HTTP(S) -> คืนค่า false │
└─ Non-Web -> คืนค่า true ┌────┴───────────────────────────┐
(ระงับข้อผิดพลาด) ▼ ▼
[HTTP / HTTPS] [Non-HTTP(S) Scheme]
│ │
▼ ▼
[ตรวจสอบโดเมนโฮสต์] [CustomSchemeValidator]
├─ Internal App Link ├─ ถูกต้อง -> เปิด Pinned Intent
│ -> เราเตอร์ภายใน └─ Intent -> สร้างใหม่ & สำรอง
└─ External Web
-> คืนค่า false
วิธีจัดการการมองเห็นแพ็กเกจ Android 11 และการสำรองข้อมูล App Store

การเปลี่ยนแปลงการมองเห็นแพ็กเกจใน Android 11: เหตุใดการสอบถามล่วงหน้า (Preflight) จึงต้องมีการประกาศ
เริ่มตั้งแต่ Android 11 (API level 30) Google ได้แนะนำการกรอง Package Visibility เพื่อเพิ่มความเป็นส่วนตัวของผู้ใช้ ภายใต้กรอบงานนี้ มุมมองของแอปพลิเคชันเกี่ยวกับแอปพลิเคชันอื่นที่ติดตั้งไว้จะถูกจำกัดโดยค่าเริ่มต้น
สิ่งสำคัญคือต้องเข้าใจความแตกต่างระหว่างการเปิด Intent และการสอบถาม Intent:
- Direct Intent Launching: การเรียกมาตรฐานไปยัง
context.startActivity(intent)ไม่ จำเป็นต้องมีการประกาศการมองเห็นแพ็กเกจ หากติดตั้งแอปพลิเคชันเป้าหมายไว้ ระบบปฏิบัติการจะกำหนดเส้นทางและเปิดใช้งานได้สำเร็จโดยไม่คำนึงว่าแอปนั้นจะมองเห็นการสอบถามแพ็กเกจหรือไม่ - Preflight Intent Queries: หากสถาปัตยกรรม
WebViewClientของคุณดำเนินการตรวจสอบล่วงหน้า เช่น การเรียกpackageManager.resolveActivity(intent, 0)หรือpackageManager.queryIntentActivities(intent, 0)เพื่อตรวจสอบการมีอยู่ของแอปพลิเคชันก่อนเปิดใช้งาน การสอบถามจะคืนค่าnullหรือรายการว่างเปล่า เว้นแต่แพ็กเกจหรือ scheme เป้าหมายจะถูกประกาศในAndroidManifest.xml
การประกาศองค์ประกอบ <queries> ใน AndroidManifest.xml สำหรับ Custom Schemes และเป้าหมายแพ็กเกจ
เพื่อให้สอดคล้องกับ Android Developers Guide บน Package Visibility ใน Android 11 แอปพลิเคชันที่ทำการตรวจสอบล่วงหน้าก่อนดำเนินการ Intent ควรประกาศแพ็กเกจเป้าหมายหรือลายเซ็น Intent ใน Manifest:
<manifest package="com.example.app">
<!-- การประกาศการสอบถามล่วงหน้าทางเลือกสำหรับการค้นหาแพ็กเกจที่ชัดเจน -->
<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>
หากสถาปัตยกรรมของคุณชอบรูปแบบการเปิดใช้งานมาตรฐาน การดำเนินการ startActivity() ภายในบล็อก try-catch ที่ดักจับ ActivityNotFoundException จะเป็นการนำไปใช้ที่ยืดหยุ่นและไม่ขึ้นอยู่กับการสอบถาม
การใช้เครือข่ายสำรอง (Fallback Chains): การกำหนดเส้นทางร้านค้าอย่างสง่างามเมื่อแอปพลิเคชันเป้าหมายไม่อยู่
เมื่อ custom scheme หรือ Intent URI ที่ถูกดักจับไม่สามารถเปิดได้เนื่องจากไม่ได้ติดตั้งแอปพลิเคชันเป้าหมายไว้ในอุปกรณ์ของผู้ใช้ WebViewClient จะต้องดำเนินการตามเครือข่ายสำรองที่ชัดเจน:
- Extract and Bind Fallback URL: สำหรับ URI แบบ
intent://ให้แยกพารามิเตอร์สตริงS.browser_fallback_urlและตรวจสอบว่ามันชี้ไปยังปลายทางที่ได้รับอนุญาต สำหรับ URL ของ Google Play ให้ตรวจสอบว่าพารามิเตอร์idตรงกับชื่อแพ็กเกจเป้าหมายอย่างเคร่งครัด (id=targetPackage) เพื่อป้องกันการโจมตีแบบ open-redirect - Market URI Generation: หากไม่มี URL สำรอง ให้สร้าง Intent ของตลาด:
market://details?id=com.example.app - Web Play Store Fallback: หากปิดการใช้งานหรือไม่มี Google Play ให้ดักจับ
ActivityNotFoundExceptionใน Intent ของตลาดและโหลดhttps://play.google.com/store/apps/details?id=com.example.app
การเชื่อมโยงพารามิเตอร์บริบทข้ามการเปลี่ยนเส้นทางสำรองด้วย OpoInstall
เมื่อผู้ใช้ที่ไม่ได้ติดตั้งแอปถูกส่งไปยัง Google Play ลิงก์ร้านค้าแบบคงที่จะละทิ้งพารามิเตอร์แบบไดนามิก OpoInstall ซึ่งเป็นแพลตฟอร์มการระบุแหล่งที่มา (attribution) และ deep linking บนมือถือ จะเชื่อมช่องว่างนี้โดยการจับโทเค็นอ้างอิงและบริบทแคมเปญบนแบ็กเอนด์ของ attribution ก่อนการเปลี่ยนเส้นทางร้านค้า ตรวจสอบพฤติกรรมการเรียกคืน deferred-parameter และ API การเริ่มต้นกับ OpoInstall Android SDK เวอร์ชันที่ปรับใช้ในการผลิต
เมื่อติดตั้งในภายหลังและเปิดแอปครั้งแรก OpoInstall Android SDK จะเรียกข้อมูลพารามิเตอร์ที่แคชไว้ ช่วยให้แอปพลิเคชันหลักกู้คืนฉากเป้าหมายของผู้ใช้ได้ โปรดตรวจสอบ เอกสารประกอบการรวม SDK สำหรับคำแนะนำในการรับพารามิเตอร์ deferred หลังจากติดตั้ง
การนำ WebViewClient ไปใช้ในการผลิตเพื่อการดักจับ Scheme อย่างปลอดภัย
การสร้างสถาปัตยกรรม WebViewClient แบบกำหนดเองใน Kotlin
WebViewClient ในการผลิตจะประสานการตรวจสอบเฟรม, การตรวจสอบ Allowlist ของ scheme, การสร้าง Intent ใหม่, การแยก URL สำรอง, และการกำหนดเส้นทางภายใน การนำไปใช้จะสรุปกฎการตรวจสอบความถูกต้องไว้ภายในวัตถุตัวตรวจสอบเฉพาะ (AppLinkRouteValidator และ CustomSchemeValidator) เพื่อรักษา callback การนำทางให้สะอาดและดูแลรักษาง่าย
การนำ AppLinkRouteValidator และ CustomSchemeValidator ไปใช้
วัตถุตัวตรวจสอบบังคับใช้ความปลอดภัยแบบ fail-closed ที่เข้มงวด:
AppLinkRouteValidatorตรวจสอบโดเมน HTTPS ภายใน, คำนำหน้าเส้นทางที่อนุญาต, และคีย์ Query ที่ได้รับอนุญาต (target,id,promo,utm_source) โดยคืนค่าเป็น data classValidatedAppRouteที่มีโครงสร้างCustomSchemeValidatorตรวจสอบ custom scheme ที่ได้รับอนุญาต (myapp), ตรวจสอบโฮสต์ที่อนุญาต (open,product,event), บังคับใช้คำนำหน้าเส้นทาง, และทำความสะอาดค่าพารามิเตอร์ Query กับชุดอักขระตัวเลขและข้อจำกัดความยาว (ตัวอักษร)
ตัวอย่างการนำไปใช้ในโค้ดด้านล่างแสดงให้เห็นวิธีกำหนดค่าตัวเลือกการประกาศการมองเห็นแพ็กเกจ AndroidManifest.xml และการนำ CustomWebViewClient ที่ปลอดภัยและ thread-safe ไปใช้ใน Kotlin พร้อมการจัดการสำรองอัตโนมัติ ไบนารี SDK ที่ผ่านการรับรองและ Wrapper สำหรับการรวมสามารถรับได้จาก ศูนย์ดาวน์โหลด SDK ของ OpoInstall
<!-- AndroidManifest.xml: ตัวเลือกการประกาศการมองเห็นแพ็กเกจสำหรับการสอบถามล่วงหน้า Intent (Android 11+) -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.app">
<!-- ประกาศแพ็กเกจภายนอกและ custom scheme เฉพาะในกรณีที่ใช้การสอบถาม preflight resolveActivity -->
<queries>
<!-- อนุญาตการค้นหาล่วงหน้าสำหรับพันธมิตรเฉพาะหรือแพ็กเกจเป้าหมาย -->
<package android:name="com.example.partner" />
<!-- อนุญาตการแก้ไขล่วงหน้าของ custom deep linking schemes -->
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="myapp" />
</intent>
<!-- อนุญาตการแก้ไขล่วงหน้าของโดเมน HTTPS App Link ที่ผ่านการยืนยัน -->
<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">
<!-- คอมโพเนนต์ของแอปพลิเคชัน -->
</application>
</manifest>
// Android: Custom WebViewClient พร้อมความปลอดภัยของขอบเขตเฟรม, การตรวจสอบ Allowlist, และการกำหนดเส้นทางสำรอง
// ตัวอย่างการรวมอ้างอิง; ตรวจสอบลายเซ็นเมธอดกับสถาปัตยกรรม Android ที่ปรับใช้
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) {
// ท่าทีด้านความปลอดภัยที่เข้มงวด: ปฏิเสธคำขอหากมีคีย์ Query ที่ไม่รู้จัก
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
// ข้อกำหนดด้านความปลอดภัย: จำกัดการกำหนดเส้นทางแอปภายนอกเฉพาะการนำทางเฟรมหลักเท่านั้น
if (!request.isForMainFrame) {
val scheme = request.url.scheme?.lowercase()
// อนุญาตการโหลด subframe HTTP(S) มาตรฐาน แต่ระงับ scheme ที่ไม่ใช่เว็บเพื่อป้องกันการจี้ iframe และข้อผิดพลาด
return if (scheme == "http" || scheme == "https") {
false
} else {
Log.w("WebViewClient", "ระงับ non-web scheme ใน subframe/iframe: $scheme")
true // บริโภค/ยกเลิกโดยไม่เปิด Intent ภายนอกหรือทำให้เกิดข้อผิดพลาด Chromium
}
}
val url = request.url.toString()
return handleUrlNavigation(view, url)
}
@Deprecated("Deprecated in Java")
override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean {
if (url == null) return false
val uri = Uri.parse(url)
val scheme = uri.scheme?.lowercase()
// Legacy callback ขาดข้อมูลเมตา isForMainFrame ที่เชื่อถือได้; อย่ากำหนดเส้นทาง scheme ภายนอก
return if (scheme == "http" || scheme == "https") {
false // อนุญาตการโหลดเนื้อหาเว็บมาตรฐาน
} else {
Log.w("WebViewClient", "บล็อก external scheme บน legacy callback: $scheme")
true // ระงับข้อผิดพลาด scheme ที่ไม่ได้จัดการโดยไม่เปิด Intent ภายนอก
}
}
private fun handleUrlNavigation(view: WebView?, url: String): Boolean {
val uri = Uri.parse(url)
val scheme = uri.scheme?.lowercase() ?: return false
// 1. จัดการ HTTP / HTTPS และ Internal App Links
if (scheme == "http" || scheme == "https") {
// ตรวจสอบว่า URL ตรงกับโดเมน App Link ของแอปเองหรือไม่
if (scheme == "https" && uri.host?.lowercase() == "app.example.com") {
val validatedRoute = AppLinkRouteValidator.validate(uri)
if (validatedRoute != null) {
Log.i("WebViewClient", "ตรวจสอบเส้นทาง HTTPS ภายในแล้ว กำลังส่งไปยังเราเตอร์ภายใน")
internalRouter(validatedRoute)
} else {
Log.w("WebViewClient", "ปฏิเสธ internal App Link ที่ผิดรูปแบบหรือไม่ได้รับอนุญาต: $uri")
}
return true // จัดการภายใน; ยกเลิกการโหลด webview
}
return false // อนุญาตให้ WebView โหลดเนื้อหาเว็บมาตรฐานตามธรรมชาติ
}
// 2. ตรวจสอบ custom schemes ด้วย Allowlist ที่เข้มงวดและการตรึงแพ็กเกจเพื่อป้องกันการจี้
val validatedUri = CustomSchemeValidator.validate(uri)
if (validatedUri != null) {
val pinnedIntent = Intent(Intent.ACTION_VIEW, validatedUri).apply {
`package` = "com.example.app" // ตรึงแพ็กเกจแอปพลิเคชันหลักไว้อย่างชัดเจน
addCategory(Intent.CATEGORY_BROWSABLE)
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
return launchSafeIntent(pinnedIntent, fallbackAction = {
handleStoreFallback(view, "com.example.app")
})
}
// 3. จัดการ Chrome Intent URIs ด้วยการสร้างใหม่ที่ปลอดภัยและการแยกข้อมูลสำรอง
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) {
// สร้าง Intent ACTION_VIEW ใหม่ที่สะอาดแทนการเรียกใช้วัตถุที่แยกวิเคราะห์แบบดิบ
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", "ปฏิเสธ intent ที่มี data URI ไม่ถูกต้อง: $rawDataUri")
true
}
} else {
Log.w("WebViewClient", "ปฏิเสธ intent ที่ไม่มีข้อมูล payload ของ data URI")
true
}
} else {
Log.w("WebViewClient", "ปฏิเสธ intent ที่กำหนดเป้าหมายแพ็กเกจที่ไม่ได้รับการยืนยัน: $targetPackage")
true // ยกเลิก intent ของแพ็กเกจที่ไม่ได้รับการยืนยัน
}
} catch (e: Exception) {
Log.e("WebViewClient", "ไม่สามารถแยกวิเคราะห์ intent URI: $url", e)
true // ระงับข้อผิดพลาด scheme ที่ไม่ได้จัดการ
}
}
// ปฏิเสธ scheme ที่ไม่รู้จักเพื่อหลีกเลี่ยงข้อผิดพลาด net::ERR_UNKNOWN_URL_SCHEME
Log.w("WebViewClient", "บล็อกการนำทางด้วย scheme ที่ไม่ได้รับการยืนยัน: $scheme")
return true
}
private fun launchSafeIntent(intent: Intent, fallbackAction: () -> Unit): Boolean {
return try {
context.startActivity(intent)
true
} catch (e: ActivityNotFoundException) {
Log.w("WebViewClient", "ไม่ได้ติดตั้งแอปพลิเคชันเป้าหมาย: ${intent.data}. กำลังเรียกใช้ fallback")
fallbackAction()
true
} catch (e: SecurityException) {
Log.e("WebViewClient", "เกิดข้อผิดพลาดด้านความปลอดภัยขณะเปิด intent: ${intent.data}", e)
true
}
}
private fun handleIntentFallback(view: WebView?, fallbackUrl: String?, targetPackage: String) {
if (!fallbackUrl.isNullOrBlank() && isAuthorizedFallbackUrl(fallbackUrl, targetPackage)) {
Log.i("WebViewClient", "กำลังส่งไปยัง URL สำรองของเบราว์เซอร์ที่ได้รับอนุญาต: $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", "แอป Play Store ไม่อยู่ กำลังโหลด URL ของร้านค้าบนเว็บ: $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" -> {
// บังคับว่า URL สำรองของ Google Play ต้องชี้ไปยังแพ็กเกจเป้าหมายโดยเฉพาะ
uri.path == "/store/apps/details" && uri.getQueryParameter("id") == targetPackage
}
"app.example.com", "www.example.com" -> {
// ตรวจสอบความถูกต้องของเส้นทางสำรองโดเมนที่เป็นเจ้าของ
AppLinkRouteValidator.validate(uri) != null
}
else -> false
}
}
}
การดำเนินการ Thread-Safe: การส่งเส้นทางภายในและการจัดการสถานะวงจร
เมื่อ AppLinkRouteValidator ยืนยันเส้นทางภายในที่ถูกต้อง การเปลี่ยนผ่าน UI จะต้องดำเนินการบนเธรด UI หลักของ Android หาก internalRouter ทริกเกอร์ fragments, การเปลี่ยน Activity หรือการนำทาง Compose ให้ห่อหุ้มการเรียกใช้ dispatch ภายใน runOnUiThread หรือ coroutine ของวงจรเธรดหลักเพื่อป้องกันการแครชของเธรด
เมทริกซ์การแก้ไขปัญหาและข้อผิดพลาดการเปลี่ยนเส้นทาง Android WebView
คู่มือการแก้ไขปัญหา Android WebView Deep Linking อย่างครอบคลุม
เมทริกซ์ด้านล่างแสดงรายละเอียดลายเซ็นข้อผิดพลาดทั่วไปที่พบเมื่อจัดการ custom schemes ใน Android WebView พร้อมสาเหตุรากเหง้าหลักและการแก้ไขทางวิศวกรรม:
| ลายเซ็นข้อผิดพลาด / อาการ | สาเหตุหลัก | API Levels ที่ได้รับผลกระทบ | จุดตรวจสอบการวินิจฉัย | การแก้ไขที่แนะนำ |
|---|---|---|---|---|
net::ERR_UNKNOWN_URL_SCHEME |
Scheme ที่ไม่ใช่เว็บไม่ถูกจัดการในตัวโหลด WebView | ทุกระดับ API | shouldOverrideUrlLoading หายไปหรือคืนค่า false |
Override delegate, ดักจับ scheme, คืนค่า true |
ActivityNotFoundException |
ไม่มีแอปเป้าหมายบนอุปกรณ์ | ทุกระดับ API | context.startActivity() ไม่ได้รับการจัดการ |
ดักจับข้อยกเว้นและดำเนินการ S.browser_fallback_url หรือเส้นทาง Play Store |
| Iframe Intent Hijacking | Subframe ทริกเกอร์ Intent ภายนอก | API 24+ | request.isForMainFrame ไม่ถูกตรวจสอบ |
ป้องกันด้วย if (!request.isForMainFrame) และระงับ scheme ที่ไม่ใช่เว็บ |
| Preflight Query Returns Null | ตัวกรองการมองเห็นแพ็กเกจ Android 11+ | API 30+ (Android 11+) | เรียก resolveActivity โดยไม่มีการประกาศใน Manifest |
ประกาศ <queries> ใน Manifest หรือใช้ startActivity try-catch โดยตรง |
| Internal App Link Reload Loop | Host App Link ถูกส่งเส้นทางกลับไปยัง WebView | ทุกระดับ API | โดเมนโฮสต์ไม่ได้รับการจัดการใน delegate | ดักจับโดเมนของตนเอง, ตรวจสอบเส้นทาง, ส่งไปยังเราเตอร์เนทีฟ |
คำถามที่พบบ่อย (FAQ)
ทำไม Android WebView ของฉันถึงแครชเมื่อเปิด custom URL schemes?
ฉันจะจัดการ intent:// URLs ใน WebView อย่างปลอดภัยโดยไม่มีช่องโหว่ด้านความปลอดภัยได้อย่างไร?
Android 11 package visibility ส่งผลต่อการดักจับ deep link ใน WebView อย่างไร?
สรุปและกรอบการตัดสินใจ
การจัดการ deep linking และ custom schemes ภายใน Android WebView จำเป็นต้องเชื่อมช่องว่างระหว่าง sandbox การแสดงผลเว็บกับการกำหนดเส้นทาง Intent ของระบบปฏิบัติการเนทีฟ การพึ่งพาพฤติกรรมเริ่มต้นของ WebView จะส่งผลให้เกิดข้อผิดพลาดของโปรโตคอลที่ไม่ได้จัดการ ประสบการณ์ผู้ใช้ที่ไม่ดี และช่องโหว่ด้านความปลอดภัยที่อาจเกิดขึ้น
ด้วยการนำ WebViewClient ที่มีประสิทธิภาพไปใช้ซึ่งตรวจสอบขอบเขตเฟรม ตรวจสอบ custom schemes กับรายการที่อนุญาตที่ชัดเจน สร้าง ACTION_VIEW intent ที่ปลอดภัย และจัดการเครือข่ายสำรองของร้านค้า ทีมวิศวกรรมจะลดความล้มเหลวในการจัดการ scheme ของ WebView และสนับสนุนการนำทางที่ยืดหยุ่นมากขึ้น การรวมการกำหนดเส้นทางฝั่งไคลเอ็นต์ที่เชื่อถือได้เข้ากับกรอบงานการระบุแหล่งที่มาบนมือถือที่ครอบคลุมจะช่วยลดความขัดแย้งในการกำหนดเส้นทางข้ามโฟลว์ attribution และสนับสนุนการเดินทางของผู้ใช้ที่สอดคล้องมากขึ้น
เพื่อสำรวจสถาปัตยกรรมการกำหนดเส้นทางพารามิเตอร์และการเชื่อมโยง deep link ของ Android เนทีฟ ให้ปรึกษา เอกสารประกอบการรวม SDK, ดาวน์โหลดไลบรารีไคลเอ็นต์จาก ศูนย์ดาวน์โหลด SDK ของ OpoInstall, สำรวจ การอ้างอิงการนำ attribution บนมือถือไปใช้งาน หรือลงทะเบียนแอปพลิเคชันของคุณบน คอนโซลนักพัฒนา OpoInstall
วัสดุที่เกี่ยวข้อง
- แนวคิด: Android WebView Navigation, การดักจับ Custom URL Scheme, การสร้าง Intent URI ใหม่, Package Visibility
- เทคโนโลยี: Android WebViewClient, Chromium WebEngine, Android Debug Bridge, OpoInstall Android SDK
- มาตรฐาน: IETF RFC 3986 Uniform Resource Identifier, ข้อมูลจำเพาะของ Android Intent, OWASP Mobile Application Security Testing Guide (MASTG)
- APIs: Android
shouldOverrideUrlLoading,WebResourceRequest.isForMainFrame,Intent.parseUri,PackageManager.queryIntentActivities - เอกสารและข้อมูลอ้างอิงทางการ:
- Android Developers Reference บน WebViewClient shouldOverrideUrlLoading
- Android Developers Guide บน WebResourceRequest
- Android Developers Guide บน Package Visibility ใน Android 11
- Chrome for Developers Guide บน Android Intent URIs
- OWASP Mobile Application Security Testing Guide บน Insecure Deep Links
Share this article



