如何在iOS的Swift中全局添加HTTP请求头

36
func webView(webView: WKWebView!, decidePolicyForNavigationAction navigationAction: WKNavigationAction!, decisionHandler: ((WKNavigationActionPolicy) -> Void)!) {
     var request = NSMutableURLRequest(URL: navigationAction.request.URL)
     request.setValue("value", forHTTPHeaderField: "key")
     decisionHandler(.Allow)
}
在上面的代码中,我想在请求中添加一个标题。我尝试了navigationAction.request.setValue("IOS", forKey: "DEVICE_APP"),但它不起作用。 请以任何方式帮助我。

想要连接SOAP头吗? - Gökhan Çokkeçeci
谢谢回复...但实际上我创建了一个WKWebView,我需要在请求中添加标题。这已经完成,只是第一次。之后就不会添加了。根据苹果文档,有一个API decidePolicyForNavigationAction,每次加载请求时都会调用它。我想为该请求添加头文件。 - sandip
9个回答

35

据我所知,很遗憾你无法使用 WKWebView 来完成这个事情。

webView:decidePolicyForNavigationAction:decisionHandler: 中肯定不起作用,因为 navigationAction.request 是只读的非可变的 NSURLRequest 实例,你不能改变它。

如果我理解正确,在一个独立的内容和网络进程中运行的 WKWebView 处于沙盒状态,在 iOS 上至少没有办法拦截或更改其网络请求。

如果你退回到 UIWebView,你就可以做到这一点。


5
为什么被踩了?相信我,我已经调查了很多。这是不可能的。 - Stefan Arentz
这并不完全正确,请查看我的答案。 - Gabriel Cartier
可以覆盖WKWebView的加载函数并自己处理所有请求。这里有一个示例 - https://novemberfive.co/blog/wkwebview-redirect-with-cookies/ - Dmitrii Dushkin
@StefanArentz,我该如何为wkwebview添加多个头部? - Shrikant K

21

有很多种不同的方法可以做到这一点,我发现最简单的解决方案是子类化WKWebView并覆盖loadRequest方法。像这样:

class CustomWebView: WKWebView {
    override func load(_ request: URLRequest) -> WKNavigation? {
        guard let mutableRequest: NSMutableURLRequest = request as? NSMutableURLRequest else {
            return super.load(request)
        }
        mutableRequest.setValue("custom value", forHTTPHeaderField: "custom field")
        return super.load(mutableRequest as URLRequest)
    }
}

然后,只需像使用WKWebView一样使用CustomWebView类。

编辑说明:正如@Stefan Arentz指出的那样,这仅适用于第一个请求。

注意:有些字段无法被覆盖并且不会被更改。我没有进行彻底的测试,但我知道User-Agent字段除非您进行特定的hack(在此处查看答案),否则无法被覆盖。


14
这并没有真正解决这里所提出的问题。因为这只适用于最初的“顶层”请求。自定义标头不是黏性的,不会用于子资源加载或例如XHR。 - Stefan Arentz
1
没错,我会在我的帖子上添加一条注释。因为这个功能符合我的需求,所以我没有深入研究过webview,但我感觉可以通过委托来实现。你是否已经通过“webView:decidePolicyForNavigationAction:decisionHandler”方法进行了测试? - Gabriel Cartier
嘿,加布里埃尔,“有许多不同的方法可以做到这一点” - 比如什么?有具体的例子吗?你可能需要检查一下你的建议。 - Stefan Arentz
我会对决策策略方法进行更多测试并更新答案。 - Gabriel Cartier
1
现在它应该是 override func load(_ request: URLRequest) -> WKNavigation?(Swift 4.2) - Daniel

11
我已经修改了Au Ris的答案,使用NavigationAction而不是NavigationResponse,正如jonny建议的那样。此外,这修复了连续调用相同URL的情况,您不再需要跟踪当前URL。这仅适用于GET请求,但如果需要,可以轻松适应其他请求类型。
    import UIKit
    import WebKit
    class ViewController: UIViewController, WKNavigationDelegate  {
        var webView: WKWebView?

        override func viewDidLoad() {
            super.viewDidLoad()
            webView = WKWebView(frame: CGRect.zero)
            webView!.navigationDelegate = self
            view.addSubview(webView!)
            // [...] set constraints and stuff

            // Load first request with initial url
            loadWebPage(url: "https://my.url")
        }

        func loadWebPage(url: URL)  {
            var customRequest = URLRequest(url: url)
            customRequest.setValue("true", forHTTPHeaderField: "x-custom-header")
            webView!.load(customRequest)
        }
    
        func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping
        (WKNavigationActionPolicy) -> Void) {
            if navigationAction.request.httpMethod != "GET" || navigationAction.request.value(forHTTPHeaderField: "x-custom-header") != nil {
                // not a GET or already a custom request - continue
                decisionHandler(.allow)
                return
            }
            decisionHandler(.cancel)
            loadWebPage(url: navigationAction.request.url!)
        }
    }

