如何在 iOS WKWebView 中啟用 Universal Links? Universal Links 預設即可在 WKWebView 內的合規連結中進行解析。透過實作 WKNavigationDelegate,宿主應用程式可以自定義路由策略——包括攔截應用程式自有的目標路徑、透過 decidePolicyForNavigationAction 控制外部轉跳,以及強制執行框架級別 (frame-level) 的安全性。
在 iOS 應用程式架構中,WKWebView 導航攔截功能允許宿主應用程式針對 Universal Links、自定義 URL Scheme 及網頁目標自定義路由策略。透過在 WKNavigationDelegate 中評估導航請求元數據,應用程式可將自有目標路徑進行內部導向,將外部目標委派給系統處理程序,並執行框架層級的安全策略。
| 術語 | 定義 | 相關實體 | 搜尋意圖角色 |
|---|---|---|---|
| WebView | 一種嵌入式的基於 WebKit 的視圖元件,用於在 iOS 應用程式內渲染互動式網頁內容。 | iOS SDK | 資訊 / 商業 |
| Universal Links | 一種標準 HTTPS 機制,用於將經過驗證的網域連結至原生 iOS 應用程式視圖。 | 深度連結路由 | 技術 / 資訊 |
| Custom URL Scheme | 一種由應用程式定義的 URI 結構,用於將 URL 導向至原生應用程式。 | 行動深度連結 | 資訊 |
WKWebView 與 Universal Link 路由在 iOS 上的互動方式

