Android权限机制简述及动态权限管理的一种解决方案

简述

Android是一个权限分离的操作系统,每一个应用程序运行时都会有一个明确地系统身份标识(Linux的user ID和group ID)。部分系统也同样被特定身份标识而隔开。因此,Linux才能将应用程序与其他程序和系统隔离开来。

这样的机制可以说是相当安全,但是也阻断了各个应用程序之间或者和系统之间的“交流”。因此,Android通过一种“permission”机制强力限制某些特定地操作来达到细粒度的安全能力。

进程沙箱

Android进程沙箱机制是借鉴Linux中用户组的原理,其限制了不同应用程序之间的资源和数据的互访。当应用首次安装的时,系统会向其分配一个UID。如果该应用程序是第三方的,那么其UID值大于10000,如果是系统应用程序则小于10000。如果应用程序卸载后又重新安装,那么其UID值是会改变的。

    //获取应用程序UID方法
    public void getApplicationUid() {
        PackageManager pm = getPackageManager();
        try {
            ApplicationInfo ai = pm.getApplicationInfo(getPackageName(), PackageManager.GET_ACTIVITIES);
            Log.d(getClass().getSimpleName(), "uid = " + ai.uid);

        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
    }

不同UID的应用程序是不能进行资源互访,从而有效达到进程隔离目的。


Android应用程序的沙箱机制

此外,你可以每个应用程序的AndroidManifest.xml文件中使用ShareUserID属性来使他们拥有同一UserID。UserID相同的应用程序将会被系统当做同一应用程序,拥有相同的UserID和文件权限。

注意:为了保留系统安全性,只有签名相同(并且需要相同的shareUserId)的应用程序才会被分配相同的UserID。

一个应用程序存储的任何数据都会被分配应用程序的UserID,通常是不能被其他应用程序所访问。当使用getSharedPreferences(String, int),openFileOutput(String, int),或者openOrCreateDatabase(String, int, SQLiteDatabase.CursorFactory),你可以使用MODE_WORLD_READABLE或者MODE_WORLD_WRITEABLE标记来允许其他应用程序读或写文件。

权限使用

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.android.app.myapp" >
    <uses-permission android:name="android.permission.RECEIVE_SMS" />
    ...
</manifest>

权限的使用是相当简单的,某功能需要申请权限时,只需在AndroidManifest.xml文件中申明对应权限就行。如上述代码。

如果你的App在其manifest文件中声明一系列normal permissions(不会对用户隐私或者设备运行构成威胁的权限),系统会自动准许这些权限申请。如果你的App在其manifest文件中声明一系列dangerous permissions(对用户隐私或者设备运行构成潜在威胁的权限),系统将会询问用户是否同意这些权限申请。
询问的方式根据系统的版本而有所不同。
1.静态权限申请询问界面
若设备运行的系统版本为Android5.1(API版本22)或更低,或者App的targetSdkVersion是22或更低,Android提供的是静态权限申请询问界面。
这种询问方式只要玩过Android手机的应该都见过,当应用程序首次安装时,会弹出以下类似界面,出现在图标列表中的权限都是dangerous permissions。


应用程序首次安装权限询问界面

这种询问方式相当霸道,如果想要安装该应用,我们只有同意其申请的所有权限。当应用程序安装更新时,如果该应用程序有新申请的权限,那么该权限询问界面会将新申请的权限列出。你废除这些权限申请的唯一方式就是卸载它们!

  1. 动态权限申请询问界面
    如果设备运行的系统版本为Android6.0(API版本23)或更高,或者App的targetSdkVersion是23或更高,Android提供了动态权限申请询问界面。
    其实这种方式,早在Android6.0之前就有大批国产ROM提供动态权限管理方式,市面上主流的安全软件也提供这种功能。Google终于在Android6.0提供了动态权限管理功能(不过对我大天朝来说然并卵)。


    动态权限申请询问界面

    这种交互方式更加的人性化,也更加安全。在应用程序运行的过程中,如果需要申请网络连接权限,那么系统会弹出权限询问对话框供用户选择。

当然,权限并不仅仅局限于此。我们也可以自定义某些权限来保证安全性。比如,启动Activity或者Service时,增加权限控制,防止被外部应用程序胡乱启动。

权限组

对普通第三方应用程序来说,权限一般分为normal permission和dangerous permission。Android系统所有的dangerous permissions都属于某一权限组。如果设备运行的系统版本为Android6.0(API版本23)或更高,或者App的targetSdkVersion是23或更高,当你的应用程序需要一个dangerous permission时,那么:

  1. 如果应用程序在manifest中声明了一个dangerous permission,并且它目前没有该权限组中的任一权限,那么系统会弹出一个将要申请权限组的对话框。但是该对话框不会具体描述是该权限组中的哪一个权限。比如应用程序需要READ_CONTACTS权限,那么该对话框仅仅只描述为该应用程序需要访问联系人。
    2.如果应用程序在manifest中声明了一个dangerous permission,并且它已经拥有该权限组的其他权限,那么系统将直接允许其访问该权限,不与用户产生交互。

若设备运行的系统版本为Android5.1(API版本22)或更低,或者App的targetSdkVersion是22或更低,系统将会在应用程序安装的时候让用户同意权限申请。系统仅仅只告诉用户哪些权限组被申请,而不是单独某一个权限。

Android6.0动态权限管理

国产ROM和各类安全软件早已提供了动态权限管理功能,实现方式上大同小异,虽然对用户来说这是相当利好的消息,但是对我们开发者来说,还是很麻烦的,各种ROM的兼容性让我们很头疼。终于在棉花糖上,Android提供了动态权限管理的相关API,我们在处理权限问题上方便了很多。
当你的需要申请一个dangerous permission时候,你必须在每次申请之前进行权限检查。权限检查的方法如下。

int permissionCheck = ContextCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE);