7

在某些限制下,你是可以做到的。拦截代理函数webView:decidePolicyFornavigationResponse:decisionHandler:中的响应,如果url改变则通过传递decisionHandler(.cancel)来取消它,并重新加载设置了自定义标题和拦截的url的新的URLRequest的webview。通过这种方式,每当url改变时(例如用户点击链接),你都会取消该请求并创建一个带有自定义标题的新请求。

import UIKit
import WebKit
class ViewController: UIViewController, WKNavigationDelegate  {
    var webView: WKWebView?
    var loadUrl = URL(string: "https://www.google.com/")!

    override func viewDidLoad() {
        super.viewDidLoad()

        webView = WKWebView(frame: CGRect.zero)
        webView!.navigationDelegate = self
        view.addSubview(webView!)
        webView!.translatesAutoresizingMaskIntoConstraints = false
        webView!.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0).isActive = true
        webView!.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 0).isActive = true
        webView!.topAnchor.constraint(equalTo: view.topAnchor, constant: 0).isActive = true
        webView!.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0).isActive = true

        // Load first request with initial url
        loadWebPage(url: loadUrl)
    }

    func loadWebPage(url: URL)  {
        var customRequest = URLRequest(url: url)
        customRequest.setValue("some value", forHTTPHeaderField: "custom header key")
        webView!.load(customRequest)
    }

    // MARK: - WKNavigationDelegate

    func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) {
        guard let url = (navigationResponse.response as! HTTPURLResponse).url else {
            decisionHandler(.cancel)
            return
        }

        // If url changes, cancel current request which has no custom headers appended and load a new request with that url with custom headers
        if url != loadUrl {
            loadUrl = url
            decisionHandler(.cancel)
            loadWebPage(url: url)
        } else {
            decisionHandler(.allow)
        }
    }
}

1
即将尝试这个。只有一个问题:为什么要钩入navigationResponse?navigationAction听起来更像是正确的时机。 - Jonny
@Jonny,你可能是对的,navigationAction 可能是更好的位置。只要能提取出 URL 并检测到变化,我想你可以这样写 let url = navigationAction.request?.url ... 如果这对你行得通,我会相应地更正我的答案。 - Au Ris
没问题,我将我最终使用的内容发布为另一个答案。基本上是相同的,只是复制了现有请求并设置了参数。结果发现urlrequest是一个结构体。 - Jonny

2
为了向 AJAX 请求添加自定义标头,我使用了两个(现在是三个)技巧的组合。first 提供了本地 Swift 代码和 JavaScript 之间的同步通信渠道。second 覆盖了 XMLHttpRequest 的 send() 方法。third 将这个覆盖注入到加载到我的 WKWebView 中的网页中。
所以,这个组合的工作方式如下:
不再使用 request.setValue("value", forHTTPHeaderField: "key"),而是:
在 ViewController 中:
func webView(_ webView: WKWebView, runJavaScriptTextInputPanelWithPrompt headerName: String, defaultText _: String?, initiatedByFrame _: WKFrameInfo, completionHandler: @escaping (String?) -> Void) {
  if headerName == "key" {
    completionHandler("value")
  } else {
    completionHandler(nil)
  }
}}

在viewDidLoad方法中:
let script = 
  "XMLHttpRequest.prototype.realSend = XMLHttpRequest.prototype.send;"
  "XMLHttpRequest.prototype.send = function (body) {"
    "let value = window.prompt('key');"
    "this.setRequestHeader('key', value);"
    "this.realSend(body)"
  "};"
webView.configuration.userContentController.addUserScript(WKUserScript(source: script, injectionTime: .atDocumentEnd, forMainFrameOnly: true))

这是测试HTML文件:
<html>
<head>
  <script>
    function loadAjax() {
      const xmlhttp = new XMLHttpRequest()
      xmlhttp.onload = function() {
         document.getElementById("load").innerHTML = this.responseText
      }
      xmlhttp.open("GET", "/ajax")
      xmlhttp.send()
    }
  </script>
</head>
<body>
  <button onClick="loadAjax()">Change Content</button> <br />
  <pre id="load">load…</pre>
