为WebView资源请求添加自定义头-android

129
我需要在WebView中的每个请求中添加自定义头信息。 我知道loadURL有参数extraHeaders,但这些仅适用于初始请求。 所有后续请求都不包含头信息。 我查看了WebViewClient中的所有重写方法,但没有任何方法可以向资源请求添加头信息 - onLoadResource(WebView view, String url)。 希望能得到帮助。
谢谢, 雷伊

2
@MediumOne:这不是一个 bug,而是你认为缺失的一个功能。我不知道 HTTP 规范中是否有任何内容指出后续的 HTTP 请求必须镜像前面的任意头信息。 - CommonsWare
1
@CommonsWare:这里“后续”的词义有些误导。当我在任何浏览器中输入“http://www.facebook.com”以加载facebook.com首页时,会有几个支持的“资源请求”来加载CSS、js和img文件。你可以在Chrome中使用F12功能(Network选项卡)来检查这些请求。对于这些请求,webview不会添加头信息。我尝试使用https://addons.mozilla.org/en-us/firefox/addon/modify-headers/插件为FireFox请求添加自定义头信息。该插件能够为所有这样的支持性“资源请求”添加头信息。我认为WebView应该也这样做。 - MediumOne
1
@MediumOne:“我认为WebView应该做同样的事情” - 这是你认为缺失的功能。请注意,您不得不使用插件才能使 Firefox 实现此功能。我并不是说你提出的功能是一个坏主意。我是说将其描述为错误可能无助于让这个建议的功能添加到 Android 中。 - CommonsWare
1
假设我正在使用WebView构建一个浏览器,可以配置为使用自定义HTTP代理。该代理使用自定义身份验证,其中对其的所有请求都应具有自定义标头。现在,webview提供了一个API来设置自定义标头,但在内部,它并没有将标头设置为生成的所有资源请求。也没有其他API可用于设置这些请求的标头。因此,任何依赖于向WebView请求添加自定义标头的功能都会失败。 - MediumOne
1
@CommonsWare - 我在4年后重新访问这个对话。我现在同意 - 这不应该是一个bug。HTTP规范中没有任何内容表明后续请求应该发送相同的标头。 :) - MediumOne
显示剩余3条评论
13个回答

94

尝试

loadUrl(String url, Map<String, String> extraHeaders)

要在资源加载请求中添加标头,请创建自定义的 WebViewClient 并覆盖以下内容:

API 24+:
WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request)
or
WebResourceResponse shouldInterceptRequest(WebView view, String url)

12
抱歉,但这并不起作用。它仅适用于初始请求的标头。标头不会添加到资源请求中。还有其他的想法吗?谢谢。 - Ray
21
是的,可以像这样覆盖WebClient.shouldOverrideUrlLoading方法: public boolean shouldOverrideUrlLoading(WebView view, String url) { view.loadUrl(url, extraHeaders); return true; } - peceps
6
@peceps - 在资源加载期间,回调函数'shouldOverrideUrlLoading'不会被调用。例如,当我们尝试使用view.loadUrl("http://www.facebook.com", extraHeaders)加载网页时,WebView会发送多个类似于'http://static.fb.com/images/logo.png'等的资源请求。对于这些请求,额外的头信息将不会被添加。并且在此类资源请求期间,不会调用shouldOverrideUrlLoading回调函数。回调函数'OnLoadResource'会被调用,但是没有办法在此时设置头信息。 - MediumOne
2
@MediumOne,为了资源加载,请重写WebViewClient.shouldInterceptRequest(android.webkit.WebView view, java.lang.String url)。查看API获取更多信息。 - yorkw
4
这个方法可以捕获所有的资源请求URL,但无法为这些请求添加头部信息。我的目标是为所有请求添加自定义HTTP头部。如果可以使用shouldInterceptRequest方法实现这一点,请问能否解释一下如何实现? - MediumOne
显示剩余3条评论

43