如果该方法的返回值为PackageManager.PERMISSION_GRANTED,应用程序就可以继续后续操作。如果应用程序没有该权限,那么方法的返回值为PERMISSION_DENIED,并且将会询问用户是否允许该权限。
我们在Manifest中申请的任何dangerous permission,都会询问用户是否允许该权限,Android提供了几个申请权限的方法,调用之后,会弹出一个标准的系统对话框供用户选择,该对话框是不能自定义的。

如果一个图像类软件申请发短信权限,用户可能会产生怀疑,是不是扣费短信。那么我们如何降低用户的猜疑呢?Android提供了一个比较实用的方法shouldShowRequestPermissionRationale(),该方法给了我们一个解释的机会来增加权限申请通过的概率。如果该权限之前已被申请过但是被用户拒绝,那么shouldShowRequestPermissionRationale()方法返回true。

如果你的应用程序没有所需要的权限,那么你必须要通过调用requestPermissions()方法来申请权限,该方法调用后,系统会立刻弹出权限申请询问对话框供供用户选择,在用户交互后,系统会立刻通过onRequestPermissionsResult()将结果返回给应用程序。这里直接将官方文档中相关演示代码贴出来供参考。

// Here, thisActivity is the current activity
if (ContextCompat.checkSelfPermission(thisActivity,
                Manifest.permission.READ_CONTACTS)
        != PackageManager.PERMISSION_GRANTED) {

    // Should we show an explanation?
    if (ActivityCompat.shouldShowRequestPermissionRationale(thisActivity,
            Manifest.permission.READ_CONTACTS)) {

        // Show an expanation to the user *asynchronously* -- don't block
        // this thread waiting for the user's response! After the user
        // sees the explanation, try again to request the permission.

    } else {

        // No explanation needed, we can request the permission.

        ActivityCompat.requestPermissions(thisActivity,
                new String[]{Manifest.permission.READ_CONTACTS},
                MY_PERMISSIONS_REQUEST_READ_CONTACTS);

        // MY_PERMISSIONS_REQUEST_READ_CONTACTS is an
        // app-defined int constant. The callback method gets the
        // result of the request.
    }
}
@Override
public void onRequestPermissionsResult(int requestCode,
        String permissions[], int[] grantResults) {
    switch (requestCode) {
        case MY_PERMISSIONS_REQUEST_READ_CONTACTS: {
            // If request is cancelled, the result arrays are empty.
            if (grantResults.length > 0
                && grantResults[0] == PackageManager.PERMISSION_GRANTED) {

                // permission was granted, yay! Do the
                // contacts-related task you need to do.

            } else {

                // permission denied, boo! Disable the
                // functionality that depends on this permission.
            }
            return;
        }

        // other 'case' lines to check for other
        // permissions this app might request
    }
}

再一次,系统权限对话框仅仅只描述你所申请权限所在权限组的描述,而不是针对某一特定权限。对于同一权限组的权限,用户只需同意一次即可。这种方案的好坏见仁见智,但是有时候感觉会把一个很小的问题给扩大了,比如我们只是需要简单的获取设备的IMEI码,那么这时候系统对话框的描述为应用程序将访问设备信息。这时候用户肯定会想,你访问我设备信息作甚!然后你的申请被无情拒绝了!

注意:你的应用程序需要明确地申请每一个你需要的权限,即使用户已经同意了该权限所在权限组的另外一个权限。此外,随着Android版本的更新,权限组中所含的权限可能会改变。因此,不要偷懒,该显示申请权限的地方还是要乖乖申请吧。

