如何通过 android:readPermission 和 android:writePermission 控制访问权限?
解读
国内面试中,这道题表面问“怎么用”,实际想验证三件事:
- 是否真正写过自定义 ContentProvider,而不是只调过 query/insert;
- 是否理解 Android 权限从“声明-申请-校验”到“内核级强制”的完整链路;
- 能否把“最小权限原则”落地到组件级,而不是笼统地给整个应用申请一个大权限。
因此,回答必须覆盖:Manifest 声明、权限等级、自定义权限、uid 校验、SELinux 强制、以及国内无 GMS 场景下的适配细节。
知识点
-
Provider 标签属性
- android:readPermission:限制 openFile/query 等读路径;
- android:writePermission:限制 insert/update/delete/openFile(w) 等写路径;
- 若同时声明,系统会分别校验;若只声明其一,则另一侧默认放行(Android 14 以前行为)。
-
权限定义与等级
- 自定义权限需在 <permission> 中声明 protectionLevel: – normal:安装即授予,仅适合非敏感数据; – dangerous:国内需动态申请,用户可拒绝; – signature:仅限与声明者同证书的应用,国内厂商预装场景最常用; – signature|privileged(系统白名单):需预置到 /system/etc/permissions/privapp-permissions.xml,车机、TV 盒子常用。
-
系统校验流程
- AMS 在 bindApplication 阶段把权限缓存到 ProcessRecord;
- ContentProvider 被调用时,AMS 通过 checkComponentPermission() 对比调用者 uid 与所需权限;
- 若未通过,抛出 SecurityException,应用层可捕获并降级;
- SELinux 随后进行二次强制,防止 uid 伪造。
-
国内特殊场景
- 无 GMS:不能用 “com.google.android.gms.permission.CHANGE_NETWORK_STATE” 之类谷歌权限,必须自建权限;
- 厂商 ROM:部分机型对 signature 级权限有“权限防火墙”,需在厂商后台额外声明;
- 小程序/插件化:插件进程 uid 与宿主相同,权限继承,需用 Binder.getCallingUid() 二次鉴权,防止“越权调用”。
-
最小权限落地技巧
- 读写分离:给只读场景单独定义 xx.permission.READ,写场景用 xx.permission.WRITE,方便后台审计;
- 参数级细粒度:在 Provider 内部再检查 Uri path,如 /user/{id},拒绝访问非本用户数据;
- 可调试开关:BuildConfig.DEBUG 时通过 adb shell pm grant 一键授权,自动化测试无需弹窗。
答案
步骤一:在 AndroidManifest.xml 中定义两条自定义权限,等级选 signature,避免国内用户手动授权。
<permission android:name="com.example.permission.READ_CONFIG" android:label="读取配置" android:protectionLevel="signature" /> <permission android:name="com.example.permission.WRITE_CONFIG" android:label="修改配置" android:protectionLevel="signature" />
步骤二:声明 Provider 并绑定权限。
<provider android:name=".provider.ConfigProvider" android:authorities="com.example.provider.config" android:exported="true" android:readPermission="com.example.permission.READ_CONFIG" android:writePermission="com.example.permission.WRITE_CONFIG" />
步骤三:在 Provider 内部再做一次 uid 到包名的映射校验,防止同证书应用伪造请求。
@Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { int callingUid = Binder.getCallingUid(); if (callingUid == Process.myUid()) return queryInternal(uri, projection, selection, selectionArgs, sortOrder); String[] pkgs = getContext().getPackageManager().getPackagesForUid(callingUid); if (pkgs == null || !ArrayUtils.contains(pkgs, "com.example.authorized")) { throw new SecurityException("未在白名单的包名内"); } return queryInternal(uri, projection, selection, selectionArgs, sortOrder); }
步骤四:国内上架合规补充。
- 在应用市场后台“权限声明”模块把两条自定义权限录入,否则部分厂商扫描会报“未声明的权限被使用”;
- 隐私政策中写明“配置数据仅同证书应用可读写,不会上云”;
- 提供 adb 命令供企业客户调试:adb shell pm grant com.example.client com.example.permission.READ_CONFIG。
通过以上四步,即可在系统层、框架层、业务层形成“三级门禁”,既满足国内各大应用市场与车机厂商的安全审计,又能在签名级隔离的前提下实现零弹窗体验。
拓展思考
- 如果未来需求把“写权限”拆成“增量写”与“全量写”,是否继续用 android:writePermission 还是改在代码里用 UriMatcher 做二次分发?哪种方案对 SELinux 审计更友好?
- Android 14 开始,系统对“仅声明读权限”的 Provider 默认关闭写路径,如何写一套 UT 覆盖这一行为差异?
- 在车载场景,系统应用与普通应用证书不同,但仍需共享诊断数据,能否通过 signatureOrSystem + 白名单机制实现?若车机 OTA 升级后白名单被重置,如何设计自恢复通道?
- 国内小程序平台(如微信、支付宝)内嵌独立进程,uid 与宿主不同,且无法获取 signature 权限,是否考虑把 ContentProvider 改为 AIDL + 自定义令牌的双向校验?