关注【搜狐技术产品】,第一时间获取技术干货
01 权限介绍 权限分类
权限的目的是保护用户的隐私。
Android 安全架构的中心设计点是:在默认情况下任何应用都没有权限执行对其他应用、操作系统或用户有不利影响的任何操作。这包括读取或写入用户的私有数据(例如联系人或电子邮件)、读取或写入其他应用程序的文件、执行网络访问、使设备保持唤醒状态等。
由于每个 Android 应用都是在进程沙盒中运行,因此应用必须显示共享资源和数据。显示共享的方法是声明需要哪些权限来获取基本沙盒未提供的额外功能。应用以静态方式声明它们需要的权限,然后 Android系统提示用户同意。
一个 Android 应用默认情况下是不拥有任何权限的。在默认情况下,一个应用没有权限进行可能会造成安全影响的操作。安全影响包括此应用是否会对其它应用,操作系统,或者用户信息有安全威胁。
如果应用确实需要一些安全权限,对权限要求是合理的,就需要在 AndroidManifest.xml 中静态地声明需要用到的权限。
如果应用实际使用了没有在 Manifest 中声明的权限,在调用到相应权限功能时候,会抛出 SecurityException异常。
Android 权限分成四类:
- 普通权限(normal permission):也叫正常权限,即使拥有了该类权限,用户的隐私数据被泄露篡改的风险也很小。例如,设置时区的权限就是正常权限。如果应用声明其需要正常权限,系统会自动向应用授予该权限。
- 敏感权限(dangerous permission):也叫危险权限,运行时权限,跟普通权限相反,一旦某个应该获取了该类权限,用户的隐私数据就面临被泄露篡改的风险。比如 READ_CONTACTS 权限就属于危险权限。如果应用声明其需要危险权限,则用户必须明确向应用授予该权限。
- 签名权限(signature permission):该类权限只对拥有相同签名的应用开放,比如手机QQ 自定义了一个permission 且在权限标签中加入 android:protectionLevel=signature,而访问它的某个数据时,必须要拥有该权限。然后微信和 QQ 发布时采用相同的签名,微信就可以申请访问 QQ 中的此权限,并使用对应权限控制的数据。其他程序即使知道了这个开放数据的接口,也在 Manifest 注册了权限,但由于应用签名不同,还是无法访问的对应的数据。
- 系统签名权限(signatureOrSystem permission):与 signature permission类似,但它不光要求签名相同,还要求是同类的系统级应用,一般手机厂商开发的预制应用,才会用到该类权限。
02 权限组和动态权限
任何权限都可属于一个权限组,包括正常权限和应用定义的权限。但权限组仅当权限危险时才影响用户体验。可以忽略正常权限的权限组。
如果设备运行的是 Android 6.0(API 级别 23),并且应用的targetSdkVersion 是 23 或更高版本,则当用户请求危险权限时系统会发生以下行为:
- 如果应用请求其Manifest中列出的危险权限,而应用目前在权限组中没有任何权限,则系统会向用户显示一个对话框,描述应用要访问的权限组。对话框不描述该组内的具体权限。例如,如果应用请求 READ_CONTACTS 权限,系统对话框只说明该应用需要访问设备的联系信息。如果用户批准,系统将向应用授予其请求的权限。
- 如果应用请求其Manifest中列出的危险权限,而应用在同一权限组中已有另一项危险权限,则系统会立即授予该权限,而无需与用户进行任何交互。例如,如果某应用已经请求并且被授予了READ_CONTACTS权限,然后它又请WRITE_CONTACTS,系统将立即授予该权限。
除了危险权限,Android 还有两个特殊敏感权限,但需要通过在设置应用中授权:
- SYSTEM_ALERT_WINDOW,设置悬浮窗,进行一些黑科技。(通过发送 intent action Settings.ACTION_MANAGE_OVERLAY_PERMISSION请求权限. 通过接口Settings.canDrawOverlays()检测权限)
- WRITE_SETTINGS修改系统设置(通过发送 intent action Settings.ACTION_MANAGE_WRITE_SETTINGS.请求权限. 通过接口Settings.System.canWrite()检测权限)
这两个特殊权限的授权,和危险权限授权不同,它使用 startActivityForResult 启动授权界面来完成。另外 Location权限组的相关危险权限,除了要动态授权,也是需要在设置中进行授权的。
所有危险的 Android 系统权限都属于权限组(这些权限组也包含一些非危险权限)。
以 Android Pie(api level 28)最新代码总结(官网文档有细节缺失),Android 系统预定义了 10 组权限组(包含了所有 28 种危险权限,4 种其它权限),大部分普通权限不属于任何分组。
03 权限历史变更及常见权限
以 Android Pie(api level 28)统计,系统预定义权限有455个,可通过 Android SDK 访问的有151个。
这151个权限被引入系统的api level不同,且有些已经标识为@Deprecated,有些直接从 SDK 中移除,在官方文档也不再能查到,使用时需要根据文档标注的api level和注意事项进行版本兼容。
随着时间的发展,平台中可能会加入新的权限限制,要想使用特定 API,应用可能必须请求之前不需要的权限。默认Android将根据targetSdkVersion的值来处理此类权限,如果该值低于在其中添加权限的版本,则 Android 会自动帮应用添加该权限。
我们可以通过在 setting 应用中查看每个应用的所有权限和更新动态权限状态,也可以通过 adb 控制权限:
Android的完整权限定义在framework-res.apk中,android.jar只包含了普通应用能访问的权限组和权限定义。常见的权限如下:
04 动态权限机制及使用
从 Android 6.0(API级别 23)开始,用户开始在应用运行时向其授予危险权限,而不是在应用安装时授予。此方法可以简化应用安装过程,因为用户在安装或更新应用时不需要授予权限。用户可以随时进入应用的Settings中调整应用的动态权限授权。
在所有版本的 Android 中,您应用都需要在其应用 Manifest 中同时声明它需要的正常权限和危险权限。不过,该声明的影响因系统版本和应用的 targetSdkVersion的不同而有所差异:
- 如果设备运行的是 Android 5.1 或更低版本,或者应用的 targetSdkVersion 为 22 或更低:如果您在 Manifest 中列出了危险权限,则用户必须在安装应用时授予此权限;如果他们不授予此权限,系统根本不会安装应用。如果将新权限添加到更新的应用版本,系统会在用户更新应用时要求授予该权限。用户一旦安装应用,他们撤销权限的唯一方式是卸载应用。
- 如果设备运行的是 Android 6.0 或更高版本,并且应用的 targetSdkVersion为23 或更高:应用必须在 Manifest 中列出权限,并且它必须在运行时请求其需要的每项危险权限。用户可以授予或拒绝每项权限,且即使用户拒绝权限请求,应用仍可以继续运行有限的功能。
从 Android 6.0(API 级别 23)开始,无论您的应用面向哪个 API 级别,您都应对应用进行测试,以验证它在缺少需要的权限时行为是否正常。
应用可以通过 Activity,Fragment,和 Context 类(建议使用 Support 包中的对应类,如 ContextCompat)中三个方法和一个回调,与用户协调动态权限的授权:
- checkSelfPermission:检查动态权限,返回PackageManager.PERMISSION_GRANTED或PackageManager.PERMISSION_DENIED
- shouldShowRequestPermissionRationale:检查用户禁止了此动态权限框跳出,应用之前请求过此权限但用户拒绝了请求,此方法将返回 true。
- requestPermissions:请求动态权限,系统会弹出权限授权提示框给用户,用户可以选择授权和拒绝。选择结果通过 Activity 或 Fragment 的 onRequestPermissionsResult 接受。
- onRequestPermissionsResult:动态权限申请结果,根据请求的requestCode和grantResults是否PERMISSION_GRANTED来判断用户是否授权。
对于两个特殊权限 SYSTEM_ALERT_WINDOW 和 WRITE_SETTINGS,必须通过下面的方式判断授权情况,并通过 startActivity 来跳转到权限设置页去授权:
- Settings.canDrawOverlays()。
- Settings.System.canWrite()。
对于location权限,还需要通过 LocationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)判断是否打开定位开关。
在使用 Android 权限时,需要遵循以下原则:
1、最少原则,只使用 APP 所必需的权限。可能还有其他方法能实现同样的需求,而无需访问敏感信息。
2、全面原则,应用需考虑第三方库所需的权限,当 APP 包含一个库时,会继承其权限要求。
3、清晰原则,当您发出权限请求时,要让用户明白正在访问的内容以及为什么要访问,以便用户做出明智的决定。
4、显式原则,当您访问敏感功能(例如相机或麦克风)时,提供连续的提示可以让用户清楚您在收集数据,避免让用户感觉您在偷偷收集数据。
例如,您需要用设备的 Camera 来拍照,你可以通过调用 Camera 的 API 来实现这个功能,但是这需要请求 Camera 的权限;你也可以你 Intent 启动 Camera 模块来实现这个功能,但是不需要请求 Camera 的权限。
例如,媒体播放器在通话期间静音或暂停,是使用 PhoneStateListener 监听呼叫状态的更改或监听android.intent.action.PHONE_STATE的广播。该解决方案的问题是它需要 READ_PHONE_STATE权限,这需要用户同意访问敏感数据(例如其设备和 SIM 硬件 ID 以及来电的电话号码)。您可以通过为您的应用请求 AudioFocus 来避免这种情况,因为它不需要明确的权限(因为它不访问敏感信息)。只需将音频切换到后台时所需的代码放在 onAudioFocusChange()事件处理程序中,当操作系统移动切换其音频焦点时它将自动运行。
05 自定义权限组和权限
所有应用都可以自定义权限组和权限,方法和系统定义权限是一样的。应用安装时,自定义权限会统一加入到应用权限列表中。
自定义的权限组合权限,使用方法和系统权限一样,如下:
通常,权限失效会导致SecurityException,但不能保证每个地方都是这样,尤其是四大组件中使用到Permission机制。例如, sendBroadcast(Intent)方法在数据传递到每个接收者时会检查权限,在方法调用返回后,即使权限失效,您也不会收到异常,比如:
06 动态权限的google坑、产商坑及兼容方法
6.1 google AndroidM前的权限管理机制混乱
google 在 Android M 之前,即 Android KITKAT(4.4, API level 19)推出了一套应用程序操作权限管理AppOps。AppOps所管理的是所有可能涉及用户隐私和安全的操作,包括 access notification,keep weak lock,activate vpn,display toast 等等,有些操作是不需要 Manifest 里申请权限的。
AppOps 是 Google 原生 Android 包含的功能,但是 Google 在后续版本更新时都会隐藏掉 AppOps 的入口。
直到 AndroidM 加入 Application Permission Manage 的功能(该功能基于 AppOps实现,进一步做了动态请求封装和明确的规范),AppOps 都在各 Android 厂商中以不同形式发挥着形式各异的能量。部分厂商可以通过安全管家管理危险权限。部分厂商通过安装过程和权限设置页管理权限。不同厂商能管理的权限列表项各有不同。
这种情况,我们的兼容方案是:根据系统版本在 API19-23 之间的版本做特殊处理,通过反射调用AppOpsManager 的 checkOp 来检测对应的权限是否已经授权,如果没有的话,直接提示用户去应用权限设置或安全管家中打开权限才能正常使用功能。
6.2 google AndroidM动态权限请求方式不统一
例如,悬浮窗和系统设置两个动态权限的检测和请求方式都各有一套,和标准的请求方式不同。
这种情况,我们的兼容方案时,不要将这两个权限和其它权限放在一个时机,或一个方法中去请求,在其它权限组请求流程结束后,或开始前,单独请求这两个权限。
6.3应用权限设置界面和安全管家权限不同步
例如,小米 3 申请权限的时候,拒绝权限,应用详情中的设置和安全中心的权限都会变成拒绝。这时在应用权限设置中修改权限为允许,这时安全管家还是显示未授权,但应用检测权限状态是允许,但是拿不到数据。
即,应用权限设置和应用获取权限的状态是一致的,但是安全管家和结果的状态是一样的。
这种情况,我们的兼容方案是:如果某个权限检测是已经授权,通过手机厂商 ROM 版本做特殊兼容。如果是硬件功能,尝试启动硬件,如果启动成功,则表示确实是授权了的,否则提示用户在应用权限设置或安全管家中打开权限。
如果是接口类型,则尝试获取数据,如果数据为空,或异常,则提示用户在应用权限设置或安全管家中打开权限。
6.4错误的权限状态
例如,小米 3,如果第一次拒绝了权限,再去应用设置中打开权限,这时应用检测到权限已经被授权,但实际并没有权限,即对应接口没有返回正确的数据。
用户拒绝过授权, 或系统接口判断为已经授权,但实际上没有授权的,这在动态权限刚推出来的版本上,出现的概率挺高。
这种情况,我们的兼容方案是:给对应的 Android 版本,小米 ROM 版本做适配,如果权限授权,但接口不能返回数据,提示用户在应用权限设置或安全管家中打开权限。
6.5不标准的动态权限划分
例如,华为 mate8,申请权限 WRITE_SETTINGS 权限,必须是系统应用(系统签名权限),但是有些旧版本的手机,这个权限普通应用是可以使用的。
这种情况,应用应该尽量避免使用到这种权限,因为这个权限影响的功能,在这个手机上必然无法实现。通过其它的途径达到目标,或者在特定设备上放弃此功能。
6.6不标准的动态权限请求流程
例如,魅族手机的某些版本,权限请求框没有不再提醒的勾选框,默认只跳一次权限请求框。
例如,小米某版本第一次跳出请求权限框没有不再提醒的勾选框,第二次_=B7_请求权限框就会显示不再提醒的勾选框。(android 默认机制)但是有些版本,只要有跳请求权限框,都会有不再提醒的勾选框。
例如,vivo、oppo、魅族的 6.0 以上部分版本,被厂商修改了 6.0 系统申请机制,他们修改成系统自动申请权限了。也就是说这些系统会跟以前 6.0 以下的版本一样,需要用到权限的时候系统会自动申请,就算我们主动申请也是不会跳出权限请求框。
这种情况,遵循厂商系统设计,应用没有好的办法去统一请求流程。
6.7厂商系统自定义动态权限
例如,Android Kitkat(Api 19)的国产 ROM 上会提示应用读取运动数据权限的系统弹窗,但是 Android API 中没有相关的 Sensor 的权限定义(Google 在后来的 Api 20 版本中添加到动态权限中了),即无法在AndroidManifest 中定义,也无法检查这个权限。
系统会在我们相关权限调用接口时,自动跳出权限请求框。如果用户选择了拒绝,APP 就收不到系统的回调了。
国内部分厂商比如华为、oppo,Vivo,他们将读取应用程序列表的权限暴露给了用户,让用户可以自由决定允许或者禁止应用访问该信息。
例如,华为 Mate8,AndroidM, 系统会跳出读取应用程序列表的权限请求框,阻断应用流程,等待用户选择。但是 Android 并没有相关权限定义,应用也没有读取应用程序列表,只是调用了 PackageManager相关的一些 API,就是触发这个权限请求。
例如,华为 mate8,AndroidN,系统要求读取应用程序列表权限,默认不给,会自动跳出授权请求框(没有此权限,相关接口直接返回空),但是升级到 AndroidO 又变成默认有此权限了(没有此权限只能获取到同一个开发者账号下的应用列表,不会自动跳权限请求框)。
例如,Vivo X20在调用pm.getInstalledPackages 时候弹出读取已安装应用列表对话框,允许后也无法获取应用列表,在应用的权限设置界面看,权限的状态是打开的,但是!这里还有一个安全等级选择,分为高、中、低。发现微信安全等级属于低、支付宝属于低,手动将 Demo 的安全等级调整为低,再次启动,没有弹出申请权限对话框,获取到了全部已安装应用列表。
类似的自定义权限,还有桌面快捷方式,锁屏显示,后台弹出界面。
这些情况,因为是厂商定义的,我们无法请求权限和检测权限,只能通过 FAQ 的方式,告知功能异常的用户是否拒绝过这些权限。同时可以从技术和产品层面,考虑是否可以绕过这个权限,实现相应的功能。
6.8错误的权限请求框跳出与否状态
在实际开发中,很多手机对原生系统做了修改,例如,小米4 AndroidM的shouldShowRequestPermissionRationale就一直返回 false,而且在 Request 申请权限时,如果用户选择了拒绝,则不会再弹出对话框了。
这种情况,只要系统返回不允许跳权限请求框,应用就可以提示用户去应用权限设置或安全管家中打开权限。
6.9可以在应用权限设置或安全管家中将普通权限关闭
在 Google 的权限架构中,普通权限只要在 AndroidManifest 中声明后,就会自动授权,不需要动态请求。
部分厂商可以在应用权限设置或安全管家中将 ACCESS_NETWORK_STATE,INSTALL_SHORTCUT 等权限关闭,且不能自动跳动态请求框,导致应用无法正常工作。
这种情况,我们会封装网络的调用接口,在调用功能 API 时,先 checkSelfPermission 这个普通权限是否已经授权,如果没有授权,则提示用户是否关闭了对应权限。
6.10 Target API <= 22的应用的权限也可以动态关闭
在部分 Android M 手机中,Target API <= 22 的应用使用到了动态权限可以在设置界面 or安全软件中关闭,当使用到相关接口,会自动弹框提示用户授权,如果用户没有授权,应用会出现异常。
这种情况,Target API <= 22 的应用也可通过反射调用 AppOpsManager的 checkOp 来检测对应的权限是否已经授权,并且添加没有权限的异常流程处理。
6.11地理位置信息获取不但依赖动态权限还依赖系统GPS开关
在获取地理位置的动态权限的情况下,高德等相关 SDK 并不能获取到地理位置信息,必须打开 GPS 开关才会返回信息。
这种情况下,当调用高德相关接口时,不但要动态请求地理位置权限,还要通过检查 GPS_PROVIDER 是否 enable 的方式查看 GPS 开关,如果没有打开,提示用户打开。
6.12 Wifi相关接口依赖地理位置权限
Goodle 并未定义 Wifi 的接口需要任何权限,但是在 HuaweiMate8,AndroidO 中,没有地理位置权限时,Wifi Manager 的 Scan 相关服务一直返回空值。
这种情况,我们使用 Wifi 相关接口前,也需动态检查或请求地理位置权限。
6.13厂商系统应用权限设置各不相同且变化较大
例如,不同厂商跳转到应用权限设置界面各不相同,不同版本也可能会有所差别。且很多厂商的某些版本,无法访问 build.prop,也就无法识别出厂商 ROM 版本,就无法确认应用权限设置界面到底会是哪一个。
这种情况,我们可以通过设备的品牌,跳转到正确的权限设置界面。特别难兼容的情况,可以考虑跳转到应用详情页。如果跳转到权限设置页失败,尝试跳转到应用详情页。应用详情页跳转失败,则保底跳转到系统设置页。
07 动态权限设计方案
7.1兼容
1)targetSdkVersion 为 22 及以下的低版本应用在高版本的 ROM 设备中,安装时会默认得到所有危险权限,但是如果用户在设置中关闭了权限,应用是没有办法感知的。
这个没有特别好的解决办法,一般通过 AppOps 检测权限,提示用户没有权限,请到应用设置中打开权限。
最佳建议是直接把应用的目标版本升级到 23 及以上,因为已经有很多应用市场要求必须更新应用目标版本到 23 或更高了。
2)产商定制的 Rom,除了不规范,常常在某些版本中状态也会返回错误。
这个情况,只能根据 ROM 版本,Android 版本,异常权限的情况,一一做适配。
7.2设计
设计动态权限方案,需要考虑不同版本间的兼容,目标是达到较好的用户授权和提醒体验,同时能简化不同业务中对动态权限的调用逻辑。
google 默认的权限相关流程,根据 Rom的系统版本和应用的 Target API, minSdk 可以分为 5 种情况:
- 运行在 ROM <= 22 的设备上,Target API<= 22 和 Target API >= 23 的应用。
- 运行在 ROM >= 23 的设备上,Target API<= 22(必然 minSdk <= 22),和 Target API >= 23(minSdk <= 22,或者 minSdk >= 22)的应用。
如下图:
新的重构方案,提供单权限/多权限,检测/请求/提示/跳转设置,可定制流程的方式使用动态权限接口。
调用方不用考虑兼容性问题,简单一个接口调用,就可以通过 Callback,RxEvent 感知到最后的权限情况。
设计流程图如下:
PermissionUtils对外API分三块,一组是单个权限请求接口,一组是权限检测接口,一组是多权限申请/流程可定制接口:
通过对动态权限的封装,可以将动态权限的兼容问题,检测动态权限,请求动态权限,权限设置跳转问题,实时监听权限设置结果等处理和业务功能隔离开来,业务可以非常简单快速的接入动态权限支持,提高开发效率,复用率,降低兼容性和出错成本。
狐友技术团队其他精彩文章
通过源码对SparkShuffle深入解析
Swift之Codable实战技巧
不了解GIF的加载原理?看我就够了!
获取更多资讯请关注微信公众号【搜狐技术产品】,微信后台联系搜狐技术产品小助手。
发表评论