首页 > 代码库 > Android Permission介绍

Android Permission介绍

  • 权限介绍
    • 用户ID和文件访问权限的关系
    • 用户权限
      • 权限分类
      • 权限组-Permission groups
  • 权限问题实例
    • bug描述
    • bug分析
    • 解决方案
      • AndroidManfisetxml里注册权限添加
      • 代码种添加runntime permission 检查机制这一部分三部走
    • 终极解法
  • 一点疑惑

权限介绍

Android系统上每一个独立的应用运行在不同的系统空间,以User ID和Group ID来标识。不同应用之间互相访问数据接口资源,就牵涉到权限问题,本篇对权限问题做一个总结简介。

用户ID和文件访问权限的关系

在我们安装一个新应用的时候,Android系统就会为这个新应用分配一个user ID,这个user ID全局唯一,也就是说不可能存在两个不同的应用在同一个手机设备上拥有相同的user id,并且user id一旦分配好,就不会在变,除非用户卸载重新安装,同一个应用安装在不同的手机上,其user ID不一定相同。
由于Android的安全机制是进程级别的,user id不同,那么其运行的进程也就不同,因此通常来讲两个不同应用无法运行在对方的进程中,除非在 AndroidManifest.xml文件中配置相同的 sharedUserId。配置了相同 sharedUserId的应用在系统层会被看成同一个应用,他们的user id和权限都相同。为了防止恶意程序利用这点,恶意配置一个和目标应用相同的sharedUserId,来做破坏,Android的安全机制还规定了签名一致性。总结来说,想要保证两个不同的应用运行在同一个进程中,必须满足两点:

  • 相同的签名
  • 配置相同的 sharedUserId。

应用产生的数据会和应用的userid对应起来,其他应用是无法正常访问的。但如果使用getSharedPreferences(String, int), openFileOutput(String, int), 或者是 openOrCreateDatabase(String, int, SQLiteDatabase.CursorFactory)方法创建一个存储数据文件时,使用了 MODE_WORLD_READABLE 或者 MODE_WORLD_WRITEABLE标志,那么其他应用则对该文件拥有了读/写权限。文件的所有者虽然还是属于创建文件的程序,但它变成了全局可写可读的了。
查看user id可以使用命令

adb shell dumpsys package

比如如果要查看Settings应用的user id可以使用如下命令:

adb shell dumpsys package com.android.settings| grep -A5 userId -b5

得到输出结果入下图:
技术分享
通过上面的输出信息,我们可以看到settings应用的userid 为1000,这个userid比较特殊,特殊在它的值小于10000,通常来用户安装的三方应用userid都会大于10000.settings userid小于10000,是因为它配置了sharedUserId=”android.uid.system”,这样它的userid就与android.uid.system进程相同了。之所以这样配置,是因为Settins应用好多操作都需要有较高的系统权限。

用户权限

权限分类

上面提到了通过配置相同的shareUserId来达到不同应用间的数据共享,但这种做法局限性太大。更通常的做法是通过uses-permission来达到数据共享的目的。当开发一个应用时,如果我们不去配置uses-permission,那么这个应用几乎是什么也做不了,系统没有给应用赋予默认的基础权限,任何操作权限都必须在AndroidManifest.xml文件中声明。这里权限对系统来说,又分为四种:

  • 普通权限(normal permission):即使拥有了该类权限,用户的隐私数据被泄露篡改的风险也很小。
  • 敏感权限(dangerous permission):跟普通权限相反,一旦某个应该获取了该类权限,用户的隐私数据就面临被泄露篡改的风险。比如READ_CONTACTS权限就属于敏感权限。
  • 签名权限(signature permission):该类权限只对拥有相同签名的应用开放,比如手Q程序自定义了一个permission,微信要去访问QQ的某个数据时,必须要拥有该权限,那么手Q在自定义该权限时可以在权限标签中加入android:protectionLevel=”signature” 。然后微信和手Q发布时采用相同的签名,微信就可以申请访问手Q中的某类开放数据。即使其他程序知道了这个开放数据的接口,也在manifest注册申请了权限,但由于签名不同,还是无法访问的。
  • 系统签名权限(signatureOrSystem permission):与 signature permission类似,但它不光要求签名相同,还要求是同类的系统级应用,一般手机厂商开发的预制应用,才会用到该类权限。

可以通过命令

adb shell pm list permissions -d -g

查看当前手机系统的敏感权限。
技术分享
查看上图的输出结果,我们可以看到敏感权限也可以自定义的,比如第一条google的权限。另外这里也引出了 permission-group概念。

权限组-Permission groups

