写点什么

Android WebView 使用与 JS 交互

作者:松柏不怕雪
  • 2023-04-22
    广东
  • 本文字数:8923 字

    阅读完需:约 29 分钟

Android WebView使用与JS交互

一、WebView 简单使用

WebView 是 Android 系统中的原生控件,其主要功能与前端页面进行响应交互,快捷省时地实现如期的功能,相当于增强版的内置浏览器。使用时需要在配置文件里设置网络权限,定义布局大小和样式,绑定和操作控件。


manifest.xml<uses-permission android:name="android.permission.INTERNET" />
activity_main.xml<WebView android:id="@+id/web_view" android:layout_width="match_parent" android:layout_height="match_parent"></WebView>
MainActivity.javaString url = "https://www.baidu.com";WebView webView = (WebView) findViewById(R.id.web_view);webView.loadUrl(url);
网页跳转,包括前进、后退、自定义。boolean back = webview.canGoBack(); -> 判断网页是否可以回退webview.goBack(); -> 回退一页
boolean forward = webview.canGoForward(); -> 判断网页是否可以前进webview.goForward(); -> 前进一页
webview.goBackOrForward(1); -> 正数为前进webview.goBackOrForward(-1); -> 负数为后退
复制代码

二、WebView 生命周期

onResume():活跃状态,可以正常执行网页的响应。onPause():暂停状态,页面被失去焦点,暂停所有进行中的动作,如:DOM解析、CSS和JavaScript执行等。pauseTimers():全局WebView暂停状态,如:layout、parsing、javascripttimer等。resumeTimers():恢复到pauseTimers()执行前的状态。destroy():销毁状态,释放资源。注意:使用WebView不当容易引起内存泄漏,所以WebView的生命周期方法应跟随Activity的生命周期的方法来调用。
复制代码

三、WebSettings

控件的相关配置WebSettings webSettings = webView.getSettings();
webSettings.setJavaScriptEnabled(true); -> 是否开启JS支持webSettings.setPluginsEnabled(true); -> 是否开启插件支持webSettings.setJavaScriptCanOpenWindowsAutomatically(true); -> 是否允许JS打开新窗口
webSettings.setUseWideViewPort(true); -> 缩放至屏幕大小webSettings.setLoadWithOverviewMode(true); -> 缩放至屏幕大小webSettings.setSupportZoom(true); -> 是否支持缩放webSettings.setBuiltInZoomControls(true); -> 是否支持缩放变焦,前提是支持缩放webSettings.setDisplayZoomControls(false); -> 是否隐藏缩放控件
webSettings.setAllowFileAccess(true); -> 是否允许访问文件webSettings.setDomStorageEnabled(true); -> 是否节点缓存webSettings.setDatabaseEnabled(true); -> 是否数据缓存webSettings.setAppCacheEnabled(true); -> 是否应用缓存webSettings.setAppCachePath(uri); -> 设置缓存路径
webSettings.setMediaPlaybackRequiresUserGesture(false); -> 是否要手势触发媒体webSettings.setStandardFontFamily("sans-serif"); -> 设置字体库格式webSettings.setFixedFontFamily("monospace"); -> 设置字体库格式webSettings.setSansSerifFontFamily("sans-serif"); -> 设置字体库格式webSettings.setSerifFontFamily("sans-serif"); -> 设置字体库格式webSettings.setCursiveFontFamily("cursive"); -> 设置字体库格式webSettings.setFantasyFontFamily("fantasy"); -> 设置字体库格式webSettings.setTextZoom(100); -> 设置文本缩放的百分比webSettings.setMinimumFontSize(8); -> 设置文本字体的最小值(1~72)webSettings.setDefaultFontSize(16); -> 设置文本字体默认的大小
webSettings.setLayoutAlgorithm(LayoutAlgorithm.SINGLE_COLUMN); -> 按规则重新布局webSettings.setLoadsImagesAutomatically(false); -> 是否自动加载图片webSettings.setDefaultTextEncodingName("UTF-8"); -> 设置编码格式webSettings.setNeedInitialFocus(true); -> 是否需要获取焦点webSettings.setGeolocationEnabled(false); -> 设置开启定位功能webSettings.setBlockNetworkLoads(false); -> 是否从网络获取资源
复制代码

