一.自定义Scheme
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>
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,这也是迫不得已
除了页面发出请求,还有一种更强的方式:通过应用发出请求,例如:
代码语言:javascript复制// 通过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作为页面容器,可以过滤/拦截页面请求:
代码语言:javascript复制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
apk下载地址:http://ayqy.net/apk/android-scheme.apk
测试页面:http://ayqy.net/temp/android-scheme.html
写在最后
Android Studio实在太慢了,怀念eclipse,
参考资料
- Android 通过网页打开自己的APP(scheme)
- Android安全开发之浅谈网页打开APP
- 附iOS通过自定义的URL Scheme启动你的App