[Android翻译]拦截Android上WebView的所有请求

3,637 阅读5分钟

原文地址:medium.com/@madmuc/int…

原文作者:medium.com/@madmuc

发布时间:2019年10月29日-5分钟阅读

问题

我有一个要求,即Android上的WebView的所有HTTP请求都需要在本地处理。例如,为HTML渲染提供资产,以及处理没有互联网连接的API请求。我也无法控制在WebView中加载哪些HTML内容,而是以一个单一的URL作为切入点。


0.5版本--覆盖URL加载

比方说,要求每当错误页面即将加载时,重定向主页。我可以使用以下代码。

webView.webViewClient = object : WebViewClient() {
    override fun shouldOverrideUrlLoading(
        view: WebView,
        request: WebResourceRequest
    ): Boolean {
        return if(request.url.lastPathSegment == "error.html") {
            view.loadUrl("https//host.com/home.html")
            true
        } else {
            false
        }
    }
}

顾名思义,shouldOverrideUrlLoading返回是否应该覆盖URL加载。如果该函数返回true,WebView将中止对传入该函数的请求的加载。

问题

  • 它不捕捉POST请求。
  • 它不会在页面内加载的任何资源上被触发,如图片、脚本等。
  • 它不会被页面上的JavaScript发出的任何HTTP请求所触发。

版本1.0 - 重定向资源加载

webView.webViewClient = object : WebViewClient() {
override fun onLoadResource(view: WebView, url: String) {
        view.stopLoading()
        view.loadUrl(newUrl) // this will trigger onLoadResource
    }
}

onLoadResource提供了与shouldOverrideUrlLoading类似的功能。ButonLoadResource将对当前页面上加载的任何资源(图片、脚本等)包括页面本身进行调用。

你必须在处理逻辑上设置一个退出条件,因为这个函数将在loadUrl(newUrl)时被触发。例如

webView.webViewClient = object : WebViewClient() {
override fun onLoadResource(view: WebView, url: String) {
        // exit the redirect loop if landed on homepage
        if(url.endsWith("home.html")) return
// redirect to home page if the page to load is error page
        if(url.endsWith("error.html")) {
            view.stopLoading()
            view.loadUrl("https//host.com/home.html")
        }
    }
}

问题

  • 它不会在页面上任何由JavaScript发出的HTTP请求中被触发。

版本1.5 - 处理所有请求

webView.webViewClient = object : WebViewClient() {
    override fun shouldInterceptRequest(
        view: WebView,
        request: WebResourceRequest
    ): WebResourceResponse? {
        return super.shouldInterceptRequest(view, request)
    }
}

这是一个非常强大的回调,允许你对当前页面上的任何请求提供完整的响应,包括data:file:模式。这将捕获JavaScript在页面上提出的请求。

这个函数在后台线程中运行,类似于你在后台线程中执行API调用的方式。任何试图在这个函数中修改WebView内容的行为都会引起异常,例如loadUrl,evaluationJavascript等。

例如,我们想提供一个本地错误页面。

webView.webViewClient = object : WebViewClient() {
    override fun shouldInterceptRequest(
        view: WebView,
        request: WebResourceRequest
    ): WebResourceResponse? {
        return if (request.url.lastPathSegment == "error.html") {
            WebResourceResponse(
                "text/html",
                "utf-8",
                assets.open("error")
            )
        } else {
            super.shouldInterceptRequest(view, request)
        }
    }
}

另一个例子,我们想从本地DB提供一个用户API响应。

webView.webViewClient = object : WebViewClient() {
    override fun shouldInterceptRequest(
        view: WebView,
        request: WebResourceRequest
    ): WebResourceResponse? {
        return if (request.url.path == "api/user") {
            val userId = request.url.getQueryParameter("userId")
            repository.getUser(userId)?.let {
                WebResourceResponse(
                    "application/json",
                    "utf-8",
                    ByteArrayInputStream(it.toJson().toByteArray())
                )
            } ?: WebResourceResponse(
                "application/json",
                "utf-8",
                404,
                "User not found",
                emptyMap(),
                EmptyInputStream()
            )
        } else {
            super.shouldInterceptRequest(view, request)
        }
    }
}

问题

  • WebResourceRequest上没有payload字段。例如,如果你想用POST API请求创建一个新用户。你不能从WebResourceRequest中获取POST有效载荷。

第2.0版--解析POST请求的有效载荷。

对于这个问题,StackOverflow上有几个想法。其中之一是在JavaScript中重写XMLHttpRequest接口。基本思路是重写XMLHttpRequest.send记录发送有效载荷,并在shouldInterceptRequest上检索有效载荷。这个解决方案有3个部分。