不管是普通权限还是敏感权限,甚至自定义的权限都可以属于一个权限组。但是权限组的的概念还是主要用在敏感权限上,通过它能减少用户授权次数。
如果用户已经对同一个权限组里的某一个权限授权通过,那么该权限组里的其他权限也会被系统认为已经开放,不会二次弹出授权提示框。例如,应用之前请求过READ_CONTACTS 权限,并且系统也授权通故,当此时应用在申请WRITE_CONTACTS权限时,系统会直接开放该权限。

在Android6.0之后,Android对权限管理的更加严苛。普通权限申请跟之前版本一样,只用在AndriodManifest.xml文件里配置即可。但是敏感权限不光要在AndroidManifest.xml文件中声明,还要在代码种提供给用户一个运行时权限许可的接口界面。该功能依赖两个方面:

  • 用户设备系统版本。比如用户手机是4.0系统或者6.0系统。
  • 应用自身设置的targetSdkVersion。

比如用户手机系统是6.0,应用的targetSdkVersion设置为23,申请的是敏感权限,那么系统会强制要求运行时权限检查,如果此时AndroidManifest.xml文件中声明了敏感权限,但没有在代码中为用户添加运行时权限检查的入口,那么申请的敏感权限是根本获取不到的。如果系统虽然是Android6.0的,但应用的targetSdkVersion设置为22或者更低,则权限检查方案跟之前的版本一样,系统不会强制要求运行时权限检查。
Android6.0之后新应用的开发需要特别注意用户可以随时控制权限的开放与否,比如用户可能在安装应用的时候将应用申请的权限都许可了,但过了一段时间,用户可能会在在setting里又将申请的某些权限关闭,更极端的case,用户在使用应用的过程中,切换到权限管理界面将应用的某些权限关闭,这些case都需要引在开发时兼顾到。

权限问题实例

前面介绍了权限相关的知识,下面用一个最近碰到的权限问题bug来看看如何debug权限问题。

bug描述:

用户接受一条附件为gif图片的彩信后,用gallery打开浏览该图片,可以正常浏览,点击设置菜单,试图将该图片设置为壁纸,或者联系人头像时,没有反应。复现场景如下图
技术分享

bug分析:

从现象看无FC,系统也正常弹出了选择应用框,但点击则没反应。直观现象暂时猜不出头绪,转而抓取log,则问题一目了然。
技术分享
Log明确的显示是权限问题:

01-03 16:08:13.882 13468 13468 E AndroidRuntime: Caused by: java.lang.SecurityException: Permission Denial:
reading com.android.providers.telephony.MmsProvider uri content://mms/part/2 from pid=13468, uid=10098
requires android.permission.READ_SMS, or grantUriPermission()

问题的出错场景应该是:gif图是MMS的私有数据,gallery在设置壁纸时需要先去取MMS的这个私有数据,但系统检测到gallery没有读取短彩信的权限,因此访问被拒绝,导致问题发生。

解决方案:

有了上面的分析,那么解决方案就是给gallery添加上SMS权限。由于gallery是基于Android6.0系统开发,并且targetSdkVersion制定为23.因此得分两步来完成赋权的过程。

AndroidManfiset.xml里注册权限,添加

<uses-permission android:name="android.permission.READ_SMS"/>

代码种添加runntime permission 检查机制。这一部分三部走。

  • check权限,调用方法:
ContextCompat.checkSelfPermission(@NonNull Context context, @NonNull String permission)

已经获取权限返回:PackageManager.PERMISSION_GRANTED,否则返回:PackageManager.PERMISSION_DENIED

  • 申请权限.
ActivityCompat.requestPermissions(final @NonNull Activity activity,final @NonNull String[] permissions, final int requestCode)
  • 响应申请权限回调,在申请权限的Activity里复写:
@Override
public void onRequestPermissionsResult(int requestCode,
            String[] permissions, int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}

这里可以将check权限和申请权限封装到同一个方法中。例如:

public static boolean checkAndRequestForRunntimePermission(Activity activity,String []  permissionsNeeded) {
        // 1 检查权限
        ArrayList<String> permissionsNeedRequest = new ArrayList<String>();
        for (String permission : permissionsNeeded) {
            if (ContextCompat.checkSelfPermission(activity, permission)
                    == PackageManager.PERMISSION_GRANTED) {
                continue;
            }
            permissionsNeedRequest.add(permission);
        }
        // 2 请求权限
        if (permissionsNeedRequest.size() == 0) {
            return true;
        } else {
            String[] permissions = new String[permissionsNeedRequest.size()];
            permissions = permissionsNeedRequest.toArray(permissions);
            ActivityCompat.requestPermissions(activity, permissions, 0);
            return false;
        }
    }

