Android WebView那些事儿

Android WebView经常会有一些奇奇怪怪的问题,为了避免日后重复采坑,统一在这里总结下:

setWebContentsDebuggingEnabled

setWebContentsDebuggingEnabled()用来配置WebView是否支持远程调试,当被设置为true时,既可以用PC端的Chrome来调试手机端的WebView。具体操作如下:

  1. WebView配置如下

    1
    2
    3
    4
    //KITKAT版本一下不支持这个API
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
    WebView.setWebContentsDebuggingEnabled(true);
    }
  2. 手机通过USB连接PC,在Chrome下输入”chrome://inspect”即可进入调试页,同时会显示出所有可调式的WebView页,审查元素、network等调试操作和PC上相同。

upload successful

setWebChromeClient和setWebViewClient

  • WebViewClient主要帮助WebView处理各种通知、请求事件的,比如:
    onLoadResource,onPageStart,onPageFinish,onReceiveError,onReceivedHttpAuthRequest 等。
  • WebChromeClient主要辅助WebView处理Javascript的对话框、网站图标、网站title、加载进度等,比如:
    onCloseWindow(关闭WebView),onCreateWindow(),onJsAlert (WebView上alert无效,需要定制WebChromeClient处理弹出),onJsPrompt,onJsConfirm,onProgressChanged,onReceivedIcon,onReceivedTitle 等等。

addjavascriptinterface

addjavascriptinterface可以把一个java对象导出,通过js来调用java对象。如果这个对象涉及到敏感操作,有可能产生安全问题。

使用实例如下:

定义一个AndroidToast,实现弹出一个toast功能,并将其导出

1
2
3
4
5
6
public class AndroidToast {
@JavascriptInterface
public void show(String str) {
Toast.makeText(MainActivity.this, str, Toast.LENGTH_SHORT).show();
}
}

WebView进行设置,开启JavaScipt,注册JavascriptInterface的方法

1
2
3
4
5
6
7
8
9
private void initView() {
webView = (WebView) findViewById(R.id.webView);

WebSettings webSettings = webView.getSettings();
webSettings.setJavaScriptEnabled(true);
webSettings.setDefaultTextEncodingName("UTF-8");
webView.addJavascriptInterface(new AndroidToast(), "AndroidToast");
webView.loadUrl("file:///android_asset/index.html");
}

在javascript中调用方法,通过window属性可以找到映射的对象AndroidToast,直接调用它的show方法即可。

1
2
3
function toastClick(){
window.AndroidToast.show('from js');
}

WebResourceResponse

拦截web返回请求,配合其他漏洞造成本地文件泄露,参考https://blog.oversecured.com/Android-Exploring-vulnerabilities-in-WebResourceResponse/

setGeolocationEnabled

setGeolocationEnabled用来配置是否允许H5定位,默认为true。通过一定的配置,可以使WebView中的H5定位时,用户不会受到任何定位提示。

参照官方开发文档,实现h5定位需要满足三个条件:

  • APP本身必须具有定位权限
  • APP必须实现onGeolocationPermissionsShowPrompt(String, GeolocationPermissions.Callback) 这个回调函数,当js调用h5定位的API时,这个函数会被调用
  • 发起定位请求的站点必须为可信origin,如https,http的将不允许定位

下边这行代码用来处理定位请求,第一个参数是发起定位请求的origin,第二个 boolean 类型的参数表示是否授予网页定位权限;而第三个 boolean 类型的参数则表示是否保留这个权限状态。

1
callback.invoke(origin, true, true);

具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
前端实现
<body>
<p id="demo">点击按钮获取您当前坐标(可能需要比较长的时间获取):</p>
<button onclick="getLocation()">点我</button>
<script>
var x=document.getElementById("demo");
function getLocation()
{
if (navigator.geolocation)
{
navigator.geolocation.getCurrentPosition(showPosition);
}
else
{
x.innerHTML="该浏览器不支持获取地理位置。";
}
}

function showPosition(position)
{
x.innerHTML="纬度: " + position.coords.latitude +
"<br>经度: " + position.coords.longitude;
}
</script>
</body>

1
2
3
4
Manifest文件:
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.INTERNET"/>
1
2
3
4
5
6
7
8
9
10
11
12
WebView contentWv = (WebView) findViewById(R.id.wv_content);
WebSettings settings = contentWv.getSettings();
settings.setJavaScriptEnabled(true);
//setGeolocationEnabled默认为true
contentWv.setWebChromeClient(new WebChromeClient(){
@Override
public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) {
callback.invoke(origin, true, true);
super.onGeolocationPermissionsShowPrompt(origin, callback);
}
});
contentWv.loadUrl("file:///android_asset/location.html");

一般比较合适的做法是,在该回调函数中设置一个对话框,告知用户是否授权定位操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
wv.setWebChromeClient(new WebChromeClient(){

@Override
public void onGeolocationPermissionsShowPrompt(final String origin, final GeolocationPermissions.Callback callback) {
AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
builder.setMessage("Allow to access location information?");
DialogInterface.OnClickListener dialogButtonOnClickListener = new DialogInterface.OnClickListener() {

@Override
public void onClick(DialogInterface dialog, int clickedButton) {
if (DialogInterface.BUTTON_POSITIVE == clickedButton) {
callback.invoke(origin, true, true);//允许
} else if (DialogInterface.BUTTON_NEGATIVE == clickedButton) {
callback.invoke(origin, false, false);//拒绝
}
}
};
builder.setPositiveButton("Allow", dialogButtonOnClickListener);
builder.setNegativeButton("Deny", dialogButtonOnClickListener);
builder.show();
}

}

);