你需要使用WebViewClient.shouldInterceptRequest来拦截每个请求。

在每次拦截时,你需要获取URL并自己发起请求,然后返回内容流:

WebViewClient wvc = new WebViewClient() {
    @Override
    public WebResourceResponse shouldInterceptRequest(WebView view, String url) {

        try {
            DefaultHttpClient client = new DefaultHttpClient();
            HttpGet httpGet = new HttpGet(url);
            httpGet.setHeader("MY-CUSTOM-HEADER", "header value");
            httpGet.setHeader(HttpHeaders.USER_AGENT, "custom user-agent");
            HttpResponse httpReponse = client.execute(httpGet);

            Header contentType = httpReponse.getEntity().getContentType();
            Header encoding = httpReponse.getEntity().getContentEncoding();
            InputStream responseInputStream = httpReponse.getEntity().getContent();

            String contentTypeValue = null;
            String encodingValue = null;
            if (contentType != null) {
                contentTypeValue = contentType.getValue();
            }
            if (encoding != null) {
                encodingValue = encoding.getValue();
            }
            return new WebResourceResponse(contentTypeValue, encodingValue, responseInputStream);
        } catch (ClientProtocolException e) {
            //return null to tell WebView we failed to fetch it WebView should try again.
            return null;
        } catch (IOException e) {
             //return null to tell WebView we failed to fetch it WebView should try again.
            return null;
        }
    }
}

Webview wv = new WebView(this);
wv.setWebViewClient(wvc);

如果您的最低API版本是21级,您可以使用新的shouldInterceptRequest方法,它提供了额外的请求信息(例如标头),而不仅仅是URL。


2
以防有人在使用这个技巧时遇到和我一样的情况(不过这个技巧还是很好的),这里给你一个提示。由于http content-type头部可以包含可选参数,例如charset,它与MIME类型不完全兼容,因此WebResourceResponse构造函数的第一个参数的要求也不同。所以我们应该从content-type中提取MIME类型部分,通过任何你能想到的方式,例如正则表达式,使其适用于大多数情况。 - James Chen
2
此事件已被弃用。请使用public WebResourceResponse shouldInterceptRequest (WebView view, WebResourceRequest request)代替,更多信息请查看这里 - Hirdesh Vishwdewa
7
好的,我会尽力进行翻译。需要翻译的内容是:“@HirdeshVishwdewa - 看看最后一句话。” - Martin Konecny
6
HttpClient 不能与编译SDK版本23及以上一起使用。 - Tamás Kozmér
2
不幸的是,在向请求对象添加标头后调用 super.shouldInterceptRequest() 并不能像 @ErikReppen 描述的那样使修改后的请求生效。如果可以这样做,那就太好了,但 API 并不是这样工作的。超级调用只会返回 null 而不会发出请求。返回 null 告诉 Web 客户端不要拦截请求,并且请求将在没有修改标头的情况下进行。 - Jeff Lockhart
显示剩余5条评论

37

也许我的回复有点晚,但它涵盖了21级以下以上的API。

要添加标题,我们应该拦截每个请求创建带有所需标题的新请求

因此,我们需要覆盖两种情况下调用的shouldInterceptRequest方法: 1. 直到21级的API; 2. 21级以上的API。

    webView.setWebViewClient(new WebViewClient() {

        // Handle API until level 21
        @SuppressWarnings("deprecation")
        @Override
        public WebResourceResponse shouldInterceptRequest(WebView view, String url) {

            return getNewResponse(url);
        }

        // Handle API 21+
        @TargetApi(Build.VERSION_CODES.LOLLIPOP)
        @Override
        public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {

            String url = request.getUrl().toString();

            return getNewResponse(url);
        }

        private WebResourceResponse getNewResponse(String url) {

            try {
                OkHttpClient httpClient = new OkHttpClient();

                Request request = new Request.Builder()
                        .url(url.trim())
                        .addHeader("Authorization", "YOU_AUTH_KEY") // Example header
                        .addHeader("api-key", "YOUR_API_KEY") // Example header
                        .build();

                Response response = httpClient.newCall(request).execute();

                return new WebResourceResponse(
                        null,
                        response.header("content-encoding", "utf-8"),
                        response.body().byteStream()
                );

            } catch (Exception e) {
                return null;
            }

        }
   });
