本文档向应用开发者介绍如何使用Android提供的安全行特征。
Android是一个特权分离的操作系统,在系统中运行的每个应用程序都有一个区分系统标识(Linux用户ID和分组ID)。标识的系统部分也被区分不同的身份。因而Linux能够把应用程序以及系统进行彼此分离。
另外,更细粒度的安全特征是通过“权限”机制来提供的,这种机制强制限制了特殊进程所能执行的具体操作,并且给每个URI都设定了方位具体数据片段的权限。
安全架构
Android安全架构的核心设计点是,默认的情况下,没有任何应用程序有权来执行能够对其他应用程序、操作系统或用户带来不利影响的任何操作。包括读写用户的私有数据(如通信录或e-mail)、读写另一个应用程序的文件、执行网络访问、保持设备的唤醒状态等等。
因为Android用沙箱来分离每个应用程序,因此应用程序必须明确要共享的资源和数据。应用程序通过声明来获取它们所需要的基本沙箱所不具备的额外能力。应用程序静态的声明它们所需要的权限,并且Android系统会在安装应用程序时,提示应用程序来同意。Android没有动态(在运行时)授权机制,因为安全的损害会给用户带来复杂的用户体验。
应用程序沙箱不依赖用于构建应用程序的技术。特别是在Dalvik虚拟机中是没有安全边界的,并且任何应用程序都能运行本机代码。所有类型的应用程序,包括Java、原生的以及混合的应用程序,都以相同的方式被放在沙箱中,它们彼此都有相同的安全程度。
应用程序签名
所有的Android应用程序(.apk文件)都必须用一个证书来进行签名,证书的私有键被应用的开发者所持有。这个证书标识了应用程序的作者,它不需要由证书授权机构来签署,这是完全允许的,并且通常Android应用都使用自签名证书。Android中证书的目的是区分应用程序的作者。这样就允许系统接受或拒绝应用程序访问签名级别的权限,以及接受或拒绝应用程序要求获得与另一个应用程序相同的Linux身份的请求。
用户ID和文件访问
在安装时,Android系统会给每个包分配一个不同的Linux用户ID。这个身份标识对于设备上处于运行期间的包会保持不变。在不同的设备上,同样的包可能会有不同的UID,这不重要,重要的是在一个设备上,每个包要有不同的UID。
因为安全是在进程级别上强制发生的,任何两个包的代码通常不会运行在同一个进程中,因此它们需要用不同的Linux用户来运行。能够在每个包的Androidmanifest.xml文件中的<manifest>标签中使用sharedUserId属性,来给它们分配相同的用户ID。通过这样做,这些设置了相同用户ID的包会被视为同一个应用程序,并具有相同的权限。要注意的时,为了保留安全性,只有使用了相同的签名的应用程序(并且申请了相同的sharedUserId属性)才会给分配相同的用户ID。
应用程序存储的任何数据,都会把应用的用户ID分配给它,并且通常其他的包不能访问它。当用getSharedPreferences(String, int)、openFileOutput(String, int)或openOrCreateDatabase(String, int, SQLiteDatabase.CureorFactory)等方法创建一个新的文件时,能够使用MODE_WORLD_READABLE和(或)MODE_WORLD_WRITEABLE标记来允许其他的包来读写这个文件。当设置了这些标记时,这个文件依然被创建它的应用程序所拥有,只是被设置全局的读写权限,以便其他应用程序能够访问它。
使用权限
一个基本的Android应用程序是没有权限跟它关联,这意味着它不能够做任何给用户体验或设备上的数据带来不利影响的事情,要使用设备上那些受保护的功能,必须在应用程序的AndroidManifest.xml文件中包含一个或多个<uses-permission>标签,来声明应用程序需要的权限。
例如,需要监视器接收SMS消息的应用程序,应该指定以下设置:
<manifestxmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.app.myapp">
<uses-permissionandroid:name="android.permission.RECEIVE_SMS"/>
...
</manifest>
在应用程序安装时,应用程序申请的权限会通过Android的包安装器来授予,包安装器通过应用程序声明的权限和(或)跟用户进行交互来检查是否应该将对应的权限授予该应用程序。在应用程序运行是,它不会检查权限。也就是说,要么在安装时就给应用程序授予其所申请的权限,使其能够使用这些权限所限定的功能,要么不批准其所申请的权限,使其不能使用这些权限所限定的功能,并用户也不会得到任何提示。
很多时候,由于没有权限而导致的失败,会给应用程序抛出一个SecurityException异常。但是并不保证没有权限的任何地方都会发生这个异常。例如,sendBroadcast(Intent)方法在发送数据时,会检查每个接受器的权限,这个方法调用返回之后,如果有权限失败的情况,也不会收到异常。但是,在几乎所有的场合,权限相关的失败会打印一个系统级日志(log)。
由Android系统提供的权限能够在Manifest.permission类中找到。任何应用程序还可以定义和强制实施它自己的权限,因此这个类中列出的不是所有的可能的权限。
在程序的执行过程中,可在很多地方强制实施一个特殊的权限:
1. 在系统调用的时候,阻止应用程序执行某些功能;
2. 在Activity启动时,阻止其他应用程序的Activity来启动本应用;
3. 在发送和接收广播时,控制谁能接收你的广播或谁能给你发送广播;
4. 在访问并操作一个内容提供器时;
5. 绑定或启动服务的时候。
声明和强制实施权限
要强制执行自己的权限,首先必须使用一个或多个<permission>标签,在AndroidManifest.xml文件中来声明它们。
例如,应用程序想要控制谁能够启动它的一个Activity,就能够用下面的方法来为这个操作声明一个权限:
<manifestxmlns:android="http://schemas.android.com/apk/res/android"
package="com.me.app.myapp">
<permissionandroid:name="com.me.app.myapp.permission.DEADLY_ACTIVITY"
android:label="@string/permlab_deadlyActivity"
android:description="@string/permdesc_deadlyActivity"
android:permissionGroup="android.permission-group.COST_MONEY"
android:protectionLevel="dangerous"/>
...
</manifest>
<protectionLevel>属性是必须的,它告诉系统怎样把应用程序需要的权限通知给用户,或者是允许谁拥有这个权限。
<permissionGroup>属性是可选的,并且只用于帮助系统把相关权限显示给用户。通常用标准的系统组来设置这个属性,当然也可以使用自己定义的组(但这很少见)。我们推荐使用既存的分组,这样会简化给用户的显示的权限UI。
要注意的时,权限所支持的label和description属性。它们是能够显示给用户的字符串资源,android:label属性用于权限列表的显示,android:description属性用于单一权限的详细介绍。label属性值应该是简短的,用几个关键的单词来描述被权限保护的功能。description属性应该是权限的详细描述,惯例是使用两句话,第一句话来描述权限的功能,第二句话用来警告用户,如果应用程序获得了这个权限会带来的不利影响。
下面是一个申请CALL_PHONE权限的label和description属性设置的例子:
<stringname="permlab_callPhone">directly call phone numbers</string>
<stringname="permdesc_callPhone">Allows the application to call
phone numbers without your intervention. Malicious applications may
cause unexpected calls on your phone bill. Note that this does not
allow the application to call emergency numbers.</string>
用系统的Settings应用程序和shell命令:adb shell pm list permissions,能够查看系统中当前定义的权限。Settings应用的使用方法是:Settings->Applications,选择一个应用程序,向下滚动,可以看到这个应用程序所使用的权限。对于开发者,带有“-s”选项的adb命令可以用与用户查看格式相类似的格式来显示权限:
$ adb shell pm list permissions-s
AllPermissions:
Network communication: viewWi-Fi state, createBluetooth connections, full
Internet access, view network state
Your location: access extra location provider commands, fine (GPS) location,
mock location sources for testing, coarse(network-based) location
Services that cost you money: send SMS messages, directly call phone numbers
...
在AndroidManifest.xml中的强制权限
限制访问系统或应用程序整个组件的高级别权限,能够通过应用程序的AndroidManifest.xml文件来设定。所有这些都要求在被期望的组件上包含android:permission属性,以及用于控制访问的命名权限。
Activity的权限(应用于<activity>标签)限制了谁能够启动被关联的Activity。在Context.startActivity()方法和Activity.startActivityForResult()方法执行期间要检查这个权限,如果调用者没有要求的权限,那么就会从调用中抛出一个SecurityException异常。
Service的权限(应用于<service>标签)限制了谁能够启动或绑定被关联的服务。在Context.startService()方法、Context.stopService()方法和Context.bindService()方法执行期间会检查这个权限,如果调用者没有要求的权限,那么就会从调用中抛出一个SecurityException异常。
BroadcastReceiver的权限(应用于<receiver>标签)限制了谁能够发送广播通知给关联的接收器。在Context.sendBroadcast()方法返回后会检查这个权限,也就是在系统试图把提交的广播通知发送给设定的接收器的时候。在因没有权限而失败的时候,它不会向调用者抛出一个异常,它只是不发送Intent对象。同样,给Context.registerReceiver()方法提供的权限,是用来控制谁能够向程序中注册的接收器发送广播。另一种方式是,在调用Context.sendBroadcast()方法时,提供一个权限,来限制那个BroadcastReceiver对象能够接收广播通知。
ContentProvider的权限(应用于<provider>标签)限制了谁能够访问ContentProvider对象中的数据。(内容提供有一套额外的叫做URI权限的重要且易用的安全权限,稍后会介绍。)跟其他组件不同,它有两个独立的权限属性:android:readPermission用于限制谁能够从提供器中读取数据;android:writePermission用于限制谁能够向提供器中写入数据。要注意的是,如果提供器受到读写权限的保护,只拥有写权限并不意味着能够从提供器中读取数据。在首次获取提供器和执行提供器相关的操作时,会进行权限的检查(如果没有权限,就会抛出一个SecurityException异常)。使用ContentResolver.query()方法查询数据时,要求具有读权限,使用ContentResolver.insert()方法、ContentResolver.update()方法、ContentResolver.delete()方法编辑数据时,要求具有写权限。在所有的场景中,如果没有要求的权限,这个调用就会导致一个SecurityException异常被抛出。
发送广播时的强制权限
除了强制谁能够把Intent对象发送给一个BroadcastReceiver对象的权限之外,在发送一个广播通知时,还可以指定需求权限。通过调用带有权限字符串的Context.sendBroadcast()方法,可以要求接收器必须要拥有这个权限,才能够接受这个广播通知。
要注意的是,接收器和广播器都能够要求权限,发生这种情况时,双方的权限都必须检查通过后,才可以把Intent对象发送给匹配的目标。
其他强制性权限
在调用Service过程中,可以设置更细粒度的权限。这种设置是通过调用Context.checkCallingPermission()方法来完成的。调用时给这个方法传入所期望的权限字符串,它会返回一个整数,它指明了所期望的权限是否被当前调用的进程所接受。要注意的是,这种方法只能在执行来自另一个进程调用的时候使用。通常通过IDL接口来发布服务,或者是用其他的方法提供给另一个进程。
有很多有用的检查权限的方法。如果有另一个进程的PID,那么就可以使用Context.checkPermission(String, int, int)方法,针对这个PID来检查权限。如果有另一个应用程序的包名,就可以直接使用包管理器的PackageManager.checkPermission(String, String)方法来找出这个包是否已经被授予了指定的权限。
URI权限
到目前为止我们所介绍的标准的权限系统不能满足内容提供器的使用需要。内容提供器可能要保护它自己的读写权限,但是为了某些操作,它的客户端也需要把指定的URI交给另一个应用程序来处理。一个典型的示例是Mail应用程序中的附件。邮件的访问应该是受到权限的保护,因为这个用户敏感的数据。但是,如果要把一个图片附件的URI提供给一个Image浏览器,那么这个Image浏览器就会因没有权限而不能打开这个图片附件。
这个问题的解决方案是给每个URI都分配一个权限,当启动一个Activity或给一个Activity返回结果时,调用者能够设置Intent.FLAG_GRANT_READ_URI_PERMISSION和(或)Intent.FLAG_GRANT_WRITE_URI_PERMISSION权限。这样就给接受Intent对象的Activity授予了访问Intent对象中指定的数据URI的权限,而不管它是否有权访问与这个Intent对象对应的内容提供器中的数据权限。
这种机制允许使用一种共同的能力样式模型,这种模型利用用户交互(打开一个附件、选择一个通讯录等)来驱动设定更细粒度的权限。这种机制可以有效的减少应用程序所需要的权限,只需要那些与它们直接相关行为权限。
这种把权限细化到URI的做法,需要持有这些URI的内容提供器的配合。强烈推荐内容提供器实现这种机制,并且通过android:grantUriPermissions属性或<grant-uri-permissiongs>标签来声明它们所提供的权限。
更多的信息能够在Context.grantUriPermission()、Context.revokeUriPermission()和Context.checkUriPermission()方法中找到。