System Permissions

安卓是一个权限分离的操作系统,每个应用都使用一个系统标识(Linux user ID 和 group ID)来运行的。系统的各部分也被分配了不同的标识。Linux然后就可以把应用相互隔离,和系统隔离。

更加细致的安全特性是通过一个叫做permission的机制实现的,它会对某个进程要进行的操作,或者对某些数据进行访问的URI进行限制。

本文会讲解,开发者如何使用安卓的这些特性。安卓开源项目中有更加详细的说明。

安全架构

安卓安全架构的核心设计思路是,正常情况下,没有应用有权限来进行那些可以影响到其它应用,操作系统或者用户的操作。这包含用户私人数据的读写(联系人和邮件),读写其它应用的文件,网络访问,保持设备唤醒等。

因为安卓app是运行在自己的进程沙箱内,所以必须显式的共享数据和资源。具体是方法是:对于那些沙箱不提供的功能,需要额外声明权限。app静态地声明所需要的权限,安卓系统来提示用户同意。

app的沙箱依赖于构建应用的技术。尤其是,Dalvik VM不是一个安全的界限,任何的app都可以运行native代码(请参阅NDK).不管是java应用,native应用,还是混合应用,都是以相同的方式放在沙箱中的,拥有相同的安全级别。

应用签名

所有的apk文件必须是使用证书签名的,开发者保存证书的私钥。这个证书可以识别应用的作者。证书不需要由权威机构颁发,通常都是自签名的。证书的目的是用来区分app的作者。它可以让系统来判定签名级的权限或者应用的请求授予和其它应用同一个linux身份。

用户ID和文件访问

在安装时,系统会给每一个分配一个Linux use ID. 在整个包的生命周期中,这个身份是不变的。在另外一个设备上,同一个包可能有不同的UID,重要的是在一个设备上,每个包都有一个不同的UID.

因为进程级别的安全措施,两个包的代码是不可能在同一进程中运行,因为它们的UID不同。我们可以使用清单文件中package节点的sharedUserId属性来让他们被授予同一个UID。这样做之后,出于安全考虑,他们会被认为是同一个应用,使用相同的UID和文件权限。注意,为了保证安全,只有两个应用使用同一个证书签名的时候(并且请求两样的sharedUserId)才会被分配同一个UID.

应用存储的数据都会被标记应用的UID,通常不可被其它包访问。当使用getSharedPreferences(String, int), openFileOutput(String, int), or openOrCreateDatabase(String, int, SQLiteDatabase.CursorFactory)时,可以用MODE_WORLD_READABLE或者MODE_WORLD_WRITEABLE的标记来让其它应用来读写。当设置了这些标记时,文件是我们的应用所有,但是是全局读写权限已经设置了,这样其它应用就可以看到它。

使用权限

默认的安卓应用没有任何权限,意思就是它不能做任何影响用户体验或者设备上数据的操作。为了利用设备的特性,我们必须在清单文件中声明一个或多个标签。
比如,应用需要监控SMS的来信:


    
    ...

权限级别

想了解更多不同级别的权限,请参阅Normal和Dangerous权限

如果我们的app列出了一些普通权限(这些权限不会对用户的隐私和设备运行造成太大的风险),系统会自动的授予这些权限。如果应用申请了危险权限(这些权限极有可能会影响到用户的隐私和设备运行),系统会要求用户明确的授予这些权限。系统是否发出请求是根据系统版本号和app的target version:
1.如果设备的系统是API 23或者更高,并且app的targetSdkVersion是23或者更高,app会在运行向用户请求权限。用户可以随时撤销权限,所以app需要在每次运行时都进行权限检查。
2.如果设备的系统是API 22或者更低,或者app的targetSdkVersion是22或者更低。系统会在app安装时要求用户授予这些权限。如果在app升级时需要添加一个新的权限,系统会在用户升级app时要求用户授予这个权限。一旦用户安装了app,用户唯一可以撤销权限的方法就是卸载应用。

很多时候,权限申请失败会在app中抛出一个SecurityException.但是,这不是每次都会发生的。比如:sendBroadcast在数据传递给每个接收者,然后在方法返回时,都会进行权限检查.所以,即使有permission失败,我们也不会收到异常。但是几乎所有的情况,权限失败都被在系统日志中记录。

系统提供的权限可以在清单文件中找到。任何应用也可以定义自己的权限,因此,下边不是一个所有权限的综合列表。
程序运行中可能执行某个特定权限的位置:
1.在调用系统功能时,防止应用使用一些功能。
2.启动activity时,防止应用启动其他应用的activities。
3.发送和接收广播,决定谁可以接收你的广播,或者谁可以给你发送一个广播。
4.访问或者操作一个content provider。
5.绑定或者启动service。