</body>
</html>

调用/ajax会带来一个通用的回应,包括所有请求头。这样我就知道任务已经完成了。

1
这个解决方案很好用。谢谢你。但是我做了一点调整。在设置脚本之前,您需要调用self.webView.load(urlRequest)。 - Siddhartha Moraes

1
这是实现方法: 策略是让你的WKNavigationDelegate取消请求,修改它的可变副本并重新初始化它。if-else用于允许请求继续进行,如果它已经有所需的头,则否则会陷入无限加载/decidePolicy循环中。
不确定发生了什么,但如果在每个请求上设置标头,则会发生奇怪的事情,因此为了获得最佳结果,请仅在对应关注的域名的请求上设置标头。
此示例为header.domain.com的请求设置一个标题字段,并允许所有其他请求不带标题。
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
    NSURL * actionURL = navigationAction.request.URL;
    if ([actionURL.host isEqualToString:@"header.domain.com"]) {
        NSString * headerField = @"x-header-field";
        NSString * headerValue = @"value";
        if ([[navigationAction.request valueForHTTPHeaderField:headerField] isEqualToString:headerValue]) {
            decisionHandler(WKNavigationActionPolicyAllow);
        } else {
            NSMutableURLRequest * newRequest = [navigationAction.request mutableCopy];
            [newRequest setValue:headerValue forHTTPHeaderField:headerField];
            decisionHandler(WKNavigationActionPolicyCancel);
            [webView loadRequest:newRequest];
        }
    } else {
        decisionHandler(WKNavigationActionPolicyAllow);
    }
}

抱歉我的 ObjC 不好,请原谅。在 Swift 中做同样的事情应该很容易。 ;) - jbelkins
4
这将只加载顶层页面,不会向页面上的任何资源或XHR请求添加标头。这与您先前的答案没有任何区别。 - Stefan Arentz
这个方法只改变了HTML页面请求的头部,这是正确的。但是随后的HTML页面请求也会改变其头部。而使用@gabriel-cartier的方法则不是这种情况。当用户点击链接时,不会调用loadRequest - dreamlab
你还应该检查请求是否在主框架上 navigationAction.targetFrame?.isMainFrame。否则,你将为 iframe 请求加载一个新页面。 - dreamlab
尽管在iOS 13和iOS 14上运行良好,但在iOS < 13上会导致不良行为,其中任何CSRF标头(如ASP.NET AntiForgery Token)都不会发送到请求标头中,从而导致服务器端检查失败。我正在使用Xamarin.iOS,因此不确定这是您代码的错误还是Xamarin的WKWebView绑定或来自Apple的错误。我正在努力解决它,但尚未成功@jbelkins - Mohammad Zekrallah
显示剩余2条评论

0
private var urlrequestCurrent: URLRequest?

func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
    //print("WEB decidePolicyFor navigationAction: \(navigationAction)")
    if let currentrequest = self.urlrequestCurrent {
        //print("currentrequest: \(currentrequest), navigationAction.request: \(navigationAction.request)")
        if currentrequest == navigationAction.request {
            self.urlrequestCurrent = nil
            decisionHandler(.allow)
            return
        }
    }

    decisionHandler(.cancel)

    var customRequest = navigationAction.request
    customRequest.setValue("myvaluefffs", forHTTPHeaderField: "mykey")
    self.urlrequestCurrent = customRequest
    webView.load(customRequest)
}