合理申请权限

曾几何时,权限的滥用导致用户隐私泄露频发,而今,用户对隐私也愈发敏感,过渡的权限申请会给用户造成不良的印象。因此,作为有节操的程序员,我们在权限申请上应该慎重,而不是一股脑把所有权限都给申请。
随着Android版本的更新,相应的权限也会更新,因此我们一定要注意不同targetSdkVersion属性所带来的权限变化,并尽可能的提高targetSdkVersion。在权限使用上Google也给了我们一些建议。

  1. 考虑使用Intent来完成权限相关的操作
    这点建议,我觉得可以作为一个比较好的参考。在Manifest中,我们申请了SEND_SMS权限,那么可以通过下面代码完成发送短信功能。
SmsManager sm = SmsManager.getDefault();
sm.sendTextMessage(address, null, message, null, null);

如果发送短信时候,用户选择拒绝该权限申请,那么你的功能也就Over了。
如果我们换intent方式进行发送短信,则不会出现权限被拒绝的情况,代码如下。

    Intent intent = new Intent(Intent.ACTION_SENDTO);  
    intent.setData(Uri.parse("smsto:" + number));  
    intent.putExtra("sms_body", body);  
    context.startActivity(sendIntent); 

该方法会跳转到发送短信界面(如果系统装有多个短信类应用,那么系统会弹出一个选择应用对话框,让用户选择使用何种应用来完成发送短信功能),并填充好相应的内容。类似的,拨打电话和使用照相机等都可以使用intent来完成相应的功能,降低了用户拒绝权限的风险。
最后,权限方式和intent方式各有千秋,根据不同的业务情景,我们可以选择不同的方式。

  1. 只申请你所需要的权限
    不想让用户觉得你的应用程序是一个“流氓应用”,最好不要过度申请权限。
    3.不要“吞噬”用户
    在Android6.0中,不要在同一时刻申请多种权限。因为系统可能会弹出多个系统权限询问对话框,这种情况:
    第一,用户可能觉得很烦锁,并退出你的应用程序。
    第二,用户可能由于误操作,拒绝了你的某些权限申请。
    因此,最好的方式还是在你需要的时候进行申请吧。
    4.给出你为什么使用权限的原因
    为了降低权限申请被拒绝的风险,最好在调用requestPermissions()之前,进行权限申请的说明,使用户觉得你不是在做“坏事”。

动态权限申请的一种解决方案

虽然目前Android6.0市场占有率相当低,但是随着时间的推移,关于动态权限管理这一块,我们迟早要接触的。这里我参考Android官方开发文档,封装了动态权限管理所需的方法。虽然自己的项目中还未用到动态权限管理,但作为工作之余的学习还是大有裨益!

权限申请流程

权限申请流程图

BaseActivity中完成权限申请

这里我没有将权限申请相关方法封装成一个类,而是在BaseActivity中添加相关方法。

public class BaseActivity extends AppCompatActivity {

    //申请请求的request code
    private final static int YZT_PERMISSION_REQUEST = 12;

    public final String TAG = getClass().getSimpleName();

