总的来说,拦截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()
本文已同步发在微信公众号“是个写代码的”,欢迎关注。