根据上面的方案,来一步步解决这个bug。为了便于讲述,模拟一个简单的场景:
ActivityA对应图片的显示界面,让它负责读取一条彩信数据,简单的给出彩信附件的文件大小。
AndroidManfiset.xml里添加

<uses-permission android:name="android.permission.READ_SMS"/>

ActivityA代码如下:

public class ActivityA extends Activity implements OnClickListener {
    private static final String TAG = "azhengye/ActivityA";
    private EditText mEditText = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_a);
        findViewById(R.id.start_b).setOnClickListener(this);
        mEditText = (EditText) findViewById(R.id.edit_tx);
        readFileSize();
    }
    private void readFileSize() {
        InputStream file = null;
        try {
            file = this.getContentResolver().openInputStream(
                    Uri.parse("content://mms/part/2"));
            Log.d(TAG, "file.available()==" + file.available());
            mEditText.setText("file size is:" + file.available());
            file.close();
        } catch (FileNotFoundException e) {
            Log.d(TAG, "read mms FileNotFoundException" + e.getMessage());
            e.printStackTrace();
        } catch (IOException e) {
            Log.d(TAG, "read mms IOException" + e.getMessage());
        }
    }
    @Override
    public void onClick(View v) {
        Intent intent = new Intent(this, ActivityB.class);
        startActivity(intent);
    }
}

此时先看下没有在代码里添加运行时权限检查的效果:
技术分享
可以看到单纯的在AndroidManifest里添加SMS权限程序还有异常,后台log也给出了异常原因:

10-29 17:15:55.091 32049 32049
E AndroidRuntime: java.lang.RuntimeException: Unable to start activity
ComponentInfo{com.azhengye.demopermission/com.azhengye.demopermission.ActivityA}: java.lang.SecurityException:
Permission Denial: reading com.android.providers.telephony.MmsProvider uri content://mms/part/2 from pid=32049, uid=10170 requires
android.permission.READ_SMS, or grantUriPermission()

如果手动赋权SMS,则ActivityA里正常读取到了彩信附件的大小。
现在正式添加赋权代码。

  • 增加RunntimePermissionHelper工具类:
public class RunntimePermissionHelper {

    public static boolean checkAndRequestForRunntimePermission(Activity activity,String []  permissionsNeeded) {
        // 1 检查权限
        ArrayList<String> permissionsNeedRequest = new ArrayList<String>();
        for (String permission : permissionsNeeded) {
            if (ContextCompat.checkSelfPermission(activity, permission)
                    == PackageManager.PERMISSION_GRANTED) {
                continue;
            }
            permissionsNeedRequest.add(permission);
        }
        // 2 请求权限
        if (permissionsNeedRequest.size() == 0) {
            return true;
        } else {
            String[] permissions = new String[permissionsNeedRequest.size()];
            permissions = permissionsNeedRequest.toArray(permissions);
            ActivityCompat.requestPermissions(activity, permissions, 0);
            return false;
        }
    }
}
  • 修改ActivityA:
public class ActivityA extends Activity implements OnClickListener {
    private static final String TAG = "azhengye/ActivityA";
    private EditText mEditText = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_a);
        findViewById(R.id.start_b).setOnClickListener(this);
        mEditText = (EditText) findViewById(R.id.edit_tx);
        boolean granted = checkAndRequestPermission();
        if (granted) {
           readFileSize();
        }
    }

    private void readFileSize() {
        InputStream file = null;
        try {
            file = this.getContentResolver().openInputStream(
                    Uri.parse("content://mms/part/2"));
            Log.d(TAG, "file.available()==" + file.available());
            mEditText.setText("file size is:" + file.available());
            file.close();
        } catch (FileNotFoundException e) {
            Log.d(TAG, "read mms FileNotFoundException" + e.getMessage());
            e.printStackTrace();
        } catch (IOException e) {
            Log.d(TAG, "read mms IOException" + e.getMessage());
        }
    }

    private boolean checkAndRequestPermission() {
        return RunntimePermissionHelper.checkAndRequestForRunntimePermission(
                this, new String[] { Manifest.permission.READ_SMS });
    }

    @Override
    public void onRequestPermissionsResult(int requestCode,
            String[] permissions, int[] grantResults) {
        if (Manifest.permission.READ_SMS.equals(permissions[0])
                && grantResults[0] == PackageManager.PERMISSION_DENIED) {
            finish();
        }else {
            readFileSize();
        }
    }

    @Override
    public void onClick(View v) {
        Intent intent = new Intent(this, ActivityB.class);
        startActivity(intent);
    }
}

看下修改后的效果:
技术分享

