如何通过 android:readPermission 和 android:writePermission 控制访问权限?

解读

国内面试中,这道题表面问“怎么用”,实际想验证三件事:

  1. 是否真正写过自定义 ContentProvider,而不是只调过 query/insert;
  2. 是否理解 Android 权限从“声明-申请-校验”到“内核级强制”的完整链路;
  3. 能否把“最小权限原则”落地到组件级,而不是笼统地给整个应用申请一个大权限。

因此,回答必须覆盖:Manifest 声明、权限等级、自定义权限、uid 校验、SELinux 强制、以及国内无 GMS 场景下的适配细节。

知识点

  1. Provider 标签属性

    • android:readPermission:限制 openFile/query 等读路径;
    • android:writePermission:限制 insert/update/delete/openFile(w) 等写路径;
    • 若同时声明,系统会分别校验;若只声明其一,则另一侧默认放行(Android 14 以前行为)。
  2. 权限定义与等级

    • 自定义权限需在 <permission> 中声明 protectionLevel: – normal:安装即授予,仅适合非敏感数据; – dangerous:国内需动态申请,用户可拒绝; – signature:仅限与声明者同证书的应用,国内厂商预装场景最常用; – signature|privileged(系统白名单):需预置到 /system/etc/permissions/privapp-permissions.xml,车机、TV 盒子常用。
  3. 系统校验流程

    • AMS 在 bindApplication 阶段把权限缓存到 ProcessRecord;
    • ContentProvider 被调用时,AMS 通过 checkComponentPermission() 对比调用者 uid 与所需权限;
    • 若未通过,抛出 SecurityException,应用层可捕获并降级;
    • SELinux 随后进行二次强制,防止 uid 伪造。
  4. 国内特殊场景

    • 无 GMS:不能用 “com.google.android.gms.permission.CHANGE_NETWORK_STATE” 之类谷歌权限,必须自建权限;
    • 厂商 ROM:部分机型对 signature 级权限有“权限防火墙”,需在厂商后台额外声明;
    • 小程序/插件化:插件进程 uid 与宿主相同,权限继承,需用 Binder.getCallingUid() 二次鉴权,防止“越权调用”。
  5. 最小权限落地技巧

    • 读写分离:给只读场景单独定义 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); }

步骤四:国内上架合规补充。

  1. 在应用市场后台“权限声明”模块把两条自定义权限录入,否则部分厂商扫描会报“未声明的权限被使用”;
  2. 隐私政策中写明“配置数据仅同证书应用可读写,不会上云”;
  3. 提供 adb 命令供企业客户调试:adb shell pm grant com.example.client com.example.permission.READ_CONFIG。

通过以上四步,即可在系统层、框架层、业务层形成“三级门禁”,既满足国内各大应用市场与车机厂商的安全审计,又能在签名级隔离的前提下实现零弹窗体验。

拓展思考

  1. 如果未来需求把“写权限”拆成“增量写”与“全量写”,是否继续用 android:writePermission 还是改在代码里用 UriMatcher 做二次分发?哪种方案对 SELinux 审计更友好?
  2. Android 14 开始,系统对“仅声明读权限”的 Provider 默认关闭写路径,如何写一套 UT 覆盖这一行为差异?
  3. 在车载场景,系统应用与普通应用证书不同,但仍需共享诊断数据,能否通过 signatureOrSystem + 白名单机制实现?若车机 OTA 升级后白名单被重置,如何设计自恢复通道?
  4. 国内小程序平台(如微信、支付宝)内嵌独立进程,uid 与宿主不同,且无法获取 signature 权限,是否考虑把 ContentProvider 改为 AIDL + 自定义令牌的双向校验?