一.自定义Scheme
Android应用/组件间通信有一种方式是intent,应用可以注册intent filter声明自己对什么样的intent感兴趣,其它应用发送intent时通过系统级广播传递过来,如果与预先注册的intent filter匹配,应用将收到该intent(无论应用是否正在运行,都会被“唤醒”,也就是隐式启动Activity),取出intent携带的数据,做进一步处理
就是这样,通过系统广播拿到一次起来的机会,例如在manifest里静态注册intent filter声明自定义scheme:
action、category、data都必须完全匹配才能获得intent,这里声明了2个category,只有在intent同时含有这2个category时才算匹配,而android.intent.category.DEFAULT是默认的,有实际意义的是android.intent.category.BROWSABLE,表示允许通过浏览器启动该activity(呼起App)。后续的data限定了触发条件,当scheme为hoho时才匹配,例如浏览器访问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.href和iframe.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.href和location.href都会导致页面跳转,显示“网页无法打开…因为net::ERR_UNKNOWN_URL_SCHEME”,而iframe方式不影响当前页
六.Demo
写在最后
Android Studio实在太慢了,怀念eclipse,另外,感谢@旭
参考资料