上一节粗略讲述了URL Scheme如何打开app,这一节对这一机制进行详细说明
Android安全开发之浅谈网页打开App(一)
一. 网页打开APP简介
Android有一个特性,可以通过点击网页内的某个链接打开APP,或者在其他APP中通过点击某个链接打开另外一个APP,一些用户量比较大的APP,已经通过发布其AppLink SDK,开发者需要申请相应的资格,配置相关内容才能使用。这些都是通过用户自定义的URI scheme实现的,不过背后还是Android的Intent机制。Google的官方文档《Android Intents with Chrome》一文,介绍了在Android Chrome浏览器中网页打开APP的两种方法,一种是用户自定义的URI scheme(Custom URI scheme),另一种是“intent:”语法(Intent-based URI)。
第一种用户自定义的URI scheme形式如下:
scheme://host/path?parameters
第二种的Intent-based URI的语法形式如下:
intent://host/uri_path#Intent;参数;end
因为第二种形式大体是第一种形式的特例,所以很多文章又将第二种形式叫Intent Scheme URL,但是在Google的官方文档并没有这样的说法。
注意:使用Custom URI scheme给APP传递数据,只能使用相关参数来传递数据,不能想当然的使用scheme://host/uri_path#intent;参数;end
的形式来构造传给APP的intent数据。具体原因看本文3.1章节
此外,还必须在APP的Androidmanifest文件中配置相关的选项才能产生网页打开APP的效果,具体在下面讲。
二. Custom Scheme URI打开APP
2.1 基本用法
需求:使用网页打开一个APP,并通过URL的参数给APP传递一些数据。
如自定义的Scheme为:
wdgz://wdfull/card?card_id=828
网页端的写法如下:
URL打开App
打开豌豆公主App
APP端接收来自网页信息的Activity,要在Androidmanifest.xml文件中Activity的intent-filter中声明相应action、category和data的scheme等。
在MyActivity中接收intent并且获取相应参数的代码:
@Override
protected void onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val intent = getIntent()
Log.d("shixiangyu",intent.toURI())
val uri = intent.getData()
此时,我们可以看一下打印的intent.toURI()信息:
由上图可知Android系统自动为Custom URI scheme添加了默认的 #intent字段,以及category,launchFlags,component等信息。
另外还有以下几个API来获取相关信息:
getIntent().getScheme(); //获得Scheme名称
getIntent().getDataString(); //获得Uri全部路径
getIntent().getHost(); //获得host
三. Intent-based URI打开APP
3.1基本用法
Intent-based URI语法:
intent://
host/uri_path
#Intent;
action=[action];
category=[category];
component=[component]
scheme=[scheme];
end
注意:第二个Intent的第一个字母一定要大写,不然不会成功调用APP。
如何正确快速的构造网页端的intent?
可以先建个Android demo app,按正常的方法构造自己想打开某个组件的Intent对象,然后使用Intent的toUri()方法,会得到Intent对象的Uri字符串表示,并且已经用UTF-8和Uri编码好,直接复制放到网页端即可。
Intent i = new Intent();
i.setAction("android.intent.action.VIEW");
i.addCategory("android.intent.category.BROWSABLE");
i.addCategory("android.intent.category.DEFAULT");
i.setData(Uri.parse("wdgz://wdfull/card?card_id=828"));
Log.d("shixiangyu",i.toUri(Intent.URI_INTENT_SCHEME));
结果:
如果在demo中的Intent对象不能传递给目标APP的Activity或其他组件,则其Uri形式放在网页端也不可能打开APP的,这样写个demo容易排查错误。
另外,对于传递的参数的设置,可以直接跟在uri_path里面,然后直接通过intent.setData()
的形式进行传递,上面的就是此种形式,也可以通过intent.putExtra()
的方式进行传递,如
Intent i = new Intent();
i.setAction("android.intent.action.VIEW");
i.addCategory("android.intent.category.BROWSABLE");
i.addCategory("android.intent.category.DEFAULT");
i.setData(Uri.parse("wdgz://wdfull/card"));
i.putExtra("card_id","828");
Log.d("shixiangyu",i.toUri(Intent.URI_INTENT_SCHEME));
我们来看一下此时构造的intent长相如何:
S.card_id跟的就是intent对象的putExtra()方法中的数据。采取这种方式的话,从intent中获取相关信息就有通过intent,getStringExtra()
方法了。
APP端中的Androidmanifest.xml的声明写法同2.1节中的APP端写法完全一样。
然后服务端的代码便可换成:
打开豌豆公主App
接着,我们在被打开的MyActivity中打印intent.toUri()信息:
不要惊呼,没有看错,android系统自动把intent://
替换成了wdgz://
,另外在后面添加了 component 的相关信息。
问题来了,为何不能用scheme://host#intent;参数;end
的形式来构造传给APP的intent数据,然后直接给网页端呢?
如对于:
打开豌豆公主App
是的,就是因为Android系统会自动为Custom URI scheme而非Intent-based URI添加默认的#intent等信息。
四. 风险评估
常见的用法是在APP获取到来自网页的数据后,重新生成一个intent,然后发送给别的组件使用这些数据。比如使用Webview相关的Activity来加载一个来自网页的url,如果此url来自url scheme中的参数,如:wdgz://wdfull/html?load_url=http://m.wamdougongzhu.cn
。
如果在APP中,没有检查获取到的load_url的值,攻击者可以构造钓鱼网站,诱导用户点击加载,就可以盗取用户信息。
在Webview加载load_url时,结合APP的自身业务采用白名单机制过滤网页端传过来的数据,黑名单容易被绕过。