拦截WKWebView实现PDF下载:iOS WebView下载PDF获登录页面问题排查
哥们,你遇到的这个问题我之前帮好几个开发者踩过坑——核心就是WebView的登录会话和下载任务的会话没打通!你在WebView里登录后,登录状态存在WebView专属的存储里,但默认发起下载时用的是全新的URLSession,这个会话没带任何登录信息,服务器自然把你当成未授权用户,返回登录页面了。
下面给你几个针对性的解决方案,你可以根据网站的认证方式选:
方案一:共享WebView的Cookie(Cookie认证场景)
如果你的网站是靠Cookie维持登录状态的,直接让下载会话复用WebView的Cookie就行:
1. 先拿到WebView里的所有Cookie
对于WKWebView,可以通过它的网站数据存储获取Cookie:
// 获取WebView绑定的网站数据存储 let webViewDataStore = webView.configuration.websiteDataStore // 异步获取所有Cookie webViewDataStore.httpCookieStore.getAllCookies { cookies in self.startDownloadWithCookies(cookies: cookies) }
2. 用这些Cookie创建下载会话
把Cookie注入到URLSessionConfiguration里,再发起下载任务:
func startDownloadWithCookies(cookies: [HTTPCookie]) { let config = URLSessionConfiguration.default let sharedCookieStorage = HTTPCookieStorage.shared // 把WebView的Cookie全部导入到会话的Cookie存储中 cookies.forEach { sharedCookieStorage.setCookie($0) } config.httpCookieStorage = sharedCookieStorage // 创建会话并发起下载(记得设置delegate处理下载完成后的文件保存) let session = URLSession(configuration: config, delegate: self, delegateQueue: nil) guard let pdfUrl = URL(string: "你的PDF下载链接") else { return } let downloadTask = session.downloadTask(with: pdfUrl) downloadTask.resume() }
进阶:拦截WebView的下载请求
更优雅的方式是直接拦截WebView里的PDF下载请求,不让它走默认流程:
实现WKNavigationDelegate的拦截方法:
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { guard let url = navigationAction.request.url else { decisionHandler(.allow) return } // 判断是不是PDF下载请求(可以根据后缀或MIME类型调整) if url.pathExtension.lowercased() == "pdf" { // 拿到Cookie后用我们的会话下载 webView.configuration.websiteDataStore.httpCookieStore.getAllCookies { cookies in self.startDownloadWithCookies(cookies: cookies, url: url) } decisionHandler(.cancel) // 取消WebView默认的下载行为 return } decisionHandler(.allow) }
方案二:传递认证令牌(Token认证场景)
如果网站是用Bearer Token或者自定义令牌认证的,就得从WebView里拿到令牌,再放到下载请求的Header里:
1. 从WebView的JS环境获取令牌
通过evaluateJavaScript读取localStorage或sessionStorage里的令牌:
webView.evaluateJavaScript("localStorage.getItem('authToken')") { result, error in guard let token = result as? String, error == nil else { print("拿令牌失败:\(error?.localizedDescription ?? "未知错误")") return } self.startDownloadWithToken(token: token) }
2. 给下载请求添加认证Header
创建请求时把令牌放到Authorization Header里:
func startDownloadWithToken(token: String) { guard let pdfUrl = URL(string: "你的PDF下载链接") else { return } var downloadRequest = URLRequest(url: pdfUrl) // 按后端要求设置Header,比如Bearer Token格式 downloadRequest.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") let session = URLSession(configuration: .default, delegate: self, delegateQueue: nil) let downloadTask = session.downloadTask(with: downloadRequest) downloadTask.resume() }
方案三:复用WebView的进程池(高级尝试)
如果上面两种方法都不行,可以试试让下载会话复用WebView的WKProcessPool,这样会话上下文和WebView完全一致:
let config = URLSessionConfiguration.default // 复用WebView的进程池 config.processPool = webView.configuration.processPool let session = URLSession(configuration: config, delegate: self, delegateQueue: nil) // 发起下载任务...
这个方法不一定适配所有场景,但可以尝试,因为它让下载会话和WebView共享同一个运行上下文,大概率会自动带上认证信息。
为啥你之前的代码没生效?
你之前试的是Basic Auth(把用户名密码转成Base64放在Header),但如果你的网站不是用Basic Auth,而是靠Session Cookie或Token维持登录,那这种方式自然没用。得先确认网站的认证机制,再选对应的方案。
内容的提问来源于stack exchange,提问作者Sanoj Kashyap

