Android scheme呼起App

Android应用/组件间通信有一种方式是intent,应用可以注册intent filter声明自己对什么样的intent感兴趣,其它应用发送intent时通过系统级广播传递过来,如果与预先注册的intent filter匹配,应用将收到该intent(无论应用是否正在运行,都会被“唤醒”,也就是隐式启动Activity),取出intent携带的数据,做进一步处理

就是这样,通过系统广播拿到一次起来的机会,例如在manifest里静态注册intent filter声明自定义scheme

<activity android:name=".MainActivity">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />

        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>

    <!--注册scheme-->
    <intent-filter>
        <action android:name="android.intent.action.VIEW"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <!--BROWSABLE指定该Activity能被浏览器安全调用-->
        <category android:name="android.intent.category.BROWSABLE"/>
        <!--声明自定义scheme,类似于http, https-->
        <data android:scheme="hoho"/>
    </intent-filter>
</activity>

actioncategorydata都必须完全匹配才能获得intent,这里声明了2个category,只有在intent同时含有这2个category时才算匹配,而android.intent.category.DEFAULT是默认的,有实际意义的是android.intent.category.BROWSABLE,表示允许通过浏览器启动该activity(呼起App)。后续的data限定了触发条件,当schemehoho时才匹配,例如浏览器访问hoho://abc,能够匹配成功,App就起来了

二.取出数据

onCreate里拿到intent,取出uri

@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判断打开哪个页,或者打开哪个功能
    }
}

这里的URI就是标准的URI,有协议、主机名、端口号、路径、查询字符串等等,但一般自定义scheme不需要这么麻烦,只用path/query做简单区分就行,比如:

// 通过path区分
hoho://toFeature/login
// 通过query区分
hoho://open?feature=login

当然,也可以通过端口号等区分,没什么区别

三.在线页面呼起App

浏览器先发出自定义scheme请求,系统广播收到后再分发给各应用,那么页面发送请求的方式就多了:

location.href
iframe.src
a.href
img.src
...其它能发出请求的方式

这些方式在强弱上有区别,比如location.href是强的,而img.src很弱,至少要强到浏览器决定把这个请求交给系统广播才行,比如img请求自定义scheme,浏览器认为没有必要交给系统广播。一般只用前2种最强的方式:location.hrefiframe.src,隐藏iframe偷偷请求自定义scheme相对用得更多,因为不会有未知的副作用(location方式或许可能被记入历史栈或者unload当前页,但iframe绝对没有太严重的副作用

但无论哪种方式,都无法得知App被呼起了没,可能没安装App,也可能intent没匹配成功,但页面肯定没有办法得知。所以一般呼起App的页面都会延迟自动跳转下载页,无论有没有成功呼起App,这也是迫不得已

除了页面发出请求,还有一种更强的方式:通过应用发出请求,例如:

// 通过webview发出请求
webview.loadUrl(mySchemeUri);

这个起点就是应用级,比WebView中页面请求要强一些。所以一般Hybrid App中,客户端会提供这样的接口,用来跳转第三方,比页面请求更强

四.Intent Scheme URL攻击

自定义Scheme存在安全风险,比如:

  • 注册优先级更高的相同intent filter,窃取scheme uri
  • 如果知道跳转的自定义scheme格式,可以跳向钓鱼页面(确实是在App里打开的页面,但它是第三方做的假的)
  • …其它风险

一般自定义scheme都是不公开的,但难免会泄漏出去(反编译App等方式),scheme接口本身要做好防范,接收intent时可以这样做:

    // forbid launching activities without BROWSABLE category
    intent.addCategory("android.intent.category.BROWSABLE");
    // forbid explicit call
    intent.setComponent(null);
    // forbid intent with selector intent
    intent.setSelector(null);

不信任所有来自自定义scheme的输入,对于跳转接口,还要有白名单限制

五.WebView Scheme白名单

WebView作为页面容器,可以过滤/拦截页面请求

class MyWebClient extends WebViewClient {

    @Override
    public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
        if (url.startsWith("hoho://")) {
            return null;
        }

        return super.shouldInterceptRequest(view, url);
    }

    @Override
    public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
        String scheme = request.getUrl().getScheme();
        if (scheme.equals("hoho")) {
            return null;
        }

        return super.shouldInterceptRequest(view, request);
    }
}

上面的用于API[11-20],21弃用String url,新增了WebResourceRequest request,在API21+只触发WebResourceRequest request形式的,所以兼容考虑,两个都要重写一遍

对于满足过滤条件的,拦截掉,所以在微信里无法呼起App,因为不在白名单里,被拦截下来,没有交给系统广播

在被拦截的情况下,iframe方式的优势就体现出来了,a.hreflocation.href都会导致页面跳转,显示“网页无法打开…因为net::ERR_UNKNOWN_URL_SCHEME”,而iframe方式不影响当前页

六.Demo

apk下载地址:http://ayqy.net/apk/android-scheme.apk

测试页面:http://ayqy.net/temp/android-scheme.html

写在最后

Android Studio实在太慢了,怀念eclipse,另外,感谢@旭

参考资料

0 条评论
发表一条评论