Web调起APP研究

应用开发中经常会有从web端启动App的需求,这里总结了常见的几种方式。

Android

0x0 自定义scheme

Android上常用的是通过scheme协议的方式唤醒本地app客户端。即App在AndroidManifest.xml中为要打开的Activity注册一个intent-filter,添加scheme,并注明host和data等关键字段,前端保持一致后就可以实现在Web页中调起本地App了。

注册scheme的代码如下:

1
2
3
4
5
6
7
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<!--BROWSABLE指定该Activity能被浏览器安全调用-->
<data android:host="xyz.com" android:scheme="m4bln"/>
</intent-filter>

这里的action、category、data都必须完全匹配才能获得intent,android.intent.category.DEFAULT是默认的,有实际意义的是android.intent.category.BROWSABLE,表示允许通过浏览器启动该activity(调起App)。data限定了触发条件,当scheme为m4bln且host为xyz.com时才匹配,例如浏览器访问m4bln://xyz.com,能够匹配成功,App就起来了。

0x1 Web页打开App

通过Web页打开App有多种方式,打开方式也因直接在浏览器打开和在webview中打开而异,这里分别进行尝试:

a标签

1
<a href="m4bln://xyz.com">打开App</a>

location.href

1
document.location.href="m4bln://xyz.com"

iframe

1
<iframe src="m4bln://xyz.com"></iframe>

注意:
安卓chrome 浏览器version 25之后版本发生了改变。不能在通过设置iframe标签的src属性来启动app了。取而代之的是通过自定义scheme实现用户手势启动app或者使用Chrome Intent语法(参考下文)

这里我们对比在浏览器中和webview中打开页面的情况:

浏览器中打开

根据官方文档, Chrome浏览器在通过自定义Scheme唤起App的时候,需要绑定用户的点击事件触发,而在浏览器地址栏里直接输入是不可以的。

因此,当我们想要用户打开网页就直接进入App时,在Chrome浏览器地址栏里直接输是无法实现的,但可以通过其他方式实现,例如绑定所有的事件,让用户碰一下屏幕还是可以轻松做到的,参考自动写入剪贴板的实现,而通过点击url的方式则可以自动唤起App。

例如,用户点开短信中的url,此时无需用户点击便可以自动唤起App。(参考之前对于支付宝的克隆攻击Demo)

WebView中自动唤起App实现代码可能如下:

  1. 对于a标签

    1
    2
    3
    4
    5
    6
    <a id="a" href="m4bln://xyz.com">打开App</a>
    <script>
    window.onload=function(){
    document.getElementById("a").click();
    }
    </script>
  2. 对于location.href

    1
    2
    3
    4
    5
    <script>
    window.onload=function(){
    document.location.href="m4bln://xyz.com";
    }
    </script>

WebView中打开

在webview中,直接通过loadurl(“m4bln://xyz.com”)是无法打开App的,需要使用shouldOverrideUrlLoading和shouldInterceptRequest进行拦截和处理。

注:

shouldOverrideUrlLoading 用于拦截页面切换
shouldInterceptRequest 拦截所有的资源请求,例如css,js,iframe

shouldOverrideUrlLoading is called when a new page is about to be opened whereas shouldInterceptRequest is called each time a resource is loaded like a css file, a js file etc

因此,对于a标签或document.location都属于页面切换类型,所以用shouldOverrideUrlLoading来实现处理

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
26
27
28
29
30
31
32
33
34
35
36
wv.setWebViewClient(new WebViewClient(){
/*
注: 在Android N中 shouldOverrideUrlLoading(WebView view, String url)被废弃,改为shouldOverrideUrlLoading(WebView view, WebResourceRequest request),通过request.getUrl()来获取url。
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
if(你想在当前页面打开新连接){
view.loadUrl(url);
}else if(在一个新的activity中打开新连接){
Intent intent = new Intent(mContext, NewWebViewActivity.class);
intent.putputExtra("URL", url);
startActivity(intent);
}
return true;
}
*/


@Override
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
String url="";
try{

url = request.getUrl().toString();
if(url.startsWith("m4bln://")){
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
startActivity(intent);
return true;
}
}catch (Exception e){
return false;
}
view.loadUrl(url);
return true;

}
});