如果需要处理响应类型,您可以进行更改。
        return new WebResourceResponse(
                null, // <- Change here
                response.header("content-encoding", "utf-8"),
                response.body().byteStream()
        );

        return new WebResourceResponse(
                getMimeType(url), // <- Change here
                response.header("content-encoding", "utf-8"),
                response.body().byteStream()
        );

并添加方法

        private String getMimeType(String url) {
            String type = null;
            String extension = MimeTypeMap.getFileExtensionFromUrl(url);

            if (extension != null) {

                switch (extension) {
                    case "js":
                        return "text/javascript";
                    case "woff":
                        return "application/font-woff";
                    case "woff2":
                        return "application/font-woff2";
                    case "ttf":
                        return "application/x-font-ttf";
                    case "eot":
                        return "application/vnd.ms-fontobject";
                    case "svg":
                        return "image/svg+xml";
                }

                type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
            }

            return type;
        }

2
抱歉回复这个旧帖子,但使用这段代码后,我的应用程序尝试下载文件(并失败了),而不是加载页面。 - Giacomo M
这个能用于POST请求吗? - mrid

23

如前所述,您可以这样做:

 WebView  host = (WebView)this.findViewById(R.id.webView);
 String url = "<yoururladdress>";

 Map <String, String> extraHeaders = new HashMap<String, String>();
 extraHeaders.put("Authorization","Bearer"); 
 host.loadUrl(url,extraHeaders);

我测试了一下,在一个MVC控制器中,我扩展了Authorize属性来检查头部信息,头部信息确实存在。

我测试了一下,在我扩展了Authorize属性的MVC控制器上进行了测试,该属性用于检查请求头,请求头是存在的。


我将不得不重新处理这个问题,因为当它被编写和发布时,它适用于Kit-Kat。 我还没有尝试使用Lolly Pop。 - leeroya
在Jelly Bean或Marshmallow上对我不起作用...在头部中没有任何变化。 - Erik Verboom
11
这并不符合原始问题的要求。他想要在Webview发出的所有请求中添加标题。这只会将自定义标题添加到第一个请求中。 - NinjaCoder
1
这不是OP所询问的。 - Akshay
我知道这并不回答 OP 所寻找的内容,但这正是我想要的,即向 WebViewIntent URL 添加额外的标头。无论如何,谢谢! - Joshua Pinter

17

这对我有效:

  1. 首先,您需要创建一个方法,该方法将返回您想要添加到请求中的标头:

