Как добавить заголовки HTTP в запрос глобально для iOS в Swift

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") но это не работает

пожалуйста, помогите мне в любом случае.

9 ответов

AFAIK к сожалению, вы не можете сделать это с WKWebView,

Это, безусловно, не работает в webView:decidePolicyForNavigationAction:decisionHandler: поскольку navigationAction.request только для чтения и не изменяемый NSURLRequest экземпляр, который вы не можете изменить.

Если я правильно понимаю, WKWebView запускает изолированную программную среду в отдельном контенте и сетевом процессе, и, по крайней мере в iOS, нет способа перехватить или изменить его сетевые запросы.

Вы можете сделать это, если вы отступите к UIWebView,

Есть много разных способов сделать это, я обнаружил, что самым простым решением было создать подкласс WKWebView и переопределить метод loadRequest. Что-то вроде этого:

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

Затем просто используйте класс CustomWebView, как если бы это был WKWebView.

ПРИМЕЧАНИЕ РЕДАКТИРОВАТЬ: Это будет работать только на первый запрос, как указано @Stefan Arentz.

ПРИМЕЧАНИЕ. Некоторые поля не могут быть переопределены и не будут изменены. Я не провел тщательного тестирования, но я знаю, что User-Agent поле не может быть переопределено, если вы не сделаете определенный хак ( проверьте здесь для ответа на это)

Я изменил ответ Au Ris, чтобы использовать NavigationAction вместо NavigationResponseкак предположил Джонни. Кроме того, это исправляет ситуации, когда впоследствии вызывается один и тот же 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!)
    }

}

С некоторыми ограничениями, но вы можете сделать это. Перехватить ответ в функции делегата webView:decidePolicyFornavigationResponse:decisionHandler:, если изменение URL отменяет его, передав decisionHandler(.cancel) и перезагрузите веб-просмотр с новымURLRequest который устанавливает пользовательские заголовки и перехваченный URL. Таким образом, каждый раз, когда изменяется 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)
        }
    }
}

Чтобы добавить пользовательские заголовки в запросы AJAX, я использую комбинацию двух - трех приемов. Первый обеспечивает синхронный канал связи между моим родным кодом Swift и javascript .Второй переопределяет метод XMLHttpRequest send() . Третий вводит переопределение в веб-страницу, которая загружается в мой WKWebView .

Итак, комбинация работает следующим образом:

вместо request.setValue(«значение», forHTTPHeaderField: «ключ») :

в ViewController:

      func webView(_ webView: WKWebView, runJavaScriptTextInputPanelWithPrompt headerName: String, defaultText _: String?, initiatedByFrame _: WKFrameInfo, completionHandler: @escaping (String?) -> Void) {
  if headerName == "key" {
    completionHandler("value")
  } else {
    completionHandler(nil)
  }
}}

в представленииDidLoad:

      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приносит общее эхо, включая все заголовки запроса. Так я узнаю, что задача выполнена.

Вот как вы это делаете: стратегия состоит в том, чтобы ваш WKNavigationDelegate отменил запрос, изменил изменчивую копию и повторно инициировал ее. If-else используется для разрешения запроса, если он уже имеет желаемый заголовок; в противном случае вы окажетесь в бесконечном цикле загрузки / решить-политики.

Не уверен, что случилось, но странные вещи случаются, если вы устанавливаете заголовок для каждого запроса, поэтому для лучших результатов устанавливайте заголовок только для запросов к интересующим вас доменам.

Приведенный здесь пример устанавливает поле заголовка для запросов к 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);
    }
}

Вышеупомянутые решения, похоже, работают на iOS 14, но на iOS < 14 тело запроса POST всегда имеет значение null, что вызывает отклонение запроса на стороне сервера. Оказалось, что это известная ошибка в WKWebView и WebKit, из-за которой navigationLink.Request.Body всегда имеет значение nil!! очень неприятная и глупая ошибка Apple, вынуждающая мигрировать UIWebView на нестабильный WKWebView!

В любом случае решение состоит в том, что вы должны (перед отменой запроса) захватить тело POST, запустив функцию javascript, а затем назначить результат обратно в 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;
    }

Мое решение - запрос на копирование и добавление заголовков, а затем повторная загрузка

    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)
    }
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)
}
Другие вопросы по тегам