Как добавить заголовки 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)
}