0
上述解决方案似乎在iOS 14上有效,但在iOS < 14上,POST请求正文始终为空,导致服务器拒绝请求。事实证明,这是WKWebView和WebKit中的已知错误,导致navigationLink.Request.Body始终为nil!!这是苹果强制UIWebView迁移至不稳定的WKWebView非常令人沮丧和愚蠢的错误!无论如何,解决方案是在取消请求之前,通过运行javascript函数获取POST正文,然后将结果分配回navigationAction.Request(如果navigationAction.Request.Body为null),然后取消操作并使用更新的navigationAction.Request再次请求它:此解决方案适用于Xamarin,但本机iOS很接近。
[Foundation.Export("webView:decidePolicyForNavigationAction:decisionHandler:")]
    public async void DecidePolicy(WebKit.WKWebView webView, WebKit.WKNavigationAction navigationAction, Action<WebKit.WKNavigationActionPolicy> decisionHandler)
    {
        try
        {
            var url = navigationAction.Request.Url;

            // only apply to requests being made to your domain
            if (url.Host.ToLower().Contains("XXXXX"))
            {
                if (navigationAction.Request.Headers.ContainsKey((NSString)"Accept-Language"))
                {
                    var languageHeaderValue = (NSString)navigationAction.Request.Headers[(NSString)"Accept-Language"];

                    if (languageHeaderValue == Globalization.ActiveLocaleId)
                    {
                       decisionHandler.Invoke(WKNavigationActionPolicy.Allow);
                        return;
                    }
                    else
                    {
                        decisionHandler(WKNavigationActionPolicy.Cancel);
                        var updatedRequest = SetHeaders((NSMutableUrlRequest)navigationAction.Request);

                        // Temp fix for navigationAction.Request.Body always null on iOS < 14
                        // causing form not to submit correctly
                        updatedRequest = await FixNullPostBody(updatedRequest);

                        WebView.LoadRequest(updatedRequest);
                    }
                }
                else
                {
                    decisionHandler(WKNavigationActionPolicy.Cancel);

                    var updatedRequest = SetHeaders((NSMutableUrlRequest)navigationAction.Request);

                    // Temp fix for navigationAction.Request.Body always null on iOS < 14
                    // causing form not to submit correctly
                    updatedRequest = await FixNullPostBody(updatedRequest);

                    WebView.LoadRequest(updatedRequest);
                }
            }
            else
            {
                decisionHandler.Invoke(WKNavigationActionPolicy.Allow);
            }
        }
        catch (Exception ex)
        {
            Logger.LogException(ex);
            decisionHandler?.Invoke(WKNavigationActionPolicy.Allow);
        }
    }
}


    private async Task<NSMutableUrlRequest> FixNullPostBody(NSMutableUrlRequest urlRequest)
    {
        try
        {
            // if on iOS 14 and higher, don't do this
            //if (UIDevice.CurrentDevice.CheckSystemVersion(14, 0))
                //return urlRequest;

            // only resume on POST http methods
            if (urlRequest.HttpMethod.ToLowerSafe() != "post")
                return urlRequest;

            // if post body is already there, exit
            if(urlRequest.Body != null)
                return urlRequest;

            if (WebView == null)
                return urlRequest;

            // get body post by running javascript
            var body = await WebView.EvaluateJavaScriptAsync("$('form').serialize()");//.ConfigureAwait(true);

            if (body != null)
            {
                //urlRequest.Body = urlRequest.Body; // always null on iOS < 14
                var bodyString = body.ToString();

                if (!bodyString.IsNullOrEmpty())
                    urlRequest.Body = NSData.FromString(bodyString);
            }

        }
        //This method will throw a NSErrorException if the JavaScript is not evaluated successfully.
        catch (NSErrorException ex)
        {
            DialogHelper.ShowErrorAlert(Logger.HandleExceptionAndGetErrorMsg(ex));
        }
        catch (Exception ex)
        {
            DialogHelper.ShowErrorAlert(Logger.HandleExceptionAndGetErrorMsg(ex));
        }

        return urlRequest;
    }


private NSMutableUrlRequest SetHeaders(NSMutableUrlRequest urlRequest)
    {
        try
        {
            if (this.UsePOST)
            {
                urlRequest.HttpMethod = "POST";
                urlRequest.Body = postParameters.Encode(NSStringEncoding.UTF8, false);
            }

            var keys = new object[] { "Accept-Language" };
            var objects = new object[] { Globalization.ActiveLocaleId };

            var dictionnary = NSDictionary.FromObjectsAndKeys(objects, keys);

            if (urlRequest.Headers == null)
            {
                urlRequest.Headers = dictionnary;
            }
            else
            {
                NSMutableDictionary httpHeadersCopy = new NSMutableDictionary(urlRequest.Headers);

                httpHeadersCopy.Remove((NSString)"Accept-Language");
                httpHeadersCopy.Add((NSString)"Accept-Language", (NSString)Globalization.ActiveLocaleId);

                urlRequest.Headers = null;
                urlRequest.Headers = (NSDictionary)httpHeadersCopy;
            }
        }
        catch (Exception ex)
        {
            Logger.LogException(ex);
        }
        return urlRequest;
    }

0

我的解决方案是复制请求并添加标头,然后再次加载它

    if navigationAction.request.value(forHTTPHeaderField: "key") == nil {
        decisionHandler(.cancel)
        
        var req:URLRequest = navigationAction.request;
        req.addValue("value", forHTTPHeaderField: "key");
        webView.load(req);
    } else {
        decisionHandler(.allow)
    }

这会影响性能吗?因为您总是取消当前请求并重新加载当前URL。 - Drew

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接