到这一步修复bug的工作算是完成了一半,为什么只完成了一半呢?因为特殊case我们还没有处理,比如用户在权限申请界面偏偏勾选上“Never ask again”,并且deny赋权操作。就好比下面这样:
技术分享
用户拒绝授权并勾选”不在提示选项”之后,在点击应用直接退出,完全没有任何提示,这种case是在”作死”,就等着被用户卸载吧。
为了不”作死”,需要 shouldShowRequestPermissionRationale方法出场了。用户之前拒绝权限的时候勾选了对话框中”Never ask again”,那么这个方法会返回false,否则为true,利用这个方法就可以针对该case给用户一个提示了。
继续完善代码,在RunntimePermissionHelper代码种新增方法:

public static boolean showDeniedPromptIfNeeded(Activity activity,
            String permission) {
        if (!ActivityCompat.shouldShowRequestPermissionRationale(activity,
                permission)) {
            Toast.makeText(
                    activity.getApplicationContext(),
                    "Read SMS Permission denied. You should open it in Settings-->Apps-->"
                            + activity.getPackageName(), Toast.LENGTH_SHORT)
                    .show();
            return true;
        }
        return false;
    }

修改ActivityA种的 onRequestPermissionsResult回调

@Override
public void onRequestPermissionsResult(int requestCode,
            String[] permissions, int[] grantResults) {
        if (Manifest.permission.READ_SMS.equals(permissions[0])
                && grantResults[0] == PackageManager.PERMISSION_DENIED) {
            RunntimePermissionHelper.showDeniedPromptIfNeeded(this, permissions[0]);
            finish();
        }else {
            readFileSize();
        }
    }

最终的效果如下:
技术分享

在测试另一种case,用户先通过赋权操作,在应用运行期间,切到Settings的权限管理界面,将权限关闭,这种情况会如何呢?看下效果
技术分享
可以看到这种情况下系统会自动弹出权限申请提示框。
到此按照这种添加运行时权限思路bug所描述的问题算是解决了。

终极解法

虽然按照上述方案,完美的解决了bug,代码的改动量也不多,初看之下算是完美。
但其实在bug分析阶段就存在一个疑问。

既然是SMS引起的权限问题,Gallery里AndroidManifest最开始压根就没有申请过SMS权限。为什么用gallery打开彩信里的图片,可以正常浏览呢?

难道是SMS和Gallery都是系统预制的应用所以它们可以直接访问?
带着这个疑问将Gallery当成普通应用安装后发现仍然可以正常打开彩信图片。看来不是签名和安装路径的问题。
SMS应用肯定是利用contentprovider丢一个彩信附件图片的uri给外部应用,外部应用去访问这个uri就能拿到数据,看来问题出在这个uri上。
查看文档终于找到了一丝线索:grantUriPermission方法,它能针对单条uri开放权限。
bug描述的访问过程入下图:
技术分享
点击彩信的附件,会采用隐式方式启动能处理的应用,这个时候应该是加入了如下类似的代码(没有SMS代码- -!):

intent.setDataAndType(imageUri, mimeType).setFlags(
                Intent.FLAG_GRANT_READ_URI_PERMISSION);

于是其他应用的Activity就对该条imageUri有了读的权限,这里就是ActivityA,于是Gallery在显示图片的时候没有问题。然而在ActivityA—>ActivityB的时候,只是将imageUri继续传递了下去,已经开放的读权限没有跟着uri继续传递。这样就能解释提出的疑问了。
于是终极的修改方法是在ActivityA启动ActivityB时,加入

grantUriPermission(getPackageName(), imageUri,
                Intent.FLAG_GRANT_READ_URI_PERMISSION);

这种改法避免为了解决一个bug多申请SMS的敏感权限,权限申请的越多用户对应用的怀疑之心也就越重。因此能少申请的权限尽量少申请。

一点疑惑

在debug运行是权限时,发现一个现象,如果在应用运行期间,用户去Settings里关闭应用权限,在后台用命令

adb shell ps

可以看到应用被杀死了,下图是我在调试过程中的后台截图。
技术分享
当在运行时关闭权限后,

adb shell ps |grep com.azhengye.demopermission

没有输出,说明此时进程已经被kill了。接着重新启动应用,
应用的PID由6142变成了6192。这背后发生了什么呢?这个问题没有跟踪下去,留在后续分析。

<script type="text/javascript"> $(function () { $(‘pre.prettyprint code‘).each(function () { var lines = $(this).text().split(‘\n‘).length; var $numbering = $(‘
    ‘).addClass(‘pre-numbering‘).hide(); $(this).addClass(‘has-numbering‘).parent().append($numbering); for (i = 1; i <= lines; i++) { $numbering.append($(‘
  • ‘).text(i)); }; $numbering.fadeIn(1700); }); }); </script>

    Android Permission介绍