WebKit 導航生命週期與應用程式自有路由策略
Apple 將 Universal Links 實作為系統級路由機制,並在 Safari 和 WKWebView 環境中提供支援。當使用者點擊 WKWebView 內的合規連結時,平台會根據作業系統策略解析網域關聯並執行導航。
雖然系統識別的 Universal Links 可以交由原生處理程序執行,但嵌入式瀏覽器環境通常需要應用程式特定的路由邏輯。例如,當連結指向宿主應用程式本身的網域時,開發者通常傾向於透過原生視圖控制器 (View Controller) 直接導航,而非觸發完整的外部 App 重啟。實作 WKNavigationDelegate 可為宿主應用程式提供連結評估的精細控制,讓開發團隊能執行自定義的允許清單 (allowlist) 並可預測地路由內部目標。
使用者體驗障礙:當應用內網頁瀏覽陷入循環重新導向時
嵌入式 WebView 常被用於在 iOS App 內託管促銷微型網站、幫助中心、合作夥伴目錄與行銷登陸頁。當應用內網頁包含旨在將使用者帶往應用程式其他區塊的連結(例如「在 App 中檢視」按鈕)或合作夥伴應用程式時,預設導航可能會導致冗餘的渲染:
- 冗餘的網頁渲染:使用者可能會看到應用內頁面的 RWD 網頁版本,而非原生視圖控制器,導致需重複驗證且視覺一致性降低。
- 網頁困境 (Web Trapping):使用者可能會受困於深層的網頁導航堆疊中,且缺乏直觀的方式回到主要的原生 App 介面。
- 失敗的 App 間轉換:若連結指向第三方服務(例如導航 App、社群分享視窗或支付閘道),且這些服務依賴自定義 URL Scheme,則需要明確的委派處理。
比較 WKWebView 與 SFSafariViewController 在 Web-to-App 轉跳中的應用
在規劃 iOS 應用內網頁瀏覽時,工程團隊必須在 WKWebView 與 SFSafariViewController 之間進行選擇:
SFSafariViewController:提供由系統管理的獨立 Safari 瀏覽介面,具備自動填寫與內容阻擋等功能。宿主應用程式無法檢查瀏覽活動或網站數據,且 UI 自定義僅限於色調。WKWebView:一種嵌入式視圖元件,託管於 App 的 UI 進程內,同時在獨立的 WebKit 進程中執行網頁內容。它允許深度 UI 自定義、JavaScript 橋接以及自定義佈局整合,但需明確實作WKNavigationDelegate以自定義路由策略並處理 App 定義的自定義 Scheme。
decidePolicyForNavigationAction 如何攔截 WebKit 路由
導航策略管線:理解 WKNavigationAction、request 與 decisionHandler
為了控制 WKWebView 內的導航流程,開發者需指派一個符合 Apple WKNavigationDelegate 官方指南的自定義代理 (delegate)。主要的攔截點是該代理方法:
func webView(
_ webView: WKWebView,
decidePolicyFor navigationAction: WKNavigationAction,
decisionHandler: @escaping (WKNavigationActionPolicy) -> Void
)
由使用者互動、程式化重新導向或表單提交觸發的導航操作都會通過此方法。WKNavigationAction 物件提供關鍵元數據:
navigationAction.request.url:正在請求的目標URL。navigationAction.navigationType:觸發類型 (.linkActivated,.other,.formSubmitted)。navigationAction.sourceFrame:關於發起導航請求的框架資訊。navigationAction.targetFrame:關於預計載入內容的目標框架資訊。
decisionHandler 是一個完成閉包 (completion closure),用於告知 WebKit 是否允許或取消請求的導航。
何時返回 .allow 與 .cancel:控制 WebKit 資源載入生命週期
傳遞給 decisionHandler 的 WKNavigationActionPolicy 可控制 WebKit 是否繼續進行導航:
.allow:通知 WebKit 在網頁視圖內繼續執行請求的導航。.cancel:指示 WebKit 取消請求的導航。此策略適用於宿主應用程式攔截自定義 Scheme、在內部路由應用程式自有 Universal Link,或將外部目標委派給UIApplication.shared.open()時。
每個導航操作必須且僅能調用一次 decisionHandler,以確保導航策略解析流程不會停滯。
評估導航類型:區分使用者點擊 (.linkActivated) 與自動化重新導向
WKNavigationAction.navigationType 允許開發者區分明確的使用者互動與自動化指令碼:
.linkActivated:使用者實際點擊了 HTML 錨點標籤 (<a href="...">)。.other:代表程式化的導航,例如window.location.href更新、meta 自動重新整理或初始的webView.load()呼叫。.formSubmitted/.formResubmitted:代表表單 POST 或 GET 提交。
評估 navigationType 讓應用程式能執行保守的、應用級別的明確連結策略。對於外部自定義 Scheme 調用或第三方 App 轉跳,將 .linkActivated 作為策略門控 (gate) 有助於防止未經提示的背景指令碼觸發自動化的外部 App 啟動。
處理無需保留循環 (Retain Cycles) 的非同步決策策略
當路由驗證或權限檢查需要在返回決策前查詢本地快取或安全驗證器時:
- 確保
decisionHandler在所有執行路徑中被執行,包括錯誤與保護條件。 - 在轉義閉包 (escaping closures) 中使用弱引用 (
[weak self]),以防止WKWebView、其代理與父級UIViewController之間產生保留循環。
區分宿主 App 關聯網域與外部 Universal Links

管理應用程式自有目標與系統級連結委派
對於應用程式自有路徑,應避免透過冗餘的 Universal Link 查詢嘗試重新進入同一個應用程式。直接透過宿主應用程式的內部路由器處理應用自有目標,並將系統開啟功能主要用於應離開當前應用程式或需由外部服務解析的目標。
這種架構分離可確保導航流暢:
- 宿主 App 關聯網域:若 URL host 符合宿主 App 自身的關聯網域 (
app.example.com),則取消網頁視圖導航 (decisionHandler(.cancel)),驗證路由路徑,並將解析出的參數直接傳遞給應用程式的內部導航路由器。 - 外部應用程式:若 URL 指向外部合作夥伴目標或允許的自定義 Scheme,請套用應用級別的明確連結門控 (
navigationType == .linkActivated),取消網頁視圖導航,並將請求轉發至UIApplication.shared.open(url),讓作業系統啟動外部 App。
架構化內部路由驗證:透過 AppRouteValidator 提取路徑與查詢參數
當傳入的 URL 符合宿主應用程式的關聯網域時,該 URL 字串必須在觸發視圖控制器轉換前通過嚴格的路由驗證器。
AppRouteValidator 模型:
- 根據支援的內部路由允許清單(例如
/open/,/product/,/promo/,/checkout/)驗證 URL 路徑。 - 提取查詢參數(例如
id,promo,utm_source)。 - 強制執行字元集限制、長度邊界,並拒絕重複鍵,最終返回一個整潔的
ValidatedAppRoute資料結構。
透過系統 UIApplication 委派處理外部第三方 Universal Links