自动权限调整

随着时间的增加,新的限制会越来越多,比如,为了使用一个API,以前可能不需要,但现在app就需要申请一个权限。因为手机上已安装应用可能对这些API是没有使用限制的,所以为了不打破新版本的向前兼容性,安卓系统提供了新的权限请求。系统根据应用的targetSdkVersion来决定这个应用是否需要权限申请。如果targetSdkVersion低于权限所需要的版本,系统会自动添加这个权限。

比如,WRITE_EXTERNAL_STORAGE权限是在API 4的时候为了限制公共区域的数据读取才添加的。如果应用的targetSdkVersion小于4,那么系统会自动在新版本的设备上添加这个权限。

注意:如果你的权限是自动添加到应用的,在Google Play上会自动列出这些权限,其实我们没有使用。

为了避免这种情况的发生,移除那些我们不需要的权限。我们应该经常更新自己targetSdkVersion。我们可以在Build.VERSION_CODES中检查哪些权限是新添加的。

普通和危险权限

系统权限被分为几个级别。最重要的两个级别是普通权限和危险权限:

1.普通权限是指那些需要访问app沙箱以外的数据和资源,但是对用户的隐私和其他app的危害非常小的权限。比如设置时区的权限是一个普通权限,如果应用声明这个权限,系统会自动授予。想要查看所有的权限,请参阅权限。
2.危险权限出现在那些app需要数据和资源,但是这些东西涉及到用户的隐私,或者可能影响其它应用的存储数据或者运行。比如,读取手机联系人是一个危险权限。如果应用声明一个危险权限,用户必须显式的授予这个权限。

特殊权限
有些权限既不是普通权限也不是危险权限。SYSTEM_ALERT_WINDOW和WRITE_SETTINGS是非常敏感的,所以很多应用都不应该使用。如果app确实需要使用,必须清单文件中声明,然后发送一个intent请求用户授予。系统会根据intent展示一个详情的管理界面给用户。

权限组

所有的危险权限都属于某个权限组。如果设备的API版本是23或更高,并且app的targetSdkVersion为23或者更高,当app申请权限时会发生下列行为:
1.如果app请求一个清单文件中的危险权限,app没有权限组的任何一个权限,系统会展示一个对话框给用户来描述权限组,对话框不会描述具体的权限。比如,如果app请求READ_CONTACTS,对话框中只会出现用户申请权限的权限组。如果用户同意,系统会授予应用相应的权限。
2.如果app请求一个清单文件中的危险权限,app已经有同一个权限组的另外一个权限,系统会不跟用户再沟通直接授予权限。比如,如果app已经有READ_CONTACTS,当再申请WRITE_CONTACTS时,系统会自动授予。

任何权限都属于某一权限组,包括普通权限和自定义权限。但是只有当权限是危险的时候,它的权限组才会影响用户体验。对于普通权限的权限组可以忽略。

如果设备的API版本低于23或者app的targetSdkVersion低于22,系统会在应用安装时申请权限。同样的,系统给用户通知的还是权限组而不是单独的权限。

System Permissions_第1张图片
危险权限和权限组

自定义权限

为了使用自定义权限,首先需要在清单文件中使用一个或多个标签来声明。

比如,app想要控制谁可以启动自己的Activity就需要为这个操作声明一个如下的权限:


    
    ...

注意:系统不允许不同包声明相同的权限名称,除非这些包是使用同一个证书签名。如果一个包声明一个权限,如果有其它包使用了相同的权限名称,除非他们使用相同的证书签名,否则系统不允许第二个包进行安装。为了避免权限名称冲突,推荐使用域名逆序的方式来命名。比如:com.example.myapp.ENGAGE_HYPERSPACE.

权限需要定义安全级别,通知系统当有权限申请时来如何告知用户或者哪些应用可以授予这些权限。

权限组是可选的,只是用来帮助系统给用户展示使用。虽然可以自定义权限组,但是通常可以使用的默认的权限组。建议使用一个已经存在的权限组,这样可心简化给用户的通知展示。

自定义权限还需要提供一个label和description.这些都是字符串,用户用来查看权限列表(label)或者一个权限的细节(description)时使用。label通常比较简短,用来描述权限主要保护的功能。description通常是几句话来说明权限允许的操作。一个典型的二句话description:第一句描述权限,第二句警告用户如果授予权限一些可能出错的东西。
下边是一个CALL_PHONE的label和description示例:

directly call phone numbers
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.

我们可以通过设置app来查看系统当前定义的权限,也可以通过adb命令来查看。使用设置app,进入Setting->Application,选择一个app,查看当前app使用的权限。对于开发者来说,使用adb -s命令来查看:

$ adb shell pm list permissions -s
All Permissions:


Network communication: view Wi-Fi state, create Bluetooth 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
...

自定义权限建议

app可以自定义权限,也可以通过 来请求其它app的自定义权限。但是,我们必须慎重考虑是否有必要去使用自定义权限。
1.如果有设计一套相互提供功能的app,尽可能让每一个权限只定义一次。如果这些app不是同一证书签名,那么必须要这么做。即使是同一证书签名,也最好每个权限只定义一次。
2.如果这些app都是同一证书签名,我们可以使用证书校验来代替自定义权限。当一个app请求另外一个app时,第二个app只需要验证两个app的签名是否一致即可。
3.如果这些app只在自己的设备上运行,我们应该开发一个包来管理所有的权限。这个包不需要提供任何服务,只需要声明所有的权限,其它应用使用 来请求权限即可。

清单文件中的权限

我们可以通过在清单文件配置权限来限制系统组件或者其它应用的组件的访问。在需要的组件节点上添加android:permission属性, 命名需要控制它的权限。

1.Activity permissions(在标签中),限制启动当前activity。这个权限会在Context.startActivity() 和Activity.startActivityForResult()时进行检验。如果调用者没有相应权限就会抛出SecurityException。
2.Service permissions(在 标签中),限制启动和绑定当前service.这个权限会在Context.startService(), Context.stopService()和Context.bindService()时进行校验。如果调用者没有相应权限就会抛出SecurityException。
3.BroadcastReceiver permissions(在 标签中),限制发送broadcasts给当前receiver.这个权限会在Context.sendBroadcast()返回之后,系统在尝试发送这个广播时进行校验。权限校验失败不会导致异常,只是中止intent的传递。同理,使用Context.registerReceiver()时也可以使用权限来限制发送广播给那些注册的监听者。另外,当调用Context.sendBroadcast()也可以使用权限来限制哪些BroadcastReceivers可以接收广播。
4.ContentProvider permissions(在标签中),限制访问当前ContentProvider的数据。(ContentProvider有另外一个非常重要的安全机制叫做URI permissions,后续会详细说明)。和其它组件不同,我们可以使用两个不同的权限属性,android:readPermission和android:writePermission.注意,如果一个provider声明了读写权限,有写的权限不意味着就有了读的权限。第一个访问provider的时候会校验权限,如果没有任何一个权限,会抛出SecurityException。当我们在操作provider时, ContentResolver.query()需要读权限,ContentResolver.insert(), ContentResolver.update(), ContentResolver.delete()需要写权限,在这些操作中,没有对应的权限会导致SecurityException。

发送广播时的权限

除了上边描述的,可以使用权限来限制发送intent给一个注册的BroadcastReceiver之外,我们也可以为发送广播指定一个权限。通过调用Context.sendBroadcast()时添加一个权限字符串,这样广播接收者就必要需要一个权限才能接收我们的广播。

注意,接收者和发送者都可能需要一个权限。当这种情况发生时,两个权限校验都必须通过,这样才能传递intent成功。

其它权限

服务可以添加任意细致的权限。可以通过Context.checkCallingPermission()来实现。调用一个permission string,返回一个integer当前进程是否有这个权限。注意:这个只能当其它进程调用的时候才有效。通常通过一个其它服务的IDL接口或者另外一个进程的其它方式。

有很多方法来校验权限。如果我们有其它进程的PID,我们可以使用Context.checkPermission(String, int, int)来校验。如果我们有其它app的包名,我们可以直接使用PackageManager.checkPermission(String, String)来校验。

URI 权限

标准的权限机制对于 content providers来说是不够的。 content providers可能需要保护自己的读写权限当它的客户端需要把一些URI交给另外的app去操作时。一个典型的例子是邮件应用的附件。访问邮件必须是权限保护的,因为是敏感的用户数据。但是,如果一个图片附件的URI给图片查看器,图片查看器是不能打开附件的,因为它没有理由有权限去访问邮件。

解决这个问题的方法是使用per-URI权限:当开启一个activity或者返回一个结果给activity时,调用者可以设置Intent.FLAG_GRANT_READ_URI_PERMISSION 和 Intent.FLAG_GRANT_WRITE_URI_PERMISSION。这样就能让接收的Activity有权限去访问intent中的数据URI,不论它是否有对应contentProvider的权限。

这个机制支持常见的能力模型——用户交互(打开附件,选择联系人等)驱动来授予更细致的权限。这对于减少应用所需权限到只有那些直接关系到app行为是非常重要的。

但是,授予细化的URI权限需要contentProvider的配合。强烈推荐contentProvider来实现这个功能,并且通过android:grantUriPermissions属性和标签来声明。

你可能感兴趣的:(System Permissions)