WebView拦截请求避坑指南

6,250 阅读3分钟

如果你刚好也在拦截WebView请求,那么下面这些坑,我已替你踩过了。\color{#FF4500}{如果你刚好也在拦截WebView请求,那么下面这些坑,我已替你踩过了。}总的来说,拦截WebView的请求的原因无外乎两种,一是想篡改请求,二是想篡改响应。
话不多说,直接上代码。一个标准的拦截请求姿势如下,

public class MyWebViewClient extends WebViewClient {
    @Override
    public WebResourceResponse shouldInterceptRequest(WebView view, final WebResourceRequest request) {
        String url = request.getUrl().toString();
        //your target url
        if (url.equals(targetUrl))) {
            try {
                OkHttpClient okHttpClient = HTTPClientManager.getInstance().getOkHttpClient();
                Request.Builder builder = new Request.Builder();
                //do you want to do for builder
                Request req = builder.url(url).get().build();
                Response response = okHttpClient.newCall(req).execute();
                //do you want to do for response
                WebResourceResponse webResourceResponse = new WebResourceResponse("text/html", "utf-8", new ByteArrayInputStream(response.body().string().getBytes()));
                return webResourceResponse;
            } catch (Exception e) {
                e.printStackTrace();
            }
            return super.shouldInterceptRequest(view, request);
        }
}

重写shouldInterceptRequest()方法,对需要拦截的url,使用OKHttpClient代替WebView发起请求。这时,请求的Request完全由自己定义,并且在拿到HTTP的Response后,可以对Response随意篡改,最后把篡改过的Response组装成WebResourceResponse进行返回就大功告成了。


一个小应用:通过拦截WebView的DOM请求,向页面中注入一个JS文件,用作jsbridge。

public class MyWebViewClient extends WebViewClient {
    @Override
    public WebResourceResponse shouldInterceptRequest(WebView view, final WebResourceRequest request) {
        String url = request.getUrl().toString();
        if (url.equalsIgnoreCase(loadUrl)) {
            try {
                OkHttpClient okHttpClient = CtripHTTPClientV2.getInstance().getOkHttpClient();
                Request.Builder builder = new Request.Builder();
                Request req = builder.url(url).get().build();
                Response response = okHttpClient.newCall(req).execute();
                String html = response.body().string().trim();
               
                //解析reponse,并向html中插入js文件
                String doctype = "<!DOCTYPE html>";
                int doctypeIndex = html.indexOf(doctype);
                String body = html.substring(doctypeIndex != -1 ? doctypeIndex + doctype.length() : 0, html.length());
                String injectJs = "<!DOCTYPE html>\r\n<script>" + getInjectJs() + "</script>\r\n";
                String injectHTML = injectJs + body;
                WebResourceResponse webResourceResponse = new WebResourceResponse("text/html", "utf-8", new ByteArrayInputStream(injectHTML.getBytes()));
                return webResourceResponse;
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return super.shouldInterceptRequest(view, request);
    }
}

敲黑板!下面进入正题,针对这个拦截demo,我都踩了哪些坑?

坑1:Request缺少cookie

WebView请求不被拦截的话,请求时会自动携带CookieManager中当前domain下的cookie,然而拦截后请求通过OkHttpClient代发,就需要手动去加回cookie。

Request req = new Request.Builder().url(url).get().headers(HttpApis.createHeaders(url)).build();
String cookie = CookieUtils.getCookiesByUrl(url);
if (!TextUtils.isEmpty(cookie)) {
    req = req.newBuilder().addHeader("cookie", cookie).build();
}
Response response = okHttpClient.newCall(req).execute();

坑2:Response header丢失

当把OkHttp的Response重新组装回WebResourceResponse返回时,只组装了Response的body部分,而丢失了Response Header,需要手动加回。并且由于Response.Header的数据类型与WebResorceResponse接收的headers类型不一致,还需要手动做一层转换。

Headers headers = response.headers();
if (headers != null) {
   Map<String, List<String>> headersMap = headers.toMultimap();
   if (headersMap.size() > 0) {
     for (String name : headers.names()) {
            for (String value : headersMap.get(name)) {
                   responseHeader.put(name, value);
                 }
            }
      }
      webResourceResponse.setResponseHeaders(responseHeader);
 }

坑3:Response set-cookie指令失效

虽然手动向WebResourceResponse中加回了Response Header,但Response的set-cookie指令却没生效。也就是说当Response Header中存在set-cookie,却没能写入对应的cookie数据。这里暂不清楚为什么,有谁知道的,还请多多赐教。于是,还需要手动向CookieManager中设置服务端返回的cookie数据。

//重组response会导致set-cookie不生效,需要手动添加cookie到CookieManager
 List<String> responseCookies = headersMap.get("set-cookie");
 if (responseCookies != null && !responseCookies.isEmpty()) {
      for (String cookie : responseCookies) {
           CookieUtils.addCookie(url, cookie);
      }
}

坑4:302重定向失败

当拦截请求返回的Response设置了302重定向指令时,有时候无法生效,常见于POST请求,这时候需要手动重定向,并且解析prioriResponse设置cookie。

if (response.priorResponse() != null
      && response.priorResponse().code() == 302
      && !response.request().url().toString().equals(loadUrl)) {
          //跟坑3中一样,需要手动组装header,并设置cookie        
          handleResponseHeader(response.priorResponse());
          ThreadUtils.runOnUIThread(() -> loadUrl(response.request().url().toString()));
  }

坑5:webview.reload()场景导致拦截失败

demo中的需求是拦截页面dom请求,而不会去拦截资源请求(css,png,js等),也就是说只有当请求的url是当前webview加载的loadUrl时才会进行拦截,并且某些时候,webview需要reload,比如加载失败后触发reload重试。问题在于,reload请求返回的url有时候会被转义编码,导致与原loadUrl在字符串上并不相等。比如:

原url:
https://host/path?backurl=https://host/m/Management/Management
reload后url:
https://host/path?backurl=https%3A%2F%2Fhost%2Fm%2FManagement%2FManagement

可以发现,当url携带的参数中又包含一个url字串,存在特殊字符,webview reload请求时,返回的url会将特殊字符进行编码,从而url.equals(loadUrl)返回了false,导致请求被过滤掉了,没能成功拦截。解决办法是显而易见的,要么对loadUrl也进行同样的编码处理,要么避免使用reload。

webview.loadUrl(loadUrl) 
替换 
webview.reload()

本文已同步发在微信公众号“是个写代码的”,欢迎关注。