四、WebViewClient

控件客户端,用于处理各种通知和请求事件。onPageStarted():页面开始加载时调用,这时候可以显示加载进度条,让用户耐心等待页面的加载。onPageFinished():页面完成加载时调用,这时候可以隐藏加载进度条,提醒用户页面已经完成加载。onLoadResource():页面每次加载资源时调用。shouldOverrideUrlLoading():WebView加载url默认会调用系统的浏览器,通过重写该方法,实现在当前应用内完成页面加载。onReceivedError():页面加载发生错误时调用,这时候可以跳转到自定义的错误提醒页面,总比系统默认的错误页面美观,优化用户体验。onReceivedHttpError():页面加载请求时发生错误。onReceivedSslError():页面加载资源时发生错误。shouldOverrideKeyEvent():覆盖按键默认的响应事件,这时候可以根据自身的需求在点击某些按键时加入相应的逻辑。onScaleChanged():页面的缩放比例发生变化时调用,这时候可以根据当前的缩放比例来重新调整WebView中显示的内容,如修改字体大小、图片大小等。shouldInterceptRequest():可以根据请求携带的内容来判断是否需要拦截请求。WebViewClient webViewClient = new WebViewClient(){  @Override  public void onPageStarted(WebView view, String url, Bitmap favicon) {
}
@Override public void onPageFinished(WebView view, String url) {
}
@Override public boolean onLoadResource(WebView view, String url) {
}
@Override public boolean shouldOverrideUrlLoading(WebView view, String url) { view.loadUrl(url); return true; -> 消费事件终止传递 }
@Override public void onReceivedError(WebView view, int errorCode, String description, String failingUrl){ view.loadUrl("file:///android_assets/error.html"); -> assets目录下放置文件}
webView.setWebViewClient(webViewClient);
复制代码

五、WebChromeClient

浏览器客户端,用于处理网站图标、网站标题、网站弹窗等。onProgressChanged():页面加载进度发生变化时调用,可以通过该方法实时向用户反馈加载情况,如显示进度条等。onReceivedIcon():接收Web页面的图标,可以通过该方法把图标设置在原生的控件上,如Toolbar等。onReceivedTitle():接收Web页面的标题,可以通过该方法把图标设置在原生的控件上,如Toolbar等。onJsAlert():处理JS的Alert对话框。onJsPrompt():处理JS的Prompt对话框。onJsConfirm():处理JS的Confirm对话框。onPermissionRequest():Web页面请求Android权限时调用。onPermissionRequestCanceled():Web页面请求Android权限被取消时调用。onShowFileChooser():Web页面上传文件时调用。getVideoLoadingProgressView():自定义媒体文件播放加载时的进度条。getDefaultVideoPoster():设置媒体文件默认的预览图。onShowCustomView():媒体文件进入全屏时调用。onHideCustomView():媒体文件退出全屏时调用。WebChromeClient webChromeClient = new WebChromeClient();
复制代码

六、Js 调用 Android

  • 方式一:通过 webView.addJavascriptInterface() 添加桥梁类


注:该方法针对Android 4.2(API 17)及以上,只有标有@JavascriptInterface注解的public方法才能从js调用。而对targetSdkVersion为API Level 16及以下的app,js可以调用Java所有的public方法。 前置设定:webview.getSettings().setJavaScriptEnabled(true)
复制代码


addJavascriptInterface()安全风险及解决办法


Android 代码


public class Bridge{    @JavascriptInterface    public void jsInvoke(String jsonParams) {    //获取js传递过来的数据,并解析出方法名        try {            JSONObject jsonObject = new JSONObject(json);            final String callback = jsonObject.optString("callbackMethod");            //根据获得的方法名,安卓进行回调,回调前封装数据过去            //封装数据            final JSONObject data = new JSONObject();            data.put("name", "zhangsan");            data.put("age", "38");//    Toast.makeText(mContext,json,Toast.LENGTH_LONG).show();            mHandler.post(new Runnable() {                @Override                public void run() {                    //回调,需要保证在主线程中                    mWebView.loadUrl("javascript:" + callbackMethod + "(" + data.toString() + ")");                }            });        } catch (JSONException e) {            e.printStackTrace();        }    }        //JavascriptInterface 可以标注多个方法    @JavascriptInterface    public void xxxx(String jsonParams) {    }} Bridge bridge = new Bridge();mWebView.addJavascriptInterface(bridge,"androidJsInterface");
复制代码


Js 代码


//将需要 安卓回调的方法名 以callbackMethod方式传递  action 可自定义操作类型  callbackMethod  App可调用的JS方法,用于Js的回调 // 注:androidJsInterface 由安卓传递的对象别名,也就是Js能拿到java端的桥对象
例子1:function xxx(){var json = {"action":"startScan","callbackMethod":"getScanResult","port":"xxx"}; window.androidJsInterface.jsInvoke(JSON.stringify(json));}例子2: function xxx(){var json = {"action":"otherAction","callbackMethod":"otherCallback","port":"xxx"};window.androidJsInterface.jsInvoke(JSON.stringify(json));}例子3:function xxx(){var json = {"action":"noCallback","callbackMethod":"","port":"xxx"};window.androidJsInterface.jsInvoke(JSON.stringify(json));}
复制代码


  • 方式二:通过 webViewClient.shouldOverrideUrlLoading()


注:这个方法的本来是拦截所有WebView的Url跳转的。我们可以通过这个API 构造一个特殊自定义格式的Url跳转,shouldOverrideUrlLoading拦截Url后判断其格式,然后Native 解析到对应的方法和数据再去执行本地方法。
复制代码


Android 代码


webview.setWebViewClient(new WebViewClient() {            @Override            public boolean shouldOverrideUrlLoading(WebView view, String url) {                Log.i("qcl0228", "拦截到的url:" + url);                //url如果以qiushi开头,就是h5和我们定义的传值协议                if (url.startsWith("qiushi")) {                    Uri uriRequest = Uri.parse(url);                    String scheme = uriRequest.getScheme();                    String action = uriRequest.getHost();                    String query = uriRequest.getQuery();                    if ("qiushi".equals(scheme)) {                        if (!TextUtils.isEmpty(query)) {                            //把url携带的参数存到一个map里                            HashMap maps = new HashMap();                            Set<String> names = uriRequest.getQueryParameterNames();                            for (String name : names) {                                maps.put(name, uriRequest.getQueryParameter(name));                            }                            JSONObject jsonObject = new JSONObject(maps);                            if ("setH5Info".equals(action)) {                                if (jsonObject != null && jsonObject.has("params")) {                                    String h5InfoParams = jsonObject.optString("params");                                    Log.i("qcl0228", "拦截到的参数:" + h5InfoParams);                                }                            }                        }                    }                } else {                    view.loadUrl(url);                }                return true;            }        });
复制代码


拦截到的 url:qiushi://setH5Info?param=%7B%22title%22:%22%E5%95%86%E5%93%81%E6%A0%87%E9%A2%98%22%7D 拦截到的参数:{"title":"商品标题"}


Js 代码


//简单重定向<div id="div" onclick="chongdingxiang()"style="width:100px; height:100px; background-color:#099;"></div><script type="text/javascript">function chongdingxiang(){ //模拟重定向 window.location.href="qiushi://setH5Info?params=%7B%22title%22%3A%22%E5%95%86%E5%93%81%E8%AF%A6%E6%83%85%22%7D";}</script>
复制代码

七、Android 调用 Js

  • 方式一 webView.loadUrl()


注: 这是无JS返回值的调用    且js方法不能有返回值,如下js代码 若有返回值,web则会发生请求重定向问题    function jsMethod(jsonParams){              return "123456";          };
复制代码


若调用的JS有返回值,webView会发生请求重定向


android 代码


        String jsonParams = "123456";      //String url = "javascript:jsMethod()";//不拼接参数,直接调用js的jsMethod函数        String url = "javascript:jsMethod(" + jsonParams + ")";//拼接参数,就可以把数据传递给js        webview.loadUrl(url);
复制代码


js 代码


     例子1 function jsMethod(){                 console.log("callbackMethod执行了");             };
例子2 function jsMethod(jsonParams){ console.log("callbackMethod执行了"); alert(JSON.stringify(jsonParams)); };
例子3 fun HyJsBridge.onComplete(port,json){ };
复制代码


  • 方式二 webView.evaluateJavascript()


注:可以获取js方法的返回值,仅支持android4.4及以上
复制代码


android 代码


  callback =new ValueCallback<String>() {                @Override                public void onReceiveValue(String value) {                   Log.i("qcl0228", "js返回的数据" + value);                }            });mWebView.evaluateJavascript("javascript:jsMethod("+ jsonParams +")",callback);
或者不需要回调mWebView.evaluateJavascript("javascript:jsMethod("+ jsonParams +")",null);
复制代码


js 代码


// js中的方法  --> 将会被Android端调用     例子1 function jsMethod(jsonParams){                    return "123456";             };
复制代码


js 返回的数据:123456

八、Html Demo

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html xmlns="http://www.w3.org/1999/xhtml"><head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> <title>Untitled Document</title></head><body><div id="div" onclick="getAndroidValue()"style="width:100px; height:100px; background-color:#099;"></div><script type="text/javascript">function chongdingxiang(){ //模拟重定向 window.location.href="qiushi://setH5Info?params=%7B%22title%22%3A%22%E5%95%86%E5%93%81%E8%AF%A6%E6%83%85%22%7D";}function jsMethod(jsonParams){ document.getElementById("div").style.backgroundColor='red'; document.getElementById("div").innerHTML=jsonParams; return '123456';}function getAndroidValue(){ document.getElementById("div").innerHTML=window.androidObject.androidMethod();}</script></body></html>
复制代码

九、Webview 漏洞及安全设置

  • 任意代码执行漏洞


JS调用Android代码是通过addJavascriptInterface接口进行对象映射。
复制代码


漏洞产生的原因


// java代码public class AndroidJS extends Object {    @JavascriptInterface    public void hello(String msg) {        System.out.println("JS调用了Android的hello方法");    }}//参数1:Android的本地对象 参数2:对象别名webView.addJavascriptInterface(new AndroidJS(), "test");
复制代码


// JS代码function callAndroid(){  test.hello("js调用了android中的hello方法");}
复制代码


JS 通过 test 对象别名 映射到 Android 的地对象 AndroidJs(),从而实现 JS 调用 Android 的对象和方法。


漏洞产生原因是:当 JS 拿到 Android 这个对象后,就可以调用这个 Android 对象中所有的方法,包括系统类(java.lang.Runtime 类),从而进行任意代码执行。如可以执行命令获取本地设备的 SD 卡中的文件等信息从而造成信息泄露


具体获取系统类的描述(结合 Java 反射机制):Android 中的对象有一公共的方法——getClass(),该方法可以获取到当前类类型 Class,该类有一关键的方法——Class.forName,该方法可以加载一个类(可加载 java.lang.Runtime 类),而该类是可以执行本地命令的。


以下是攻击的 JS 核心代码:


function execute(cmdArgs) {    // 步骤1:遍历window对象,目的是为了找到包含getClass()的对象,因为Android映射的JS对象在window中,所以肯定会遍历到  for (var obj in window) {      if ("getClass" in window[obj]) {        // 步骤2:利用反射调用forName方法得到Runtime类对象      alert(obj);                return  window[obj].getClass().forName("java.lang.Runtime")        // 步骤3:以后就可以调用静态方法来执行一些命令,比如访问文件的命令      getMethod("getRuntime",null).invoke(null,null).exec(cmdArgs);        // 从执行命令后返回的输入流中得到字符串,有很严重暴露隐私的危险      // 如执行完访问文件的命令之后,就可以得到文件名的信息了    }    }  }   
复制代码


当一些 APP 通过扫描二维码打开一个外部网页时,攻击者就可以执行这段 JS 代码进行漏洞攻击。


解决方案:


方案 1:在 Android 4.2 版本之后,Google 在 Android 4.2 版本中规定对被调用的函数以 @JavascriptInterface 进行注解从而避免漏洞攻击。


方案 2:Android 4.2 版本之前,采用拦截 prompt()进行漏洞修复。 具体如下:


每次当 WebView 加载页面前加载一段本地的 JS 代码,原理是:让 JS 调用一 Javascript 方法:该方法是通过调用 prompt 把 JS 中的信息(含特定标识,方法名称等)传递到 Android 端;在 Android 的 onJsPrompt 中 ,解析传递过来的信息,再通过反射机制调用 Java 对象的方法,这样实现安全的 JS 调用 Android 代码;(关于 Android 返回给 JS 的值,可通过 prompt 把 Java 中方法的处理结果返回到 JS 中)


javascript:(function JsAddJavascriptInterface_() {    // window.jsInterface表示在window上声明了一个Js对象,jsInterface = 注册的对象名  // 它注册了两个方法,onButtonClick(arg0)和onImageClick(arg0, arg1, arg2),如果有返回值,就添加上return  if (typeof(window.jsInterface)!='undefined') {          console.log('window.jsInterface_js_interface_name is exist!!');  } else {      window.jsInterface = {     
onButtonClick:function(arg0) { // prompt()返回约定的字符串,该字符串可自己定义 // 包含特定的标识符MyApp和JSON字符串(方法名,参数,对象名等) return prompt('MyApp:'+JSON.stringify({obj:'jsInterface',func:'onButtonClick',args:[arg0]})); },
onImageClick:function(arg0,arg1,arg2) { return prompt('MyApp:'+JSON.stringify({obj:'jsInterface',func:'onImageClick',args:[arg0,arg1,arg2]})); }, }; } })()
复制代码


  • 密码明文存储漏洞 WebView 默认开启密码保存功能:webView.setSavePassword(true)。开启后,在用户输入密码时,会弹出提示框:询问用户是否保存密码;如果选择”是”,密码会被明文保到 /data/data/com.package.name/databases/webview.db 中,这样就有被盗取密码的危险。


关闭密码保存提醒:webSettings.setSavePassword(false)
复制代码


  • 域控制不严格漏洞


如下代码:public class WebViewActivity extends Activity {  private WebView webView;  public void onCreate(Bundle savedInstanceState) {    super.onCreate(savedInstanceState);    setContentView(R.layout.activity_webview);    webView = (WebView) findViewById(R.id.webView);        //webView.getSettings().setAllowFileAccess(false);                    (1)    //webView.getSettings().setAllowFileAccessFromFileURLs(true);         (2)    //webView.getSettings().setAllowUniversalAccessFromFileURLs(true);    (3)    Intent i = getIntent();    String url = i.getData().toString(); //url = file:///data/local/tmp/attack.html     webView.loadUrl(url);  }}
复制代码


在清单文件中将 WebViewActivity 设置 android:exported = "true"属性,表示当前 Activity 是否可以被另一个 Application 的组件启动。即 A 应用可以通过 B 应用导出的 Activity 让 B 应用加载一个恶意的 file 协议的 url,从而可以获取 B 应用的内部私有文件,从而带来数据泄露威胁。


当其他应用启动可以允许外部调用的 Activity 时, intent 中的 data 直接被当作 url 来加载(假定传进来的 url 为 file:///data/local/tmp/attack.html),其它 APP 通过使用显式 ComponentName 或者其他类似方式就可以很轻松的启动该 WebViewActivity 并加载恶意 url。


对于不需要使用 file 协议的应用,禁用 file 协议:


setAllowFileAccess(false);
复制代码


对于需要使用 file 协议的应用,禁止 file 协议加载 JavaScript:


setAllowFileAccess(true); // 禁止file协议加载 JavaScriptif (url.startsWith("file://") {  setJavaScriptEnabled(false);} else {  setJavaScriptEnabled(true);}
复制代码

文章引述及参考

webview参数详解 潜力之心—简书android与js通信 编程的小石头—知乎android与js通信 竖子敢尔—CSDNweb安全漏洞 橙子19911016—CSDN

发布于: 刚刚阅读数: 4
用户头像

还未添加个人签名 2018-11-09 加入

还未添加个人简介

评论

发布
暂无评论
Android WebView使用与JS交互_webview_松柏不怕雪_InfoQ写作社区