    //是否跳转过应用程序信息详情页
    private boolean mIsJump2Settings = false;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }

    @Override
    protected void onResume() {
        super.onResume();
        if (mIsJump2Settings) {
            onRecheckPermission();
            mIsJump2Settings = false;
        }
    }
    //单个权限的检查
    public void checkPermission(@NonNull final String permission, @Nullable String reason) {
        if (Build.VERSION.SDK_INT < 23) return;
        int permissionCheck = ContextCompat.checkSelfPermission(this, permission);
        if (permissionCheck == PackageManager.PERMISSION_GRANTED) {
            //权限已经申请
            onPermissionGranted(permission);

        } else {
            if (!TextUtils.isEmpty(reason)) {
                //判断用户先前是否拒绝过该权限申请,如果为true,我们可以向用户解释为什么使用该权限
                if (ActivityCompat.shouldShowRequestPermissionRationale(this, permission)) {
                    //这里的dialog可以自定义
                    new AlertDialog.Builder(this).setCancelable(false).setTitle("温馨提示").setMessage(reason).
                            setNegativeButton("我知道了", new DialogInterface.OnClickListener() {
                                @Override
                                public void onClick(DialogInterface dialog, int which) {
                                    requestPermission(new String[]{permission});
                                    dialog.dismiss();
                                }
                            }).show();
                } else {
                    requestPermission(new String[]{permission});
                }
            } else {
                requestPermission(new String[]{permission});
            }

        }
    }
    //多个权限的检查
    public void checkPermissions(@NonNull String... permissions) {
        if (Build.VERSION.SDK_INT < 23) return;
        //用于记录权限申请被拒绝的权限集合
        List<String> permissionDeniedList = new ArrayList<>();
        for (String permission : permissions) {
            int permissionCheck = ContextCompat.checkSelfPermission(this, permission);
            if (permissionCheck == PackageManager.PERMISSION_GRANTED) {
                onPermissionGranted(permission);
            } else {
                permissionDeniedList.add(permission);
            }
        }
        if (!permissionDeniedList.isEmpty()) {
            String[] deniedPermissions = permissionDeniedList.toArray(new String[permissionDeniedList.size()]);
            requestPermission(deniedPermissions);
        }
    }

    //调用系统API完成权限申请
    private void requestPermission(String[] permissions) {
        ActivityCompat.requestPermissions(this, permissions, YZT_PERMISSION_REQUEST);
    }

    //申请权限被允许的回调
    public void onPermissionGranted(String permission) {

    }
    //申请权限被拒绝的回调
    public void onPermissionDenied(String permission) {

    }
    //申请权限的失败的回调
    public void onPermissionFailure() {

    }

    //如果从设置界面返回,则重新申请权限
    public void onRecheckPermission() {

    }

    //弹出系统权限询问对话框,用户交互后的结果回调
    @Override
    public final void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        switch (requestCode) {
            case YZT_PERMISSION_REQUEST:
                if (grantResults.length > 0) {
                    //用于记录是否有权限申请被拒绝的标记
                    boolean isDenied = false;
                    for (int i = 0; i < grantResults.length; i++) {
                        if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
                            onPermissionGranted(permissions[i]);
                        } else {
                            isDenied = true;
                            onPermissionDenied(permissions[i]);
                        }
                    }
                    if (isDenied) {
                        isDenied = false;
                        //如果有权限申请被拒绝,则弹出对话框提示用户去修改权限设置。
                        showPermissionSettingsDialog();
                    }

                } else {
                    onPermissionFailure();
                }
                break;
        }
    }

    private void showPermissionSettingsDialog() {
        new AlertDialog.Builder(this).setCancelable(false).setTitle("温馨提示").
                setMessage("缺少必要权限\n不然将导致部分功能无法正常使用").setNegativeButton("下次吧", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {

            }
        }).setPositiveButton("去设置", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                jump2PermissionSettings();
            }
        }).show();
    }

    /**
     * 跳转到应用程序信息详情页面
     */
    private void jump2PermissionSettings() {
        mIsJump2Settings = true;
        Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
        intent.setData(Uri.parse("package:" + getPackageName()));
        startActivity(intent);
    }
}

使用方法

public class MainActivity extends BaseActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //权限检查
        String[] permissionArray = {Manifest.permission.SEND_SMS, Manifest.permission.CALL_PHONE};
        checkPermissions(permissionArray);
//        checkPermission(Manifest.permission.SEND_SMS, "YZT将要发生短信进行身份验证");

    }

    @Override
    public void onRecheckPermission() {
        super.onRecheckPermission();
        String[] permissionArray = {Manifest.permission.SEND_SMS, Manifest.permission.CALL_PHONE};
        checkPermissions(permissionArray);
    }

    @Override
    public void onPermissionGranted(String permission) {
        super.onPermissionGranted(permission);
        switch (permission) {
            case Manifest.permission.SEND_SMS:
                //TODO:发送短信
                Toast.makeText(this, "发短信咯", Toast.LENGTH_LONG).show();
                break;
            case Manifest.permission.CALL_PHONE:
                //TODO:打电话
                Toast.makeText(this, "电话咯", Toast.LENGTH_LONG).show();
                break;
        }
    }

    @Override
    public void onPermissionDenied(String permission) {
        super.onPermissionDenied(permission);
        switch (permission) {
            case Manifest.permission.SEND_SMS:
                //TODO:
                break;
            case Manifest.permission.CALL_PHONE:
                //TODO:
                break;
        }
    }

    @Override
    public void onPermissionFailure() {
        super.onPermissionFailure();
        Toast.makeText(this, "权限获取失败", Toast.LENGTH_LONG).show();
    }

随着用户安全意识的提升,我们在权限的使用上也应该更加趋于合理和谨慎。虽然目前Android6.0的占有率很低,但是我们也应该未雨绸缪,尽快引入动态权限管理机制。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,511评论 1 330
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,495评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,595评论 0 225
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,558评论 0 190
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,715评论 3 270
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,672评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,112评论 2 291
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,837评论 0 181
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,417评论 0 228
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,928评论 2 232
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,316评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,773评论 2 234
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,253评论 3 220
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,827评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,440评论 0 180
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,523评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,583评论 2 249

推荐阅读更多精彩内容