33、权限系统调试:权限声明、运行时权限、权限组、SELinux上下文
权限问题,是Android调试里最让人头疼的几类问题之一。我见过太多开发者在权限上栽跟头——明明清单里写了,运行时也弹窗了,可功能就是跑不起来。说白了,Android的权限系统是个多层过滤网,你只打通一层是不够的。
今天咱们就把这四层全捋一遍:权限声明、运行时权限、权限组、SELinux上下文。每一层都可能卡住你。
33.1 权限声明:第一道门槛
权限声明写在AndroidManifest.xml里。这是最基础的一步,但也是最容易被忽略的。
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
这里有个坑:从Android 12开始,部分权限的声明方式变了。比如BLUETOOTH_SCAN、BLUETOOTH_CONNECT这些新权限,不能再用旧的方式声明。我去年帮一个团队排查蓝牙连接失败的问题,查了半天才发现是权限声明格式不对。
READ_MEDIA_IMAGES、READ_MEDIA_VIDEO、READ_MEDIA_AUDIO三个细粒度权限,替代了READ_EXTERNAL_STORAGE。如果你的targetSdkVersion >= 34,必须使用新权限。
33.2 运行时权限:动态申请的逻辑
Android 6.0(API 23)之后,危险权限需要运行时申请。这个流程大家应该都熟悉:检查权限 → 请求权限 → 处理回调。
// 检查权限
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
!= PackageManager.PERMISSION_GRANTED) {
// 请求权限
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.CAMERA}, REQUEST_CODE);
} else {
// 已有权限,直接操作
openCamera();
}
但调试时真正让人头疼的是权限被永久拒绝的情况。用户点了"不再询问"之后,你再弹窗也没用了。这时候只能引导用户去设置里手动开启。
adb shell dumpsys package <包名>可以查看当前应用的权限授予状态。我经常用这个命令快速确认权限是否真的被授予了,比看代码log更直接。
33.3 权限组:别被名字骗了
权限组(Permission Group)是个容易混淆的概念。同一个组里的权限,只要用户授权了一个,系统会自动授权同组的其他权限。但要注意:这只适用于运行时权限的首次授权。
| 权限组 | 包含的权限 |
|---|---|
| STORAGE | READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE |
| LOCATION | ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION |
| CAMERA | CAMERA |
| MICROPHONE | RECORD_AUDIO |
我曾经遇到过一个场景:用户授权了ACCESS_FINE_LOCATION,但我的代码里同时请求了ACCESS_COARSE_LOCATION。我以为同组权限会自动授权,结果发现checkSelfPermission返回的是PERMISSION_DENIED。为什么?因为权限组的自动授权只在请求权限的弹窗流程中生效,如果你单独检查某个权限,系统不会自动帮你补上。
33.4 SELinux上下文:最隐蔽的拦路虎
SELinux(Security-Enhanced Linux)是Android安全模型的最后一道防线。即使你的应用有了所有权限,SELinux策略仍然可能阻止你访问某些资源。
调试SELinux问题,最常用的命令是dmesg和logcat。当SELinux拒绝访问时,内核会打印类似这样的日志:
avc: denied { read } for pid=1234 comm="myapp" name="secret_file"
scontext=u:r:untrusted_app:s0:c512,c768
tcontext=u:object_r:system_data_file:s0 tclass=file permissive=0
这条日志告诉我们:untrusted_app域的应用试图读取system_data_file类型的文件,被拒绝了。解决方案要么修改SELinux策略(需要系统签名),要么换个路径存放文件。
1. 看
avc: denied日志,确认拒绝的上下文2. 用
ls -Z查看文件/进程的SELinux标签3. 用
setenforce 0临时关闭SELinux(仅限eng/userdebug版本)来验证问题是否由SELinux引起
我个人习惯在调试阶段先setenforce 0,如果问题消失,那基本就是SELinux策略的问题。但注意:生产环境绝对不能这么做,这会让设备处于不安全状态。
33.5 四层权限的调试流程图
下面这张图总结了权限调试的完整路径。你可以把它贴在工位旁边,遇到权限问题就按这个流程排查。
33.6 实战排查案例
我记得有一次,一个第三方应用反馈说无法读取外部存储的图片。我检查了清单文件——READ_EXTERNAL_STORAGE声明了。运行时权限也申请了。但就是读不到。
我打开logcat,搜avc: denied,果然有一条:
avc: denied { read } for pid=5678 comm="thirdpartyapp"
name="DCIM" dev="mmcblk0p25" ino=12345
scontext=u:r:untrusted_app:s0:c512,c768
tcontext=u:object_r:userdata_file:s0 tclass=dir permissive=0
问题出在tcontext是userdata_file,而应用期望的是fuse_file或media_rw_data_file。原来是存储挂载方式变了,SELinux策略没跟上。最终通过修改file_contexts配置解决了问题。
allow规则写错了domain。记住:系统应用也有SELinux限制,别以为有系统签名就万事大吉。
33.7 总结
权限调试,说白了就是一层一层往下查。先看清单声明,再看运行时申请,然后确认权限组逻辑,最后查SELinux。大部分问题卡在SELinux这一层,因为它的日志不够直观,很多人不会看。
嗯,这里再强调一句:不要一上来就关SELinux。先看日志,找到拒绝的原因,再针对性解决。关SELinux只是验证手段,不是解决方案。
公众号:蓝海资料掘金营,微信deep3321