我的编程空间,编程开发者的网络收藏夹
学习永远不晚

系统应用根据Uri授予权限方法详解

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

系统应用根据Uri授予权限方法详解

系统应用根据Uri授予权限的正确姿势

在我们印象中,Android6.0以后访问外部的媒体资源文件都是需要申请READ_EXTERNAL_STORAGE才可以正常访问,思考一个场景,假如我们不申请该权限,使用系统的Intent.ACTION_PICK意图跳转系统图库选取图片是否可以正常显示该图片?

答案是可以的,这是为什么呢?我们都没有申请权限,或者说是谁给了我们这个权限?带着这个疑问我们先来了解下UriPermission

UriPermission

Allow access on a per-URI basis

You can also grant permissions on a per-URI basis. When starting an activity or returning a result to an activity, set the Intent.FLAG_GRANT_READ_URI_PERMISSION, intent flag, the Intent.FLAG_GRANT_WRITE_URI_PERMISSION, intent flag, or both flags. This gives another app read, write, and read/write permissions, respectively, for the data URI that's included in the intent. The other app gains these permissions for the specific URI regardless of whether it has permission to access data in the content provider more generally.

上面是Android官网的解释,大概意思是,你可以进一步对其他应用如何访问你应用的Contete Provider或者数据URI进行精细控制,可以通过读写权限来保护自己,根据 URI 授予权限。

在启动 activity 或将结果返回给 activity 时,请设置 Intent.FLAG_GRANT_READ_URI_PERMISSION intent 标志、Intent.FLAG_GRANT_WRITE_URI_PERMISSIONintent 标志或者同时设置两者。

这样便可针对 intent 中包含的数据 URI 分别向另一个应用授予读取权限、写入权限和读写权限。

理解完UriPermission,上面的问题就可以解释了,虽然我们没有申请读取的权限,但是系统图库在选图后将结果返回activity时设置了Intent.FLAG_GRANT_READ_URI_PERMISSION,这样我们就具有了读取该Uri指定图片的权限。

理想很丰满,现实很骨感。

背景

最近在项目中上线一款自研的图库应用(systemUid),支持系统默认选图action跳转,给调用者返回已选的Uri资源地址,因为安全合规整改的原因,一些第三方应用去掉了读写权限的申请,问题就被暴露出来了,第三方应用无法正常通过图库获取到图片资源。

分析

分析堆栈信息可以定位到,是调用者没有访问Uri的权限导致的异常,而我们自研图库在选图回传的时候是有设置FLAG_GRANT_READ_URI_PERMISSION,把URI的临时访问权限传递给调用者,且报错的堆栈打印是在第三方应用,所以可以初步判断问题应该是出自系统的权限授予过程。

我们可以通过context.grantUriPermission()作为切入点,来分析下系统是如何授予Uri权限

最终调用的是UriGrantsManagerService$checkGrantUriPermission()

checkGrantUriPermission

int checkGrantUriPermission(int callingUid, String targetPkg, GrantUri grantUri,
        final int modeFlags, int lastTargetUid) {
   ....
    // Bail early if system is trying to hand out permissions directly; it
    // must always grant permissions on behalf of someone explicit.
    final int callingAppId = UserHandle.getAppId(callingUid);
    if ((callingAppId == SYSTEM_UID) || (callingAppId == ROOT_UID)) {
        if ("com.android.settings.files".equals(grantUri.uri.getAuthority())
                || "com.android.settings.module_licenses".equals(grantUri.uri.getAuthority())) {
            // Exempted authority for
            // 1. cropping user photos and sharing a generated license html
            //    file in Settings app
            // 2. sharing a generated license html file in TvSettings app
            // 3. Sharing module license files from Settings app
        } else {
            Slog.w(TAG, "For security reasons, the system cannot issue a Uri permission"
                    + " grant to " + grantUri + "; use startActivityAsCaller() instead");
            return -1;
        }
    }
    ....
}
复制代码

问题就是出现在这里,如果你的应用是root用户,或者是具有系统级权限(systemUid),并且提供的Uri的authority不是指定的,就会拒绝授权 (return -1),也就是说这个Uri的权限并没有传递成功。

这一点在上面的日志也有体现。

/system_process W UriGrantsManagerService: For security reasons, the system cannot issue a Uri permission grant to

系统为什么要这么做?

注释里面有说,出于安全原因,系统应用不能使用startActivityAsCaller()来直接授予Uri权限,它必须显式地让应用自己授予权限。只有以下几种情况才会豁免授权

  • 裁剪用户照片并共享生成的许可HTML文件的设置应用程序
  • 在TvSettings app中共享生成的许可html文件
  • 从设置应用程序共享模块license文件

调用者端

分析完图库端,我们再来看下调用者端的异常堆栈打印

客户端远程调用服务端打开指定文件,然后服务端把文件描述符跨进程传递到客户端(后面Binder驱动跨进程传递文件描述符就不展开分析)

通过分析时序图,可以定位到报错的堆栈信息是发生在enforceCallingPermissionInternal()

