权限管理:Android权限模型演进、最小权限原则、运行时权限处理、自定义权限
聊到Android安全,权限管理绝对是个绕不开的话题。我做了这么多年安全开发,见过太多因为权限处理不当引发的安全事故。说白了,权限就是应用和系统之间的「通行证」——你拿到的权限越多,能做的事情就越多,但风险也越大。
今天咱们就把权限管理这块彻底讲透。从历史演进到实战编码,从系统权限到自定义权限,我会把我在项目中踩过的坑、总结的经验都分享出来。
Android权限模型的演进
Android的权限模型不是一天建成的。它经历了三个阶段,每个阶段都是为了解决上一阶段的问题。
| 版本 | 权限模型 | 核心特点 |
|---|---|---|
| Android 1.0 - 5.1 | 安装时授权 | 安装时一次性授予,无法撤销 |
| Android 6.0 - 9.0 | 运行时授权 | 用户可动态授予/撤销,支持权限分组 |
| Android 10+ | 精细化授权 | 新增仅限一次、后台位置等细粒度控制 |
我记得刚入行那会儿,Android 4.4还是主流。那时候权限管理特别粗暴——你安装应用时,系统会把所有权限列出来,用户要么全接受,要么不安装。很多用户根本看不懂那些权限列表,直接点「安装」就完事了。
这种模式的问题很明显。恶意应用可以申请一堆跟功能无关的权限,比如一个手电筒应用要读取联系人,用户也没办法拒绝。直到Android 6.0推出了运行时权限模型,才真正把控制权交还给了用户。
核心变化:从「安装时一次性授权」变成了「使用时按需授权」。用户可以在任何时候撤销某个权限,应用必须处理权限被拒绝的情况。
到了Android 10,权限管理又进了一步。新增了「仅限一次」选项,用户可以让应用只在使用期间访问位置或摄像头。Android 11更是加入了「自动重置权限」功能——如果用户长时间不用某个应用,系统会自动撤销之前授予的权限。
为什么会这样演进?说白了,用户隐私意识越来越强,监管也越来越严。Google必须不断收紧权限控制,才能应对层出不穷的隐私问题。
最小权限原则
最小权限原则,听起来很学术,其实道理很简单:只申请你真正需要的权限,一个都不要多。
我在做代码审查时,经常看到这样的场景:一个简单的记事本应用,居然申请了读取联系人、访问位置、拍照权限。你想想看,用户会怎么想?「这破应用想干嘛?」
具体来说,最小权限原则包含三个层面:
- 功能层面:每个权限必须对应一个明确的功能。如果某个功能可以不用权限实现,那就别申请。
- 时机层面:只在需要的时候申请权限,不要在应用启动时一股脑全要了。
- 范围层面:能用普通权限就别用危险权限,能用单次授权就别用始终允许。
我的习惯:每次写新功能前,我会先问自己三个问题:这个功能真的需要权限吗?有没有替代方案?如果用户拒绝权限,功能能不能降级运行?
举个例子。如果你的应用需要扫描Wi-Fi列表,很多人第一反应是申请ACCESS_FINE_LOCATION权限。但如果你只需要获取Wi-Fi名称和信号强度,其实可以用ACCESS_WIFI_STATE权限配合WifiManager来实现,完全不需要位置权限。
我曾经接手过一个项目,里面申请了十几个权限,但实际用到的只有五个。剩下的权限都是早期版本遗留的,或者某个开发人员「以防万一」加上的。清理掉这些冗余权限后,应用在Google Play的评分都涨了——因为用户看到权限列表变短了,信任感自然就上来了。
运行时权限处理
运行时权限是Android 6.0引入的核心机制。处理得好,用户体验流畅;处理不好,用户直接卸载。
先看一个标准的权限请求流程:
// 检查权限是否已授予
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
!= PackageManager.PERMISSION_GRANTED) {
// 是否需要向用户解释为什么需要这个权限
if (ActivityCompat.shouldShowRequestPermissionRationale(this,
Manifest.permission.CAMERA)) {
// 显示解释对话框
showRationaleDialog();
} else {
// 直接请求权限
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.CAMERA},
REQUEST_CAMERA_PERMISSION);
}
} else {
// 权限已授予,直接执行操作
openCamera();
}
这段代码看起来简单,但实际开发中有很多坑。我一个个说。
第一个坑:shouldShowRequestPermissionRationale的返回值
这个方法的返回值很多人搞不清楚。它返回true的情况只有一种:用户之前拒绝过权限请求,但没有勾选「不再询问」。如果用户第一次请求,或者用户勾选了「不再询问」,它都返回false。
所以正确的处理逻辑应该是:
- 返回true → 用户拒绝过但没勾选「不再询问」→ 显示解释说明
- 返回false且权限未授予 → 可能是第一次请求,也可能是用户勾选了「不再询问」→ 直接请求或引导用户去设置
第二个坑:不要一次性请求多个权限
我见过有些应用一上来就请求五六个权限,用户看到一堆弹窗,直接懵了。正确的做法是按需请求——用户要拍照时才请求相机权限,要发定位时才请求位置权限。
注意:如果用户连续拒绝两次权限请求,系统会自动把该权限标记为「不再询问」。之后你再调用requestPermissions,系统会直接拒绝,不会弹出任何对话框。这时候你必须引导用户去系统设置里手动开启权限。
第三个坑:权限被拒绝后的降级处理
很多应用在用户拒绝权限后直接崩溃或闪退,这是最糟糕的体验。你应该做的是:
@Override
public void onRequestPermissionsResult(int requestCode,
@NonNull String[] permissions,
@NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_CAMERA_PERMISSION) {
if (grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// 权限授予,打开相机
openCamera();
} else {
// 权限被拒绝,降级处理
showToast("无法使用相机,已切换为相册选择");
openGallery();
}
}
}
我曾经做过一个扫码应用,用户拒绝相机权限后,我们自动切换到手动输入条码的模式。虽然体验差了点,但至少应用还能用。很多用户后来觉得手动输入太麻烦,反而主动去开启了权限。
自定义权限
自定义权限是Android提供的一个高级特性,主要用于应用之间的安全通信。如果你的应用暴露了某些组件给其他应用调用,自定义权限可以控制谁有资格调用。
定义自定义权限需要在AndroidManifest.xml中声明:
<permission
android:name="com.example.myapp.permission.MY_CUSTOM_PERMISSION"
android:label="@string/permission_label"
android:description="@string/permission_description"
android:protectionLevel="signature" />
protectionLevel有几种取值,我列个表方便你对照:
| 保护级别 | 说明 | 使用场景 |
|---|---|---|
| normal | 低风险,安装时自动授予 | 不涉及用户隐私的权限 |
| dangerous | 高风险,需要运行时请求 | 涉及用户隐私的权限 |
| signature | 仅同签名应用可获取 | 自家应用之间的通信 |
| signatureOrSystem | 同签名或系统应用可获取 | 系统级应用通信 |
我个人最常用的是signature级别。为什么?因为同签名意味着这些应用都是你自家开发的,安全性有保障。而且用户不需要做任何操作,权限会自动授予。
举个例子。我做过一个电商应用,包含主App和支付插件两个APK。支付插件暴露了一个支付Activity,主App需要调用它。这时候自定义权限就派上用场了:
<!-- 支付插件中声明权限 -->
<permission
android:name="com.example.shop.permission.PAY"
android:protectionLevel="signature" />
<!-- 支付插件中保护Activity -->
<activity
android:name=".PayActivity"
android:permission="com.example.shop.permission.PAY">
<intent-filter>
<action android:name="com.example.shop.action.PAY" />
</intent-filter>
</activity>
<!-- 主App中声明使用权限 -->
<uses-permission android:name="com.example.shop.permission.PAY" />
这样配置后,只有跟支付插件使用相同签名的应用才能调用PayActivity。其他应用即使知道这个Activity的包名和类名,也无法启动它。
避坑指南:我曾经犯过一个错误——把自定义权限的protectionLevel设成了dangerous。结果用户每次调用支付功能时都要弹窗确认,体验极差。后来改成signature级别,问题就解决了。所以记住:自家应用之间的通信,用signature就够了。
另外要注意一点:自定义权限的命名一定要用反向域名格式,比如com.example.app.permission.XXX。这样能避免跟其他应用的权限冲突。我见过有人直接用「MY_PERMISSION」这种名字,结果跟另一个应用的权限重名了,导致各种奇怪的问题。
知识体系总览
下面这张图把权限管理的核心知识点串起来了,你可以对照着回顾一下:
权限管理说难不难,说简单也不简单。关键在于你有没有把用户隐私放在第一位。我见过太多应用因为权限问题被下架、被投诉、被差评。其实只要遵循最小权限原则,做好运行时权限的降级处理,大部分问题都能避免。
嗯,关于权限管理就讲到这里。记住一句话:权限是能力,也是责任。拿到权限意味着你要对用户的数据负责,别滥用,别乱用。
公众号:蓝海资料掘金营,微信deep3321