如何在macOS上用URLProtocol捕获WKWebView全部网络请求
问题:在自研浏览器中实时捕获所有HTTP/HTTPS请求(含JS后台发起请求)
我正在开发一款实验分析类应用,需要识别所有HTTP/HTTPS请求(GET、POST、PUT等)及其内容、URL、媒体资源等,无论请求是用户触发还是JavaScript后台发起的。WebKit启用开发者功能后原生支持该能力,但我需要在自研浏览器中实现可控的实时捕获,无需缓存功能。
目前应用仅能捕获func canInit(with request: URLRequest)中的请求,无法获取全部后台请求活动。我已查阅URLProtocol、WKURLSchemeHandler(不支持HTTP/HTTPS)、WKNavigationDelegate(仅捕获高层流量)相关文档,不想使用方法混写,优先寻求原生方案。
当前代码可运行,但无法捕获全部请求事务:startLoading()方法从未被调用,按逻辑在canInit决定处理请求后该方法应触发,但实际并未执行。如何用原生代码实现所有请求的实时捕获?预期效果如下:

/* Identify all http and https requests (GET, POST, PUT, etc) and content, URLs, media, etc. whether they were initiated by the user or done in the background by javascript. This is something WebKit can do natively but the goal is to have full controllable visibility I do not need to cache anything, only see and capture the activity in real time. */ import Foundation import WebKit class CustomURLProtocol: URLProtocol, URLSessionDelegate { var connection : NSURLConnection? var data : Data? var response : URLResponse? var dataTask : URLSessionDataTask? static var requestCount = 0 lazy var session: URLSession = { let config = URLSession.shared.configuration return URLSession(configuration: config, delegate: self, delegateQueue: nil) }() static func printHeaders(allHeaders: [AnyHashable : Any]?) { print("Begin Headers: ****") for (key, value) in allHeaders ?? [:] { print("\(key): \(value)") } print("End Headers: ****") } static func logRequest(callingFunction: String = #function, statusCode: String = "N/A", request: URLRequest, response: URLResponse? = nil){ // Log the request **OR** response, status code and trace which class method made the call var bodyStr = "Empty Body" var urlStr = "No URL" var headers = [:] as [AnyHashable : Any]? var logType = "Request " /* Get the header of the request **OR** the response. The headers in the request and response are in different locations and hash types. allHTTPHeaderFields is [String : String]? so normalize it to [AnyHashable : Any]? then pass it to "printHeaders" */ if (response != nil) { // This is actually a RESPONSE, so get its header headers = (response as? HTTPURLResponse)?.allHeaderFields logType = "Response " } else { // This is a request, so get its header headers = (request.allHTTPHeaderFields!) as [AnyHashable : Any]? } if (request.httpBody != nil) { bodyStr = String(data: request.httpBody!, encoding: .utf8)!} if (request.url != nil) { urlStr = request.url!.absoluteString} print("Function = \(callingFunction)") print("\(logType) : #\(requestCount)") print("Method : \(request.httpMethod ?? "None")") print("Body : \(bodyStr)") print("Status Code : \(statusCode)") print("URL = \(urlStr)") // print("URL: \(request.url!)") CustomURLProtocol.printHeaders(allHeaders:headers) print("--------------\n\n") } override class func canInit(with request: URLRequest) -> Bool { // Return true if the request should be intercepted // We can add additional logic here to only intercept certain types of requests CustomURLProtocol.logRequest(request: request) var isHandled = true if (URLProtocol.property(forKey: "CustomURLProtocolHandledKey", in: request as URLRequest) != nil){ // prevent recursive loop isHandled = false } // if (request.url?.scheme == "http" || request.url?.scheme == "https") && // URLProtocol.property(forKey: "CustomURLProtocolHandledKey", in: request as URLRequest) != nil{ // isHandled = false // } requestCount += 1 // count all request but handle our custom types return isHandled } override class func canonicalRequest(for request: URLRequest) -> URLRequest { // Return the canonical version of the request CustomURLProtocol.logRequest(request: request) return request } override func startLoading() { // If we are handling the request, copy it, update with a custom key and pass // along so we can let it be handled by forwarding it CustomURLProtocol.logRequest(request: request) // Log the request data here // Forward the request to the original destination let newRequest = (self.request as NSURLRequest).mutableCopy() as! NSMutableURLRequest URLProtocol.setProperty(true, forKey: "CustomURLProtocolHandledKey", in: newRequest) var newResponse = self.response if (newResponse == nil){ newResponse = HTTPURLResponse(url: request.url!, statusCode: 301, httpVersion: "HTTP/1.1", headerFields: ["Location": "https://www.example.com"])! } self.client?.urlProtocol(self, wasRedirectedTo: newRequest as URLRequest, redirectResponse: newResponse!) self.dataTask = self.session.dataTask(with: newRequest as URLRequest) self.dataTask?.resume() } override func stopLoading() { // Cancel the connection and log it CustomURLProtocol.logRequest(request: request) self.connection?.cancel() self.connection = nil } override class func requestIsCacheEquivalent( _ a: URLRequest, to b: URLRequest ) -> Bool{ return super.requestIsCacheEquivalent( a, to: b ) // we dont care to cache, just pass it along } } extension CustomURLProtocol: NSURLConnectionDelegate { func connection(_ connection: NSURLConnection, didReceive response: URLResponse){ // Store the response and log the status code and headers self.response = response let statusCode = String("Status code: \((response as? HTTPURLResponse)?.statusCode ?? 0)") CustomURLProtocol.logRequest(statusCode: statusCode, request: request, response: response) self.client!.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) self.data = Data() } func connection(_ connection: NSURLConnection, didReceive data: Data) { // Accumulate the data print("function \(#function)") print("--------------\n\n") self.client!.urlProtocol(self, didLoad: data) if self.data == nil { self.data = Data() } self.data?.append(data) } func connection(_ connection: NSURLConnection, didFailWithError error: Error) { // Log the error and notify the client that the request has failed print("function \(#function)") print("Error: \(error)") print("--------------\n\n") self.client?.urlProtocol(self, didFailWithError: error) } func connectionDidFinishLoading(_ connection: NSURLConnection) { // Log the final response data and notify the client that the request has completed print("function \(#function)") if let data = self.data { print("Body: \(String(data: data, encoding: .utf8)!)") } else { print("Body is NIL") } print("--------------\n\n") self.client?.urlProtocolDidFinishLoading(self) } } class TrafficInspector: NSViewController { private let webView = WKWebView() func deleteCache(){ let websiteDataTypes = WKWebsiteDataStore.allWebsiteDataTypes() let date = NSDate(timeIntervalSince1970: 0) WKWebsiteDataStore.default().removeData(ofTypes: websiteDataTypes, modifiedSince: date as Date, completionHandler: { print("remove all cached data") }) } override func viewDidLoad() { // load our webview super.viewDidLoad() URLProtocol.registerClass(CustomURLProtocol.self) self.webView.configuration.preferences.setValue(true, forKey: "developerExtrasEnabled") deleteCache() // Set up web view view.addSubview(webView) webView.translatesAutoresizingMaskIntoConstraints = false webView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true webView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true webView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true webView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true // webView.autoresizingMask = [.width, .height] // Load a webpage using the intercepted URLProtocol instance let urlStr = "https://cnn.com" let url = URL(string:urlStr)! let request = URLRequest(url: url) webView.load(request) /* func startLoading() doesnt get called unless I do the following let config = URLSessionConfiguration.default config.protocolClasses = [CustomURLProtocol.self] let session = URLSession(configuration: config) let task = session.dataTask(with: url) task.resume() */ } }
解决方案
核心问题分析
WKWebView的网络请求栈独立于系统全局URLProtocol注册,直接调用URLProtocol.registerClass(CustomURLProtocol.self)无法让WKWebView使用自定义协议。这就是canInit被调用但startLoading从未触发的原因——WKWebView仅询问是否处理请求,但未将实际处理权交给自定义协议类。
修正后的完整代码
/* 识别所有HTTP/HTTPS请求(GET、POST、PUT等)及其内容、URL、媒体资源等, 无论请求由用户触发还是JavaScript后台发起。 WebKit原生支持该功能,但我们需要可控的实时可见性,无需缓存,仅需实时捕获请求活动。 */ import Foundation import WebKit class CustomURLProtocol: URLProtocol, URLSessionDataDelegate { private var dataTask: URLSessionDataTask? private var receivedData = Data() static var requestCount = 0 // 创建不带自定义协议的URLSession,避免递归调用 private lazy var session: URLSession = { let config = URLSessionConfiguration.default // 移除自定义协议,防止递归处理 config.protocolClasses = config.protocolClasses?.filter { $0 != CustomURLProtocol.self } return URLSession(configuration: config, delegate: self, delegateQueue: nil) }() // 打印请求/响应头 static func printHeaders(allHeaders: [AnyHashable: Any]?) { print("===== 开始打印头信息 =====") for (key, value) in allHeaders ?? [:] { print("\(key): \(value)") } print("===== 结束打印头信息 =====") } // 统一日志打印方法 static func logRequest(callingFunction: String = #function, statusCode: String = "N/A", request: URLRequest, response: URLResponse? = nil) { var bodyStr = "空请求体" var urlStr = "无URL" var headers: [AnyHashable: Any]? var logType = "请求" if let response = response { // 处理响应日志 headers = (response as? HTTPURLResponse)?.allHeaderFields logType = "响应" } else { // 处理请求日志 headers = request.allHTTPHeaderFields as [AnyHashable: Any]? } if let body = request.httpBody, let str = String(data: body, encoding: .utf8) { bodyStr = str } if let url = request.url { urlStr = url.absoluteString } print("调用方法: \(callingFunction)") print("\(logType) #\(requestCount)") print("请求方法: \(request.httpMethod ?? "无")") print("请求体: \(bodyStr)") print("状态码: \(statusCode)") print("URL: \(urlStr)") CustomURLProtocol.printHeaders(allHeaders: headers) print("-------------------------\n") } // 判断是否处理该请求 override class func canInit(with request: URLRequest) -> Bool { // 只处理HTTP/HTTPS请求,且未被处理过的请求 guard let scheme = request.url?.scheme, scheme.lowercased() == "http" || scheme.lowercased() == "https", URLProtocol.property(forKey: "CustomURLProtocolHandledKey", in: request) == nil else { return false } CustomURLProtocol.logRequest(request: request) requestCount += 1 return true } // 返回标准化请求 override class func canonicalRequest(for request: URLRequest) -> URLRequest { return request } // 开始处理请求 override func startLoading() { CustomURLProtocol.logRequest(request: self.request) // 标记请求已被处理,避免递归 let mutableRequest = (self.request as NSURLRequest).mutableCopy() as! NSMutableURLRequest URLProtocol.setProperty(true, forKey: "CustomURLProtocolHandledKey", in: mutableRequest) // 发起实际请求 dataTask = session.dataTask(with: mutableRequest as URLRequest) dataTask?.resume() } // 停止处理请求 override func stopLoading() { dataTask?.cancel() dataTask = nil receivedData.removeAll() } } // MARK: - URLSessionDataDelegate 实现 extension CustomURLProtocol { // 收到响应 func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse) { receivedData.removeAll() client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) let statusCode = String((response as? HTTPURLResponse)?.statusCode ?? 0) CustomURLProtocol.logRequest(statusCode: statusCode, request: self.request, response: response) } // 收到数据 func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { receivedData.append(data) client?.urlProtocol(self, didLoad: data) } // 请求完成 func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { if let error = error { print("请求失败: \(error)") client?.urlProtocol(self, didFailWithError: error) } else { // 打印响应体 if let responseStr = String(data: receivedData, encoding: .utf8) { print("响应体内容: \(responseStr)") } client?.urlProtocolDidFinishLoading(self) } } } class TrafficInspector: NSViewController { private let webView: WKWebView override init(nibName nibNameOrNil: NSNib.Name?, bundle nibBundleOrNil: Bundle?) { // 配置WKWebView使用自定义URLProtocol let config = WKWebViewConfiguration() // 注入自定义协议类 config.protocolClasses = [CustomURLProtocol.self] + (config.protocolClasses ?? []) webView = WKWebView(frame: .zero, configuration: config) super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } // 清除缓存 func deleteCache() { let websiteDataTypes = WKWebsiteDataStore.allWebsiteDataTypes() let date = Date(timeIntervalSince1970: 0) WKWebsiteDataStore.default().removeData(ofTypes: websiteDataTypes, modifiedSince: date) { print("已清除所有缓存数据") } } override func viewDidLoad() { super.viewDidLoad() webView.configuration.preferences.setValue(true, forKey: "developerExtrasEnabled") deleteCache() // 添加WebView到视图 view.addSubview(webView) webView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ webView.topAnchor.constraint(equalTo: view.topAnchor), webView.bottomAnchor.constraint(equalTo:
相关产品推荐
相关产品推荐