第一部分--JavaScript重写

XMLHttpRequest.prototype.origOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function(method, url, async, user, password) {
    // these will be the key to retrieve the payload
    this.recordedMethod = method;
    this.recordedUrl = url;
    this.origOpen(method, url, async, user, password);
};
XMLHttpRequest.prototype.origSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.send = function(body) {
    // interceptor is a Kotlin interface added in WebView
    if(body) recorder.recordPayload(this.recordedMethod, this.recordedUrl, body);
    this.origSend(body);
};

这个代码片段覆盖了XMLHttpRequest.open和XMLHttpRequest.send函数,将HTTP方法、URL和HTTP有效载荷传递给一个Kotlin函数。将此代码片段保存到assets文件夹中的JS文件中,并使用以下方法将其加载到WebView中。

webView.evaluateJavascript(
    assets.open("override.js").reader().readText(),
    null
)

第二部分 - 使用@JavascriptInterface的Kotlin类

class PayloadRecorder {
    private val payloadMap: MutableMap<String, String> = 
        mutableMapOf()
    @JavascriptInterface
    fun recordPayload(
        method: String, 
        url: String, 
        payload: String
    ) {
        payloadMap["$method-$url"] = payload
    }
    fun getPayload(
        method: String, 
        url: String
    ): String? = 
        payloadMap["$method-$url"]
}

这个类将接收前面JS代码中的recordPayload调用,并将有效载荷放入一个map中。我们可以在WebView中添加一个该类的实例,使用

val recorder = PayloadRecorder()
webView.addJavascriptInterface(recorder, "recorder")

第三部分 - 检索POST有效载荷

webView.webViewClient = object : WebViewClient() {
    override fun shouldInterceptRequest(
        view: WebView,
        request: WebResourceRequest
    ): WebResourceResponse? {
        val payload = recorder.getPayload(request.method, request.url.toString())
        // handle the request with the given payload and return the response
        return super.shouldInterceptRequest(view, request)
    }
}

这一部分与1.5版本的方法非常相似,只是我们可以从Kotlin JavaScript类中获取重新编码的POST有效载荷。

问题

  • 对于Android API 24+,evaluateJavascript所产生的状态不会跨页面持久化。这意味着任何加载的新页面都不会有第一部分的JavaScript覆盖。

2.1版本--确保JS覆盖在每个页面上可用

webView.webViewClient = object : WebViewClient() {
    override fun onPageStarted(
        view: WebView,
        url: String,
        favicon: Bitmap?
    ) {
        webView.evaluateJavascript(
            assets.open("override.js").reader().readText(),
            null
        )
    }
override fun shouldInterceptRequest(
        view: WebView,
        request: WebResourceRequest
    ): WebResourceResponse? {
        val payload = recorder.getPayload(request.method, request.url.toString())
        // handle the request with the given payload and return the response
        return super.shouldInterceptRequest(view, request)
    }
}

WebViewClient提供了onPageStarted,它在每次页面开始加载到WebView上时被调用。我们将在每次页面启动时执行JS覆盖代码snipper。

问题

  • 当一个页面被加载到当前页面的iFrame中时,不会调用onPageStarted。

2.2版本 - 在每个HTML页面中注入JS代码。

几乎每一种类型的请求都会调用的函数只有shouldInterceptRequest。但是,我们不允许在这个函数里面执行任何JS代码,因为它是在后台线程中运行的。我的解决方法是在shouldInterceptRequest返回的WebResourceResponse中的HTML内容中注入JS代码。

例如,你可以直接将JS覆盖代码添加到你的HTML中。

<html>
    <head>
        <script type="text/javascript" src="file:///android_asset/override.js" >
        </script>
    </head>
</html>

你也可以将JS代码动态地注入到HTML页面中。

webView.webViewClient = object : WebViewClient() {
    override fun shouldInterceptRequest(
        view: WebView,
        request: WebResourceRequest
    ): WebResourceResponse? {
        val resp = getResponse(request)
        if(resp.mMimeType == "text/html") 
            injectJs(resp)
        return resp
    }
}

但请记住

  • 在HTML的顶部注入JS代码,使其尽快生效。
  • 不要解析完整的HTML内容,因为这样做效率不高,而且HTML可能是无效的。

你大概可以在<head>上进行文本搜索,然后在后面附加脚本行。


你大概可以在上进行文字搜索,然后在后面追加脚本行。 如果你喜欢阅读这篇文章,请使用👏 按钮鼓掌,并通过你的圈子分享它。谢谢大家。


通过www.DeepL.com/Translator(免费版)翻译