而对于iframe,上文中虽然提到在浏览器中无法达到目的,但在webview中可以通过shouldInterceptRequest来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*
注: shouldInterceptRequest(WebView view, String url)同样被废弃,这里不再描述
*/
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {

String scheme = request.getUrl().getScheme();
if (scheme.equals("m4bln")) {
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(request.getUrl().toString()));
startActivity(intent);
return null;
}
return super.shouldInterceptRequest(view, request);
}

webview完成上述设置后,使用webview加载web页面,均可以实现一启动便自动唤起对应的App。

需要注意的是:

  1. 不是使用webview.loadurl(“m4bln://xyz.com”)直接加载scheme,loadurl加载的是和上述浏览器部分相同的web页。

  2. 如果webview没有作相应的设置,是无法再通过scheme打开其他App的

  3. 有人曾经想过利用自定义scheme实现App之间的一个循环启动,实现loop还得取决于App中的webview是如何配置的

  4. App的webview里利用自己的scheme唤起自己确实会导致死循环

0x2 Chrome Intent

为了更有序的打通浏览器页面和本地应用推出了Chrome Intent机制,作为标准协议进行推广,其格式如下:

1
2
3
4
5
6
7
8
9
intent:
HOST/URI-path // Optional host
#Intent;
package=[string];
action=[string];
category=[string];
component=[string];
scheme=[string];
end;

当打开本地App失败的时候,还可以使用如下代码重定向到一个新页面,例如App下载页等。

1
S.browser_fallback_url=[encoded_full_url]

S.用来传递string类型的参数,i.用来传递整数类型参数,例如:

1
2
3
4
5
6
7
8
9
10
11
12
intent://foobar/#Intent;action=myaction;type=text/plain;S.xyz=123;i.abc=678;end

其他参数类型如下:
String => 'S'
Boolean =>'B'
Byte => 'b'
Character => 'c'
Double => 'd'
Float => 'f'
Integer => 'i'
Long => 'l'
Short => 's'

对于上述例子,我们构造intent为:

1
intent://xyz.com#Intent;scheme=m4bln;package=com.m4bln.webviewgps;S.browser_fallback_url=https%3A%2F%2Fwww.baidu.com;end

需要注意的是:

  1. package、action等字段非必须,取决于应用的AndroidMenifest.xml配置,上述例子中使用”intent://xyz.com#Intent;scheme=m4bln;end”也可以成功唤起App.(参照Android源码中对intent://的解析 http://androidxref.com/7.1.2_r36/xref/frameworks/base/core/java/android/content/Intent.java#5044)

  2. 除了intent://,intent:,android-app://可以达到同样效果,同样参照源码

  3. 仅有Chrome支持这种方式,部分国产浏览器屏蔽了intent://这种scheme

  4. Chrome Intent同样需要用户点击事件, “#Intent”中的“I”必须为大写!!!

  5. Chrome Intent在webview里不支持,同样需要shouldInterceptRequest等来处理

App获取Web传参

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Override
protected void onCreate(Bundle savedInstanceState) {
//...

// 获取uri参数
Intent intent = getIntent();
String scheme = intent.getScheme();
Uri uri = intent.getData();
String str = "";
if (uri != null) {
String host = uri.getHost();
String dataString = intent.getDataString();
String from = uri.getQueryParameter("from");
String path = uri.getPath();
String encodedPath = uri.getEncodedPath();
String queryString = uri.getQuery();

//...根据uri判断打开哪个页,或者打开哪个功能
}
}

0x3 App监听本地特定端口

参照WormWhole漏洞(http://www.freebuf.com/vuls/83789.html)

iOS

0x0 自定义Scheme

在Info.plist中配置Scheme

1
2
3
4
5
6
7
8
9
10
11
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>com.xyz</string>
<key>CFBundleURLSchemes</key>
<array>
<string>m4bln</string>
</array>
</dict>
</array>

Universal links为iOS9上一个所谓通用链接的深层链接特性,一种能够方便的通过传统HTTP链接来启动app,使用相同的网址打开网站和app;通过唯一的网址,就可以链接一个特定的视图到你的app里面,不需要特别的scheme。

  1. 通过scheme启动app时,浏览器会弹出确认框提示用户是否打开,Universal links不会提示,体验更好;
  2. Universal links可在微信浏览器中打开外部app

参考资料:

  1. Chrome官方文档
  2. shouldoverrideurlloading和shouldinterceptrequest区别
  3. Android scheme呼起App
  4. iOS中的URL Scheme
  5. iOS Universal Links(通用链接)的使用