private Map<String, String> getCustomHeaders()
{
    Map<String, String> headers = new HashMap<>();
    headers.put("YOURHEADER", "VALUE");
    return headers;
}
  • 接下来您需要创建WebViewClient:

  • private WebViewClient getWebViewClient()
    {
    
        return new WebViewClient()
        {
    
        @Override
        @TargetApi(Build.VERSION_CODES.LOLLIPOP)
        public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request)
        {
            view.loadUrl(request.getUrl().toString(), getCustomHeaders());
            return true;
        }
    
        @Override
        public boolean shouldOverrideUrlLoading(WebView view, String url)
        {
            view.loadUrl(url, getCustomHeaders());
            return true;
        }
    };
    }
    
  • 将WebViewClient添加到您的WebView中:

  • webView.setWebViewClient(getWebViewClient());
    

    希望这可以帮助。


    1
    看起来不错,但这个操作是添加一个头部还是替换头部? - Ivo Renkema
    1
    @IvoRenkema loadUrl(String url, Map<String, String> additionalHttpHeaders) 表示添加额外的头部信息。 - AbhinayMe
    显然,这对页面内部的资源(例如图片)不起作用,因此这并没有解决提问者的问题。 - undefined

    4

    以下是使用HttpUrlConnection实现的示例:

    class CustomWebviewClient : WebViewClient() {
        private val charsetPattern = Pattern.compile(".*?charset=(.*?)(;.*)?$")
    
        override fun shouldInterceptRequest(view: WebView, request: WebResourceRequest): WebResourceResponse? {
            try {
                val connection: HttpURLConnection = URL(request.url.toString()).openConnection() as HttpURLConnection
                connection.requestMethod = request.method
                for ((key, value) in request.requestHeaders) {
                    connection.addRequestProperty(key, value)
                }
    
                connection.addRequestProperty("custom header key", "custom header value")
    
                var contentType: String? = connection.contentType
                var charset: String? = null
                if (contentType != null) {
                    // some content types may include charset => strip; e. g. "application/json; charset=utf-8"
                    val contentTypeTokenizer = StringTokenizer(contentType, ";")
                    val tokenizedContentType = contentTypeTokenizer.nextToken()
    
                    var capturedCharset: String? = connection.contentEncoding
                    if (capturedCharset == null) {
                        val charsetMatcher = charsetPattern.matcher(contentType)
                        if (charsetMatcher.find() && charsetMatcher.groupCount() > 0) {
                            capturedCharset = charsetMatcher.group(1)
                        }
                    }
                    if (capturedCharset != null && !capturedCharset.isEmpty()) {
                        charset = capturedCharset
                    }
    
                    contentType = tokenizedContentType
                }
    
                val status = connection.responseCode
                var inputStream = if (status == HttpURLConnection.HTTP_OK) {
                    connection.inputStream
                } else {
                    // error stream can sometimes be null even if status is different from HTTP_OK
                    // (e. g. in case of 404)
                    connection.errorStream ?: connection.inputStream
                }
                val headers = connection.headerFields
                val contentEncodings = headers.get("Content-Encoding")
                if (contentEncodings != null) {
                    for (header in contentEncodings) {
                        if (header.equals("gzip", true)) {
                            inputStream = GZIPInputStream(inputStream)
                            break
                        }
                    }
                }
                return WebResourceResponse(contentType, charset, status, connection.responseMessage, convertConnectionResponseToSingleValueMap(connection.headerFields), inputStream)
            } catch (e: Exception) {
                e.printStackTrace()
            }
            return super.shouldInterceptRequest(view, request)
        }
    
        private fun convertConnectionResponseToSingleValueMap(headerFields: Map<String, List<String>>): Map<String, String> {
            val headers = HashMap<String, String>()
            for ((key, value) in headerFields) {
                when {
                    value.size == 1 -> headers[key] = value[0]
                    value.isEmpty() -> headers[key] = ""
                    else -> {
                        val builder = StringBuilder(value[0])
                        val separator = "; "
                        for (i in 1 until value.size) {
                            builder.append(separator)
                            builder.append(value[i])
                        }
                        headers[key] = builder.toString()
                    }
                }
            }
            return headers
        }
    }
    

    请注意,这种方法不适用于POST请求,因为WebResourceRequest不提供POST数据。有一个Request Data - WebViewClient库,它使用JavaScript注入的方法来拦截POST数据。

    3

    通过跳过 loadUrl 方法并编写自己的 loadPage,使用 Java 的 HttpURLConnection 可以控制所有标头。然后使用 webview 的 loadData 方法显示响应。

    Google 提供的标头没有访问权限。它们在 WebView 源代码中的 JNI 调用中。


    5
    你的回答中是否有任何参考资料?如果你能在回答中提供实现参考资料,对其他人会很有帮助。 - Hirdesh Vishwdewa

    2

    这个对我有用。创建下面的WebViewClient并将其设置为您的webview。由于我的内容中的urls只有相对urls,所以我不得不使用webview.loadDataWithBaseURL。仅当使用loadDataWithBaseURL设置了baseurl时,才能正确获取url。

    public WebViewClient getWebViewClientWithCustomHeader(){
        return new WebViewClient() {
            @Override
            public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
                try {
                    OkHttpClient httpClient = new OkHttpClient();
                    com.squareup.okhttp.Request request = new com.squareup.okhttp.Request.Builder()
                            .url(url.trim())
                            .addHeader("<your-custom-header-name>", "<your-custom-header-value>")
                            .build();
                    com.squareup.okhttp.Response response = httpClient.newCall(request).execute();
    
                    return new WebResourceResponse(
                            response.header("content-type", response.body().contentType().type()), // You can set something other as default content-type
                            response.header("content-encoding", "utf-8"),  // Again, you can set another encoding as default
                            response.body().byteStream()
                    );
                } catch (ClientProtocolException e) {
                    //return null to tell WebView we failed to fetch it WebView should try again.
                    return null;
                } catch (IOException e) {
                    //return null to tell WebView we failed to fetch it WebView should try again.
                    return null;
                }
            }
        };
    
    }
    

    对我来说它是有效的:.post(reqbody),其中RequestBody reqbody = RequestBody.create(null, ""); - narancs

    0
    你可以尝试我的解决方案来解决这个问题,虽然我承认它可能不适用于所有情况,但在满足我的特定需求方面,它已经被证明是有效的,特别是在处理大量不触发WebViewClient回调的JavaScript fetch调用时。
    长话短说:
    为了在每个WebView的HTTP请求中添加自定义头部,你可以注入一段覆盖window.fetch行为的JavaScript代码到你自己的WebViewClient中。
    以下是详细的实现步骤:
    1. 创建你自己的自定义WebViewClient
    class CustomHeaderWebViewClient(headerValue: String) : WebViewClient() { }
    

    在这个自定义的WebViewClient中,你可以传递想要注入到HTTP头部的headerValue。
    2. 定义JS注入代码
    创建一个属性来存储你的JavaScript注入代码。这段代码将修改window.fetch方法以包含自定义的头部:
    private val authHeaderInjection = """
                (function() {
                    const originalFetch = window.fetch;
                    window.fetch = function(input, init) {
                        init = init || {};
                        init.headers = init.headers || {};
                        init.headers['My-Header'] = '$headerValue';
                        return originalFetch.apply(this, arguments);
                    };
                })();
            """.trimIndent()
    

    3. 重写onPageStarted方法

    你需要重写onPageStarted方法,在每次导航时执行JavaScript注入:

    override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
        view?.evaluateJavascript(authHeaderInjection) { Timber.d(it) }
        super.onPageStarted(view, url, favicon)
    }
    

    注意:根据WebView内容的实现方式,您可以选择不同的回调函数。

    4. 最后,不要忘记为您的WebView设置自定义的WebViewClient。

    注意:请注意,根据您的需求,当使用webview.loadUrl(url, yourHeaders)时,您可能仍然需要手动添加其他自定义标头。

    最终代码:

    import android.graphics.Bitmap
    import android.webkit.WebView
    import android.webkit.WebViewClient
    import timber.log.Timber
    
    class CustomHeaderWebViewClient(headerValue: String) : WebViewClient() {
        private val authHeaderInjection = """
                (function() {
                    const originalFetch = window.fetch;
                    window.fetch = function(input, init) {
                        init = init || {};
                        init.headers = init.headers || {};
                        init.headers['My-Header'] = '${headerValue}';
                        return originalFetch.apply(this, arguments);
                    };
                })();
            """.trimIndent()
    
        override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
            view?.evaluateJavascript(authHeaderInjection) { Timber.d(it) }
            super.onPageStarted(view, url, favicon)
        }
    }
    

    0

    shouldInterceptRequest 无法拦截 request.body。 我建议在 webview.loadUrl 前重置 userAgent。

    例如: webview.settings.userAgentString += " key1/value1 key2/value2"


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