enforceCallingPermissionInternal()

 private void enforceCallingPermissionInternal(Uri uri, boolean forWrite) {
          ... // 省略部分代码
          // First, check to see if caller has direct write access
          if (forWrite) {
              final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_UPDATE, uri, table, null);
              try (Cursor c = qb.query(db, new String[0], null, null, null, null, null)) {
                  if (c.moveToFirst()) {
                      // Direct write access granted, yay!
                      return;
                  }
              }
          }
          ... // 省略部分代码
          // Second, check to see if caller has direct read access
          final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_QUERY, uri, table, null);
          try (Cursor c = qb.query(db, new String[0], null, null, null, null, null)) {
              if (c.moveToFirst()) {
                  if (!forWrite) {
                      // Direct read access granted, yay!
                      return;
                  } else if (allowUserGrant) {
                      // Caller has read access, but they wanted to write, and
                      // they'll need to get the user to grant that access
                      final Context context = getContext();
                      final PendingIntent intent = PendingIntent.getActivity(context, 42,
                              new Intent(null, uri, context, PermissionActivity.class),
                              FLAG_ONE_SHOT | FLAG_CANCEL_CURRENT | FLAG_IMMUTABLE);
 
                      final Icon icon = getCollectionIcon(uri);
                      final RemoteAction action = new RemoteAction(icon,
                              context.getText(R.string.permission_required_action),
                              context.getText(R.string.permission_required_action),
                              intent);
 
                      throw new RecoverableSecurityException(new SecurityException(
                              getCallingPackageOrSelf() + " has no access to " + uri),
                              context.getText(R.string.permission_required), action);
                  }
              }
          }
 
          throw new SecurityException(getCallingPackageOrSelf() + " has no access to " + uri);
      }

在这个方法里面,它会根据参数forWrite判断当前调用者是否拥有读写权限,如果没有则会抛出异常提示,和上面报错的异常堆栈符合。

解决办法

以下两种方法都可以

  • 去除应用systemUid配置
  • 修改framework源码

考虑到修改系统源码的影响面比较大,所以采用去除systemUid的方式,再次验证跳转选图后可以正常加载。查看系统原生图库的清单文件配置,也是没有设置systemUid,原生图库也是采用了这种方式。

扩展

系统原生图库既不是系统应用,也没有动态申请存储权限,那它是怎么获取系统的存储权限的?

如果有了解过系统权限的授予流程,可以知道Android系统在开机后会对一些特殊的应用进行自动授权,而运行时权限的默认授予工作由DefaultPermissionGrantPolicy类的grantDefaultPermissions方法完成。

在这个方法里面可以看到它对图库进行默认授予存储权限的代码,具体是通过查找清单文件配置的category是Intent.CATEGORY_APP_GALLERY的应用。

以上就是系统应用根据Uri授予权限方法详解的详细内容,更多关于系统应用Uri授权的资料请关注编程网其它相关文章!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

系统应用根据Uri授予权限方法详解

下载Word文档到电脑,方便收藏和打印~

下载Word文档

编程热搜

  • Android:VolumeShaper
    VolumeShaper(支持版本改一下,minsdkversion:26,android8.0(api26)进一步学习对声音的编辑,可以让音频的声音有变化的播放 VolumeShaper.Configuration的三个参数 durati
    Android:VolumeShaper
  • Android崩溃异常捕获方法
    开发中最让人头疼的是应用突然爆炸,然后跳回到桌面。而且我们常常不知道这种状况会何时出现,在应用调试阶段还好,还可以通过调试工具的日志查看错误出现在哪里。但平时使用的时候给你闹崩溃,那你就欲哭无泪了。 那么今天主要讲一下如何去捕捉系统出现的U
    Android崩溃异常捕获方法
  • android开发教程之获取power_profile.xml文件的方法(android运行时能耗值)
    系统的设置–>电池–>使用情况中,统计的能耗的使用情况也是以power_profile.xml的value作为基础参数的1、我的手机中power_profile.xml的内容: HTC t328w代码如下:
    android开发教程之获取power_profile.xml文件的方法(android运行时能耗值)
  • Android SQLite数据库基本操作方法
    程序的最主要的功能在于对数据进行操作,通过对数据进行操作来实现某个功能。而数据库就是很重要的一个方面的,Android中内置了小巧轻便,功能却很强的一个数据库–SQLite数据库。那么就来看一下在Android程序中怎么去操作SQLite数
    Android SQLite数据库基本操作方法
  • ubuntu21.04怎么创建桌面快捷图标?ubuntu软件放到桌面的技巧
    工作的时候为了方便直接打开编辑文件,一些常用的软件或者文件我们会放在桌面,但是在ubuntu20.04下直接直接拖拽文件到桌面根本没有效果,在进入桌面后发现软件列表中的软件只能收藏到面板,无法复制到桌面使用,不知道为什么会这样,似乎并不是很
    ubuntu21.04怎么创建桌面快捷图标?ubuntu软件放到桌面的技巧
  • android获取当前手机号示例程序
    代码如下: public String getLocalNumber() { TelephonyManager tManager =
    android获取当前手机号示例程序
  • Android音视频开发(三)TextureView
    简介 TextureView与SurfaceView类似,可用于显示视频或OpenGL场景。 与SurfaceView的区别 SurfaceView不能使用变换和缩放等操作,不能叠加(Overlay)两个SurfaceView。 Textu
    Android音视频开发(三)TextureView
  • android获取屏幕高度和宽度的实现方法
    本文实例讲述了android获取屏幕高度和宽度的实现方法。分享给大家供大家参考。具体分析如下: 我们需要获取Android手机或Pad的屏幕的物理尺寸,以便于界面的设计或是其他功能的实现。下面就介绍讲一讲如何获取屏幕的物理尺寸 下面的代码即
    android获取屏幕高度和宽度的实现方法
  • Android自定义popupwindow实例代码
    先来看看效果图:一、布局
  • Android第一次实验
    一、实验原理 1.1实验目标 编程实现用户名与密码的存储与调用。 1.2实验要求 设计用户登录界面、登录成功界面、用户注册界面,用户注册时,将其用户名、密码保存到SharedPreference中,登录时输入用户名、密码,读取SharedP
    Android第一次实验

目录