當 WKWebView 內的網頁連結至外部服務(例如合作夥伴 App、社群平台或外部工具)時,宿主應用程式可將路由委派給 iOS 系統:
let options: [UIApplication.OpenExternalURLOptionsKey: Any] = [
.universalLinksOnly: true
]
UIApplication.shared.open(url, options: options) { success in
if !success {
// 應用策略備援:若無原生 App 處理該 Universal Link,則載入網頁目標
}
}
使用 .universalLinksOnly 作為應用策略,可確保僅在安裝了處理該 Universal Link 的已驗證原生應用程式時,使用者才會被轉移出當前 App。
管理自定義 URL Scheme 備援 (myapp://) 與 HTTPS Universal Links
儘管 HTTPS Universal Links 代表 iOS 上的標準深度連結,但 App 定義的自定義 Scheme (myapp:// 或 partnerapp://) 在促銷活動與合作夥伴整合中仍很常見。
在統一的 WKNavigationDelegate 實作中:
- 首先檢查非 HTTP/HTTPS Scheme。若 Scheme 符合允許的自定義協定且滿足明確連結策略 (
navigationType == .linkActivated),代理會先驗證 host、path 與參數,再派發至UIApplication.shared.open()。 - 無法識別的 Scheme 或未經提示的背景 Scheme 調用會立即取消,從而防止未處理的導航錯誤或指令碼驅動的意圖濫發。
[使用者與 iOS WKWebView 內的連結互動]
│
▼
[WKNavigationDelegate: decidePolicyForNavigationAction]
│
┌─────────────┴─────────────┐
▼ ▼
[!action.sourceFrame.isMainFrame] [action.sourceFrame.isMainFrame]
│ │
▼ ▼
[子框架安全門控] [檢查目標 Scheme 與 Host]
├─ HTTP(S) -> .allow │
└─ 非網頁 -> .cancel ┌───────────┼───────────┐
(抑制子框架) ▼ ▼ ▼
[主機網域] [外部網頁] [自定義 Scheme]
│ │ │
▼ ▼ ▼
[AppRoute] [檢查連結] [檢查連結]
├─ 有效 -> ├─ 合作夥伴 -> ├─ 有效且點擊 ->
│ 內部導向 │ 開啟 App │ 開啟 App
└─ 無效 -> └─ 網頁 -> └─ 無效/自動 ->
.cancel .allow .cancel
如何強制執行主框架安全並防止 Iframe 劫持

將嵌入式網頁導航視為不受信任的輸入:OWASP 深度連結安全標準
根據 OWASP 行動應用安全測試指南關於不安全深度連結的指導,所有由行動導航處理程序處理的 URL 與參數酬載 (payload) 都必須視為不受信任的外部輸入。
在 WKWebView 中渲染的網頁可能會載入第三方指令碼、廣告橫幅或使用者產生的內容。若導航代理未經驗證就將任意 URL 轉發至原生視圖控制器或 UIApplication.shared.open(),意外的參數可能會針對敏感的內部應用路徑。
將主框架導航與嵌入式 Iframe 及新視窗目標隔離
根據 Apple Developer 文檔關於 WKNavigationAction 的說明,評估框架安全性需要檢查發起框架:
sourceFrame.isMainFrame == true:導航由主要、最頂層的文件框架直接發起。sourceFrame.isMainFrame == false:導航由嵌入的子框架或 iframe 發起。targetFrame == nil:導航請求新視窗目標(例如帶有target="_blank"的錨點)。
為防止 iframe 劫持——即嵌入的 iframe 嘗試在背景啟動外部應用程式或觸發原生視圖轉換——代理必須評估 sourceFrame.isMainFrame。若發起框架為 iframe (sourceFrame.isMainFrame == false),允許標準 HTTP/HTTPS 子框架導航 (.allow),但應阻擋任何非網頁的自定義 Scheme 或原生路由交接 (.cancel)。
防止惡意的子框架協定調用與背景 Scheme 濫發
執行發起框架檢查可防止子框架觸發未經提示的外部 Scheme 調用:
if !navigationAction.sourceFrame.isMainFrame {
let scheme = url.scheme?.lowercased() ?? ""
if scheme == "http" || scheme == "https" {
decisionHandler(.allow) // 允許標準 HTTP(S) 子框架導航
} else {
decisionHandler(.cancel) // 抑制來自子框架的非網頁 Scheme
}
return
}
在客戶端路由中強制執行嚴格的路徑與查詢參數允許清單
內部關聯網域 URL 與外部自定義 Scheme 皆必須在執行前通過嚴格的驗證模型:
- 路徑前綴允許清單:強制執行核准的路徑前綴(例如
/open/,/product/,/promo/,/checkout/),拒絕任意或畸形的路徑。 - 查詢鍵過濾:棄用未預期的查詢鍵並拒絕重複的參數鍵,以防止參數污染。
- 資料類型與長度限制:將參數值限制為字母數字字元集,並執行最大長度限制(
個字元)。
Swift 中的正式環境 WKNavigationDelegate 實作
在 Swift 中架構 CustomWebViewController 與代理
正式環境的 WKWebView 控制器會協調網頁配置、導航策略評估、內部路由與外部委派。該實作將驗證規則封裝在專用的驗證器類別 (AppRouteValidator 與 CustomSchemeValidator) 中,以保持代理回調的整潔、可測試與安全。
實作 AppRouteValidator 與 CustomSchemeValidator 模型
驗證器模型強制執行嚴格的「故障封閉」(fail-closed) 安全性:
AppRouteValidator驗證內部關聯網域,檢查路徑前綴並將查詢參數清理為結構化的ValidatedAppRoute物件。CustomSchemeValidator驗證授權的自定義 Scheme (myapp),驗證允許的 Host (open,product,event),並清理查詢值。
OpoInstall 可與應用程式自有的路由層一同整合,以進行歸因分析與延遲參數恢復。請查閱 SDK 整合文件以獲取完整的整合指南。
下方的技術實作展示了如何在 Swift 中配置安全的 WKNavigationDelegate:
// iOS: 具備嚴格 WKNavigationDelegate 路由與框架安全性的 CustomWebViewController
// 參考整合範例。請根據您的部署架構驗證方法簽章與網域映射。
import UIKit
import WebKit
struct ValidatedAppRoute {
let path: String
let queryParams: [String: String]
}
// 1. 宿主應用程式關聯網域的驗證器 (內部路由)
class AppRouteValidator {
private static let allowedPrefixes = ["/open/", "/product/", "/promo/", "/checkout/"]
private static let allowedQueryKeys = Set(["target", "id", "promo", "utm_source"])
static func validate(url: URL) -> ValidatedAppRoute? {
let path = url.path
// 強制執行批准的路徑前綴允許清單
guard allowedPrefixes.contains(where: { path.hasPrefix($0) }) else {
return nil
}
var sanitizedParams: [String: String] = [:]
var seenKeys = Set<String>()
if let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
let queryItems = components.queryItems {
let validChars = CharacterSet(charactersIn: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-")
for item in queryItems {
// 故障封閉驗證:若存在未經授權的查詢鍵或重複鍵,則拒絕 URL
guard allowedQueryKeys.contains(item.name), !seenKeys.contains(item.name) else { return nil }
seenKeys.insert(item.name)
let value = item.value ?? ""
if value.count <= 64 && value.rangeOfCharacter(from: validChars.inverted) == nil {
sanitizedParams[item.name] = value
} else {
return nil
}
}
}
return ValidatedAppRoute(path: path, queryParams: sanitizedParams)
}
}
// 2. 外部自定義 Scheme 的驗證器 (myapp://)
class CustomSchemeValidator {
private static let allowedSchemes = Set(["myapp"])
private static let allowedHosts = Set(["open", "product", "event"])
private static let allowedPathPrefixes = ["/detail/", "/view/", "/main/"]
private static let allowedQueryKeys = Set(["target", "id", "promo", "utm_source"])
static func validate(url: URL) -> URL? {
guard let scheme = url.scheme?.lowercased(), allowedSchemes.contains(scheme) else {
return nil
}
guard let host = url.host?.lowercased(), allowedHosts.contains(host) else {
return nil
}
let path = url.path
if !path.isEmpty && !allowedPathPrefixes.contains(where: { path.hasPrefix($0) }) {
return nil
}
var seenKeys = Set<String>()
if let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
let queryItems = components.queryItems {
let validChars = CharacterSet(charactersIn: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-")
for item in queryItems {
guard allowedQueryKeys.contains(item.name), !seenKeys.contains(item.name) else { return nil }
seenKeys.insert(item.name)
let value = item.value ?? ""
if value.count > 64 || value.rangeOfCharacter(from: validChars.inverted) != nil {
return nil
}
}
}
return url
}
}
// 3. 託管 WKWebView 並具備安全導航策略攔截的 UIViewController
class CustomWebViewController: UIViewController, WKNavigationDelegate {
var webView: WKWebView!
private let hostAssociatedDomain = "app.example.com"
private let allowedExternalPartnerHosts = Set(["partner.example.com"])
override func viewDidLoad() {
super.viewDidLoad()
let configuration = WKWebViewConfiguration()
webView = WKWebView(frame: view.bounds, configuration: configuration)
webView.navigationDelegate = self
view.addSubview(webView)
}
func webView(
_ webView: WKWebView,
decidePolicyFor navigationAction: WKNavigationAction,
decisionHandler: @escaping (WKNavigationActionPolicy) -> Void
) {
guard let url = navigationAction.request.url else {
decisionHandler(.allow)
return
}
// 安全檢查 1: 強制執行發起框架邊界 (sourceFrame) 以防止 iframe 劫持
if !navigationAction.sourceFrame.isMainFrame {
let scheme = url.scheme?.lowercased() ?? ""
if scheme == "http" || scheme == "https" {
decisionHandler(.allow) // 允許標準 HTTP(S) 子框架導航
} else {
decisionHandler(.cancel) // 抑制來自子框架/iframe 的非網頁 Scheme
}
return
}
let scheme = url.scheme?.lowercased() ?? ""
let isExplicitLinkActivation = (navigationAction.navigationType == .linkActivated)
// 安全檢查 2: 處理宿主 App 的關聯網域
// 進行內部路由,而非呼叫 UIApplication.shared.open
if scheme == "https", let host = url.host?.lowercased(), host == hostAssociatedDomain {
if let validatedRoute = AppRouteValidator.validate(url: url) {
AppInternalRouter.shared.navigate(to: validatedRoute)
}
decisionHandler(.cancel) // 取消網頁視圖載入,改為內部路由
return
}
// 安全檢查 3: 處理具備連結啟動策略的允許自定義 Scheme (myapp://)
if scheme != "http" && scheme != "https" && scheme != "about" {
// 強制要求自定義 Scheme 的外部 App 啟動需具備明確的使用者連結啟動
if isExplicitLinkActivation, let validatedURL = CustomSchemeValidator.validate(url: url) {
UIApplication.shared.open(validatedURL, options: [:], completionHandler: nil)
}
decisionHandler(.cancel) // 取消網頁視圖載入,防止未處理的 Scheme 錯誤
return
}
// 安全檢查 4: 處理外部目標與新視窗 (target="_blank") 請求
if scheme == "http" || scheme == "https" {
let host = url.host?.lowercased() ?? ""
// 將已驗證的合作夥伴 Universal Links 委派給具備明確連結策略的外部 App
if allowedExternalPartnerHosts.contains(host) && isExplicitLinkActivation {
let options: [UIApplication.OpenExternalURLOptionsKey: Any] = [
.universalLinksOnly: true
]
UIApplication.shared.open(url, options: options) { [weak self] success in
if !success {
// 應用策略備援:若無原生 App 處理,則在網頁視圖內載入外部合作夥伴目標
guard let self = self else { return }
self.webView.load(navigationAction.request)
}
}
decisionHandler(.cancel)
return
}
// 若 targetFrame 為 nil (新視窗請求),則安全地載入至當前 webView
if navigationAction.targetFrame == nil {
webView.load(navigationAction.request)
decisionHandler(.cancel)
return
}
// 標準網頁內容繼續在 WKWebView 內載入
decisionHandler(.allow)
return
}
decisionHandler(.allow)
}
}
// 應用程式特定的內部路由器佔位符 (非 OpoInstall SDK API)
class AppInternalRouter {
static let shared = AppInternalRouter()
func navigate(to route: ValidatedAppRoute) {
// 根據路徑與查詢參數執行內部 UI 視圖控制器轉換
}
}
執行緒安全:確保 UI 轉換在 Main Actor 上執行
在目前的 Swift 並行模型中,WKNavigationDelegate 回調被隔離在主執行緒 (Main Actor)。應用程式路由與視圖控制器轉換在主執行緒上執行,藉此維持原生導航工作流程中的執行緒安全。
WKWebView 深度連結導航錯誤與診斷矩陣
全面的 iOS WKWebView 深度連結排查指南
下方矩陣列出了管理 iOS WKWebView 內深度連結與自定義 Scheme 時常見的失敗模式,以及主要根源與建議的工程解決方案:
| 錯誤特徵 / 徵狀 | 主要根源 | 適用 iOS 版本 | 診斷檢查點 | 建議解決方案 |
|---|---|---|---|---|
| Universal Link 在網頁中載入 | 應用自有網域未被攔截 | iOS 9+ | decidePolicyForNavigationAction 未處理 |
攔截主機網域,解析路徑,內部路由,回傳 .cancel |
| 自有網域連結無法路由 | 在自有網域上呼叫 UIApplication.open |
iOS 9+ | 對自有 Host 呼叫了 UIApplication.shared.open |
避免外部自開啟,直接路由至內部路由器 |
| 自定義 Scheme 無聲失敗 | WebKit 無法識別非 HTTP 協定 | iOS 9+ | Scheme 未委派至 UIApplication |
在代理中攔截 Scheme,驗證允許清單,透過 UIApplication 開啟 |
| Iframe 協定劫持 | 子框架觸發外部自定義 Scheme | iOS 9+ | sourceFrame.isMainFrame 未檢查 |
使用 if !sourceFrame.isMainFrame 防護並抑制非網頁 Scheme |
| UI 交接警告或轉換問題 | UI 轉換在非主執行緒上執行 | iOS 9+ | 缺少 Main-Actor 指派 | 確保內部路由器與視圖控制器轉換在主執行緒上執行 |
常見問題 (FAQ)
開發者如何攔截 WKWebView 中的 Universal Links?
我可以使用 UIApplication.shared.open 從 WKWebView 啟動我自己的 App 嗎?
如何防止 WKWebView 中的嵌入式 iframe 觸發外部 App 啟動?
總結與決策框架
在 iOS WKWebView 內處理 Universal Links 與自定義 Scheme,需要架起 WebKit 網頁渲染容器與原生 UIKit 導航生命週期之間的橋樑。僅依賴預設導航策略,在需要應用自有路由邏輯時可能無法達成順暢的轉跳。
透過實作穩健的 WKNavigationDelegate,工程團隊可以驗證框架邊界、對外部轉跳執行保守的連結啟動策略、透過嚴格的路由驗證器解析內部關聯網域,並將外部目標安全地委派給 UIApplication.shared.open,藉此在維護可控導航的同時,防禦 iframe 協定劫持。
若要進一步探索原生 iOS 深度連結與參數路由架構,請參閱 SDK 整合文件。
相關資源
-
概念:iOS WebView 路由、Universal Links 攔截、WKNavigationDelegate、框架邊界隔離
-
技術:Apple WebKit、iOS UIKit、WKWebView、OpoInstall iOS SDK
-
標準:IETF RFC 3986 統一資源識別碼 (URI)、Apple 關聯網域規範、OWASP 行動應用安全測試指南 (MASTG)
-
API:
WKNavigationDelegate.decidePolicyForNavigationAction,WKNavigationAction.sourceFrame,UIApplication.shared.open -
官方文件與參考資料:
Share this article



