本文来自于CSDN博客,作者:Shawn_Dut,已获授权,版权归原作者所有,未经作者同意,请勿转载。
欢迎同有博客好文章的作者加微信(ID:tm_forever_miss)或直接邮件(mobilehub@csdn.net)投稿、约稿、给文章纠错。
这篇博客主要来介绍 WebView 的相关使用方法,常见的几个漏洞,开发中可能遇到的坑和最后解决相应漏洞的源码,以及针对该源码的解析。
由于博客内容长度,这次将分为上下两篇,上篇详解 WebView 的使用,下篇讲述 WebView 的漏洞和坑,以及修复源码的解析。
下篇:Android WebView详解,常见漏洞详解和安全源码(下)
Android Hybrid 和 WebView 解析
现在市面上的 APP 根据类型大致可以分为 3 类:Native APP、Web APP 和 Hybrid APP,而 Hybrid APP 兼具 “Native APP 良好用户交互体验的优势”和 “Web APP 跨平台开发的优势”,现在很多的主流应用也是使用 Hybrid 模式开发的。
Hybrid 的优势与原生的体验差距
Hybrid 的优势
为什么要使用 Hybrid 开发呢,这就要提到 native 开发的限制:
1.客户端发板周期长
众所周知,客户端的发板周期在正常情况下比较长,就算是创业公司的迭代也在一到两个星期一次,大公司的迭代周期一般都在月这个数量级别上,而且 Android 还好,iOS 的审核就算变短了也有几天,而且可能会有审核不通过的意外情况出现,所谓为了应对业务的快速发展,很多业务比如一些活动页面就可以使用 H5 来进行开发。
2.客户端大小体积受限
如果所有的东西都使用 native 开发,比如上面提到的活动页面,就会造成大量的资源文件要加入到 APK 中,这就造成 APK 大小增加,而且有的活动页面更新很快,造成资源文件可能只会使用一个版本,如果不及时清理,就会造成资源文件的残留。
3.web 页面的体验问题
使用纯 Web 开发,比以前迭代快速很多,但是从某种程度上来说,还是不如原生页面的交互体验好;
4.无法跨平台
一般情况下,同一样的页面在 android 和 iOS 上需要写两份不同的代码,但是现在只需要写一份即可,Hybrid 具有跨平台的优势。
所以综上这两种方式单独处理都不是特别好,考虑到发版周期不定,而且体验交互上也不能很差,所以就把两种方式综合起来,让终端和前端共同开发一个 APP,这样一些迭代很稳定的页面就可以使用原生,增加体验性;一些迭代很快速的页面就可以使用 H5,让两种优点结合起来,弥补原来单个开发模式的缺点。
H5 与 Native 的体验差距
H5 和 Native 的体验差距主要在两个方面:
1.页面渲染瓶颈
第一个是前端页面代码渲染,受限于 JS 的解析效率,以及手机硬件设备的一些性能,所以从这个角度来说,我们应用开发者是很难从根本上解决这个问题的;
2.资源加载缓慢
第二个方面是 H5 页面是从服务器上下发的,客户端的页面在内存里面,在页面加载时间上面,根据网络状况的不同,H5 页面的体验和 Native 在很多情况下相比差距还是不小的,但是这种问题从某种程度上来说也是可以弥补的,比如说我们可以做一些资源预加载的方案,在资源预加载方面,其实也有很多种方式,下面主要列举了一些:
第一种方式是使用 WebView 自身的缓存机制:
如果我们在 APP 里面访问一个页面,短时间内再次访问这个页面的时候,就会感觉到第二次打开的时候顺畅很多,加载速度比第一次的时间要短,这个就是因为 WebView 自身内部会做一些缓存,只要打开过的资源,他都会试着缓存到本地,第二次需要访问的时候他直接从本地读取,但是这个读取其实是不太稳定的东西,关掉之后,或者说这种缓存失效之后,系统会自动把它清除,我们没办法进行控制。基于这个 WebView 自身的缓存,有一种资源预加载的方案就是,我们在应用启动的时候可以开一个像素的 WebView ,事先去访问一下我们常用的资源,后续打开页面的时候如果再用到这些资源他就可以从本地获取到,页面加载的时间会短一些。
第二种方案是,我们自己去构建,自己管理缓存:
把这些需要预加载的资源放在 APP 里面,他可能是预先放进去的,也可能是后续下载的,问题在于前端这些页面怎么去缓存,两个方案,第一种是前端可以在 H5 打包的时候把里面的资源 URL 进行替换,这样可以直接访问本地的地址;第二种是客户端可以拦截这些网页发出的所有请求做替换:
这个是美团使用的预加载方案(详情请看:美团大众点评 Hybrid 化建设),归属于第二种加载方案,每当 WebView 发起资源请求的时候,我们会拦截这些资源的请求,去本地检查一下我们这些静态资源本地离线包有没有。针对本地的缓存文件我们有些策略能够及时的去更新它,为了安全考虑,也需要同时做一些预下载和安全包的加密工作。预下载有以下几点优势:
我们拦截了 WebView 里面发出的所有的请求,但是并没有替换里面的前端应用的任何代码,前端这套页面代码可以在 APP 内,或者其他的 APP 里面都可以直接访问,他不需要为我们 APP 做定制化的东西;
这些 URL 请求,他会直接带上先前用户操作所留下的 Cookie ,因为我们没有更改资源原始 URL 地址;
整个前端在用离线包和缓存文件的时候是完全无感知的,前端只用管写一个自己的页面,客户端会帮他处理好这样一些静态资源预加载的问题,有这个离线包的话,加载速度会变快很多,特别是在弱网情况下,没有这些离线包加载速度会慢一些。而且如果本地离线包的版本不能跟 H5 匹配的话,H5 页面也不会发生什么问题。
实际资源预下载也确实能够有效的增加页面的加载速度,具体的对比可以去看美团的那片文章。
那么什么地方需要使用 Native 开发,什么地方需要使用 H5 开发呢:一般来说 Hybrid 是用在一些快速迭代试错的地方,另外一些非主要产品的页面,也可以使用 Hybrid 去做;但是如果是一些很重要的流程,使用频率很高,特别核心的功能,还是应该使用 Native 开发,让用户得到一个极致的产品体验。
WebView 详细介绍
我们来看看 Google 官网关于 WebView 的介绍:
A View that displays web pages. This classis the basis upon which you can roll your own web browser or simply display some online content within your Activity. It uses the WebKit rendering engine to display web pages and includes methods to navigate forward and backward through a history, zoom inand out, perform text searches and more.
可以看到 WebView 是一个显示网页的控件,并且可以简单的显示一些在线的内容,并且基于 WebKit 内核,在 Android4.4(API Level 19) 引入了一个基于 Chromium 的新版本 WebView ,这让我们的 WebView 能支持 HTML5 和 CSS3 以及 Java,有一点需要注意的是由于 WebView 的升级,对于我们的程序也带来了一些影响,如果我们的 targetSdkVersion 设置的是 18 或者更低, single and narrow column 和 default zoom levels 不再支持。Android4.4 之后有一个特别方便的地方是可以通过 setWebContentDebuggingEnabled() 方法让我们的程序可以进行远程桌面调试。
WebView 加载页面
WebView 有四个用来加载页面的方法:
loadUrl (String url)
loadUrl (String url, Map<String,String>additionalHttpHeaders)
loadData(String data, String mimeType, String encoding)
loadDataWithBaseURL(String baseUrl, String data, String mimeType, String encoding, String historyUrl)
使用起来较为简单,loadData 方法会有一些坑,在下面的内容会介绍到。
WebView 常见设置
使用 WebView 的时候,一般都会对其进行一些设置,我们来看看常见的设置:
WebSettings webSettings = webView.getSettings();
//设置了这个属性后我们才能在 WebView 里与我们的 Js 代码进行交互,对于 WebApp 是非常重要的,默认是 false,
//因此我们需要设置为 true,这个本身会有漏洞,具体的下面我会讲到
webSettings.setJavaEnabled( true);
//设置 JS 是否可以打开 WebView 新窗口
webSettings.setJavaCanOpenWindowsAutomatically( true);
//WebView 是否支持多窗口,如果设置为 true,需要重写
//WebChromeClient#onCreateWindow(WebView, boolean, boolean, Message) 函数,默认为 false
webSettings.setSupportMultipleWindows( true);
//这个属性用来设置 WebView 是否能够加载图片资源,需要注意的是,这个方法会控制所有图片,包括那些使用 data URI 协议嵌入的图片。使用 setBlockNetworkImage(boolean) 方法来控制仅仅加载使用网络 URI 协议的图片。需要提到的一点是如果这个设置从 false 变为 true 之后,所有被内容引用的正在显示的 WebView 图片资源都会自动加载,该标识默认值为 true。
webSettings.setLoadsImagesAutomatically( false);
//标识是否加载网络上的图片(使用 http 或者 https 域名的资源),需要注意的是如果 getLoadsImagesAutomatically()
//不返回 true,这个标识将没有作用。这个标识和上面的标识会互相影响。
webSettings.setBlockNetworkImage( true);
//显示WebView提供的缩放控件
webSettings.setDisplayZoomControls( true);webSettings.setBuiltInZoomControls( true);
//设置是否启动 WebView API,默认值为 false
webSettings.setDatabaseEnabled( true);
//打开 WebView 的 storage 功能,这样 JS 的 localStorage,sessionStorage 对象才可以使用
webSettings.setDomStorageEnabled( true);
//打开 WebView 的 LBS 功能,这样 JS 的 geolocation 对象才可以使用
webSettings.setGeolocationEnabled( true);webSettings.setGeolocationDatabasePath( "");
//设置是否打开 WebView 表单数据的保存功能
webSettings.setSaveFormData( true);
//设置 WebView 的默认 userAgent 字符串
webSettings.setUserAgentString( "");
//设置是否 WebView 支持 “viewport” 的 HTML meta tag,这个标识是用来屏幕自适应的,当这个标识设置为 false 时,页面布局的宽度被一直设置为 CSS 中控制的 WebView 的宽度;如果设置为 true 并且页面含有 viewport meta tag,那么被这个 tag 声明的宽度将会被使用,如果页面没有这个 tag 或者没有提供一个宽度,那么一个宽型 viewport 将会被使用。
webSettings.setUseWideViewPort( false);
//设置 WebView 的字体,可以通过这个函数,改变 WebView 的字体,默认字体为 "sans-serif"
webSettings.setStandardFontFamily( "");
//设置 WebView 字体的大小,默认大小为 16
webSettings.setDefaultFontSize( 20);
//设置 WebView 支持的最小字体大小,默认为 8
webSettings.setMinimumFontSize( 12);
//设置页面是否支持缩放
webSettings.setSupportZoom( true);
//设置文本的缩放倍数,默认为 100
webSettings.setTextZoom( 2);
然后还有最常用的 WebViewClient 和 WebChromeClient,WebViewClient主要辅助WebView执行处理各种响应请求事件的,比如:
Resource
onPageStart
onPageFinish
onReceiveError
onReceivedHttpAuthRequest
shouldOverrideUrlLoading
WebChromeClient 主要辅助 WebView 处理J ava 的对话框、网站 Logo、网站 title、load 进度等处理:
onCloseWindow(关闭WebView)
onCreateWindow
onJsAlert
onJsPrompt
onJsConfirm
onProgressChanged
onReceivedIcon
onReceivedTitle
onShowCustomView
WebView 只是用来处理一些 html 的页面内容,只用 WebViewClient 就行了,如果需要更丰富的处理效果,比如 JS、进度条等,就要用到 WebChromeClient,我们接下来为了处理在特定版本之下的 js 漏洞问题,就需要用到 WebChromeClient。
接着还有 WebView 的几种缓存模式:
LOAD_CACHE_ONLY
不使用网络,只读取本地缓存数据;
LOAD_DEFAULT
根据 cache-control 决定是否从网络上取数据;
LOAD_CACHE_NORMAL
API level 17 中已经废弃, 从 API level 11 开始作用同 LOAD_DEFAULT 模式 ;
LOAD_NO_CACHE
不使用缓存,只从网络获取数据;
LOAD_CACHE_ELSE_NETWORK
只要本地有,无论是否过期,或者 no-cache,都使用缓存中的数据。
www.baidu.com 的 cache-control 为 no-cache,在模式 LOAD_DEFAULT 下,无论如何都会从网络上取数据,如果没有网络,就会出现错误页面;在 LOAD_CACHE_ELSE_NETWORK 模式下,无论是否有网,只要本地有缓存,都会加载缓存。本地没有缓存时才从网络上获取,这个和 Http 缓存一致,我不在过多介绍,如果你想自定义缓存策略和时间,可以尝试下,volley 就是使用了 http 定义的缓存时间。
清空缓存和清空历史记录,CacheManager 来处理 webview 缓存相关:mWebView.clearCache(true);;清空历史记录mWebview.clearHistory();,这个方法要在 onPageFinished() 的方法之后调用。
WebView 与 native 的交互
使用 Hybrid 开发的 APP 基本都需要 Native 和 web 页面的 JS 进行交互,下面介绍一下交互的方式。
js 调用 native
如何让 web 页面调用 native 的代码呢,有三种方式:
第一种方式:通过 addJavaInterface 方法进行添加对象映射
这种是使用最多的方式了,首先第一步我们需要设置一个属性:
mWebView.getSettings().setJavaEnabled( true);
这个函数会有一个警告,因为在特定的版本之下会有非常危险的漏洞,我们下面将会着重介绍到,设置完这个属性之后,Native 需要定义一个类:
public classJSObject { private Context mContext; public JSObject(Context context) { mContext = context; } @JavaInterface public StringshowToast( Stringtext) { Toast.show(mContext, text, Toast.LENGTH_SHORT).show(); return"success"; }}...
//特定版本下会存在漏洞
mWebView.addJavaInterface( newJSObject( this), "myObj"); 需要注意的是在 API17 版本之后,需要在被调用的地方加上 @addJavaInterface 约束注解,因为不加上注解的方法是没有办法被调用的,JS 代码也很简单:
functionshowToast(){
varresult = myObj.showToast( "我是来自web的Toast");}
可以看到,这种方式的好处在于使用简单明了,本地和 JS 的约定也很简单,就是对象名称和方法名称约定好即可,缺点就是下面要提到的漏洞问题。
第二种方式:利用 WebViewClient 接口回调方法拦截 url
这种方式其实实现也很简单,使用的频次也很高,上面我们介绍到了 WebViewClient ,其中有个回调接口shouldOverrideUrlLoading (WebView view, String url),我们就是利用这个拦截 url,然后解析这个 url 的协议,如果发现是我们预先约定好的协议就开始解析参数,执行相应的逻辑,我们先来看看这个函数的介绍:
Give the host application a chance to take over the control when a newurl is about to be loaded inthe current WebView. If WebViewClient is not provided, by defaultWebView will ask Activity Manager to choose the proper handler forthe url. If WebViewClient is provided, returntruemeans the host application handles the url, whilereturnfalsemeans the current WebView handles the url. This method is not called forrequests using the POST "method".
注意这个方法在 API24 版本已经废弃了,需要使用 shouldOverrideUrlLoading (WebView view, WebResourceRequest request) 替代,使用方法很类似,我们这里就使用shouldOverrideUrlLoading (WebView view, String url) 方法来介绍一下:
public boolean shouldOverrideUrlLoading(WebView view, Stringurl) {
//假定传入进来的 url = "js://openActivity?arg1=111&arg2=222",代表需要打开本地页面,并且带入相应的参数Uri uri = Uri.parse(url);
Stringscheme = uri.getScheme();
//如果 scheme 为 js,代表为预先约定的 js 协议if(scheme.equals( "js")) {
//如果 authority 为 openActivity,代表 web 需要打开一个本地的页面if(uri.getAuthority().equals( "openActivity")) {
//解析 web 页面带过来的相关参数HashMap< String, String> params = newHashMap<>(); Set< String> collection = uri.getQueryParameterNames();
for( Stringname : collection) { params.put(name, uri.getQueryParameter(name)); } Intent intent = newIntent(getContext(), MainActivity.class); intent.putExtra( "params", params); getContext().startActivity(intent); }
//代表应用内部处理完成returntrue; }
returnsuper.shouldOverrideUrlLoading(view, url);}
代码很简单,这个方法可以拦截 WebView 中加载 url 的过程,得到对应的 url,我们就可以通过这个方法,与网页约定好一个协议,如果匹配,执行相应操作,我们看一下 JS 的代码:
functionopenActivity(){
document.location = "js://openActivity?arg1=111&arg2=222";}
这个代码执行之后,就会触发本地的 shouldOverrideUrlLoading 方法,然后进行参数解析,调用指定方法。这个方式不会存在第一种提到的漏洞问题,但是它也有一个很繁琐的地方是,如果 web 端想要得到方法的返回值,只能通过 WebView 的 loadUrl 方法去执行 JS 方法把返回值传递回去,相关的代码如下:
//java
mWebView.loadUrl( "java:returnResult("+ result + ")"); //java
functionreturnResult(result){ alert( "result is"+ result);}
所以说第二种方式在返回值方面还是很繁琐的,但是在不需要返回值的情况下,比如打开 Native 页面,还是很合适的,制定好相应的协议,就能够让 web 端具有打开所有本地页面的能力了。
第三种方式:利用 WebChromeClient 回调接口的三个方法拦截消息
这个方法的原理和第二种方式原理一样,都是拦截相关接口,只是拦截的接口不一样:
@Overridepublic boolean onJsAlert(WebView view, Stringurl, Stringmessage, JsResult result) {
returnsuper.onJsAlert(view, url, message, result);}@Overridepublic boolean onJsConfirm(WebView view, Stringurl, Stringmessage, JsResult result) {
returnsuper.onJsConfirm(view, url, message, result);}@Overridepublic boolean onJsPrompt(WebView view, Stringurl, Stringmessage, StringdefaultValue, JsPromptResult result) {
//假定传入进来的 message = "js://openActivity?arg1=111&arg2=222",代表需要打开本地页面,并且带入相应的参数Uri uri = Uri.parse(message); Stringscheme = uri.getScheme();
if(scheme.equals( "js")) {
if(uri.getAuthority().equals( "openActivity")) { HashMap< String, String> params = newHashMap<>(); Set< String> collection = uri.getQueryParameterNames();
for( Stringname : collection) { params.put(name, uri.getQueryParameter(name)); } Intent intent = newIntent(getContext(), MainActivity.class); intent.putExtra( "params", params); getContext().startActivity(intent);
//代表应用内部处理完成result.confirm( "success"); } returntrue; }
returnsuper.onJsPrompt(view, url, message, defaultValue, result);}
和 WebViewClient 一样,这次添加的是 WebChromeClient 接口,可以拦截 JS 中的几个提示方法,也就是几种样式的对话框,在 JS 中有三个常用的对话框方法:
onJsAlert 方法是弹出警告框,一般情况下在 Android 中为 Toast,在文本里面加入n就可以换行;
onJsConfirm 弹出确认框,会返回布尔值,通过这个值可以判断点击时确认还是取消,true表示点击了确认,false表示点击了取消;
onJsPrompt 弹出输入框,点击确认返回输入框中的值,点击取消返回 null。
但是这三种对话框都是可以本地拦截到的,所以可以从这里去做一些更改,拦截这些方法,得到他们的内容,进行解析,比如如果是 JS 的协议,则说明为内部协议,进行下一步解析然后进行相关的操作即可,prompt 方法调用如下所示:
functionclickprompt(){
varresult=prompt( "js://openActivity?arg1=111&arg2=222"); alert( "open activity "+ result);}
这里需要注意的是 prompt 里面的内容是通过 message 传递过来的,并不是第二个参数的 url,返回值是通过 JsPromptResult 对象传递。为什么要拦截 onJsPrompt 方法,而不是拦截其他的两个方法,这个从某种意义上来说都是可行的,但是如果需要返回值给 web 端的话就不行了,因为 onJsAlert 是不能返回值的,而 onJsConfirm 只能够返回确定或者取消两个值,只有 onJsPrompt 方法是可以返回字符串类型的值,操作最全面方便。
以上三种方案的总结和对比
以上三种方案都是可行的,在这里总结一下:
第一种方式:
是现在目前最普遍的用法,方便简洁,但是唯一的不足是在 4.2 系统以下存在漏洞问题;
第二种方式:
通过拦截 url 并解析,如果是已经约定好的协议则进行相应规定好的操作,缺点就是协议的约束需要记录一个规范的文档,而且从 Native 层往 Web 层传递值比较繁琐,优点就是不会存在漏洞,iOS7 之下的版本就是使用的这种方式。
第三种方式:
和第二种方式的思想其实是类似的,只是拦截的方法变了,这里拦截了 JS 中的三种对话框方法,而这三种对话框方法的区别就在于返回值问题,alert 对话框没有返回值,confirm 的对话框方法只有两种状态的返回值,prompt 对话框方法可以返回任意类型的返回值,缺点就是协议的制定比较麻烦,需要记录详细的文档,但是不会存在第二种方法的漏洞问题。
native 调用 js
第一种方式
native 调用 js 的方法上面已经介绍到了,方法为:
//java
mWebView.loadUrl( "java:show("+ result + ")"); //java
< type= "text/java">
functionshow(result){ alert( "result"=result);
return"success";}< />
需要注意的是名字一定要对应上,要不然是调用不成功的,而且还有一点是 JS 的调用一定要在 onPageFinished 函数回调之后才能调用,要不然也是会失败的。
第二种方式
如果现在有需求,我们要得到一个 Native 调用 Web 的回调怎么办,Google 在 Android4.4 为我们新增加了一个新方法,这个方法比 loadUrl 方法更加方便简洁,而且比 loadUrl 效率更高,因为 loadUrl 的执行会造成页面刷新一次,这个方法不会,因为这个方法是在 4.4 版本才引入的,所以我们使用的时候需要添加版本的判断:
final int version = Build.VERSION.SDK_INT;
if(version < 18) { mWebView.loadUrl(jsStr);} else{ mWebView.evaluateJava(jsStr, newValueCallback< String>() { @Override public voidonReceiveValue( Stringvalue) {
//此处为 js 返回的结果} });}
两种方式的对比
一般最常使用的就是第一种方法,但是第一种方法获取返回的值比较麻烦,而第二种方法由于是在 4.4 版本引入的,所以局限性比较大。
WebView 常见漏洞和坑
常见漏洞和坑请看下篇博客:android WebView详解,常见漏洞详解和安全源码(下)
源码
源码解析请看下篇博客:android WebView详解,常见漏洞详解和安全源码(下)
下载源码:https://github.com/zhaozepeng/SafeWebView;
参考自:https://github.com/yushiwo/WebViewBugDemo,在此基础上做了一些优化。
引用
https://group.jobbole.com/26417/?utm_source=android.jobbole.com&utm_medium=sidebar-group-topic
https://blog.csdn.net/jiangwei0910410003/article/details/52687530
https://blog.csdn.net/leehong2005/article/details/11808557
https://github.com/yushiwo/WebViewBugDemo/blob/master/src/com/lee/webviewbug/WebViewEx.java
https://blog.csdn.net/sk719887916/article/details/52402470
https://zhuanlan.zhihu.com/p/24202408
https://github.com/lzyzsd/JsBridge
https://www.jianshu.com/p/93cea79a2443#
https://www.codexiu.cn/android/blog/33214/
https://github.com/pedant/safe-java-js-webview-bridge
https://blog.sina.com.cn/s/blog_777f9dbb0102v8by.html
https://www.cnblogs.com/chaoyuehedy/p/5556557.html
https://blogs.360.cn/360mobile/2014/09/22/webview%E8%B7%A8%E6%BA%90%E6%94%BB%E5%87%BB%E5%88%86%E6%9E%90/
https://my.oschina.net/zhibuji/blog/100580
https://www.cnblogs.com/punkisnotdead/p/5062631.html?utm_source=tuicool&utm_medium=referral
了解最新移动开发、VR/AR 干货技术分享,请关注 mobilehub 微信公众号(ID: mobilehub)。
标签: app客户端源码分析