Android多用户下数据隔离方案与常见问题解决思路

Android多用户下数据隔离方案与常见问题解决思路##

1.Android 多用户概述###

Android从4.2开始支持多用户模式,不同的用户运行在不同的用户空间,相关的系统设置是各不相同而且不同用户安装的应用和应用数据也是不一样的,但是系统中和硬件相关的设计则是共用的。
Android的多用户实现主要通过UserManagerService(UMS)对多用户进行创建、删除等操作。

相关的引入类是

framework/base/services/core/java/com/android/server/pm/UserManagerService.java
framework/base/core/java/android/os/UserManager.java
framework/base/core/java/android/content/pm/UserInfo.java
framework/base/core/java/android/os/UserHandle.java

我们知道,Linux有原生的用户管理系统。作为一个多用户多任务的分时操作系统,它通过uid和gid来进行用户管理。
但是Android将Linux的多用户管理,移植到了apk管理上。Android给每个APK进程分配一个单独的空间,manifest中的userid就是对应一个分配的Linux用户ID,并且为它创建一个沙箱,以防止影响其他应用程序(或者其他应用程序影响它)。

android用户空间

用户ID 在应用程序安装到设备中时被分配,并且在这个设备中保持它的永久性。也就是说,原生的Linux用户id,被用在了apk做自己的标识。所以,需要有管理当前所有app的用户的概念,Android引入了UserHandle

2.UserHandle,userId, uid, gid###

Android对于不同UserHandler,认为数据是相互隔离的。有不同的用户数据,权限等。每个UserHandle有它的userId,主用户是0。默认新创建一个用户,userId加1。

在 Android 上,一个用户 UID 标示一个应用程序。应用程序在安装时被分配用户 UID,应用程序在设备上的存续期间内,用户 UID 保持不变。对于普通的应用程序,GID即等于UID。

GIDS 是由框架在 Application 安装过程中生成,与 Application 申请的具体权限相关。 如果 Application 申请的相应的 permission 被 granted ,而且有对应的GIDS, 那么 这个Application 的 gids 中将 包含这个 gids。记住权限(GIDS)是关于允许或限制应用程序(而不是用户)访问设备资源。

转换关系为:

uid = userId * 100000 + appId

3.多用户下数据隔离架构###

不同用户下,应用的数据需要隔离。所以Android通过不同的数据路径来达成数据隔离的效果。
普通应用数据的保存路径为/data/user/userId/packageName
以日历的数据库的数据为例,假设手机有userId 0userId 11的数据,那么它不同用户下的存储路径为:

user11:/data/user/11/com.android.providers.calendar/databases
user 0:/data/user/0/com.android.providers.calendar/databases

常规的数据差异问题,可以导出对应路径下的数据进行分析。

4.Settings多用户下数据隔离架构与常见问题###

作为Android设置项的数据库,android.provider.Settings是被其他应用使用得最多,问题也出现最多的数据库。
android.provider.Settings的操作类有三个,System,Secure,Global。他们通过key-value的方式进行保存,user0对应的存储实体文件为:

/data/system/users/0/settings_system.xml
/data/system/users/0/settings_secure.xml
/data/system/users/0/settings_global.xml

user11对应的存储实体文件为:

/data/system/users/11/settings_system.xml
/data/system/users/11/settings_secure.xml

4.1全局变量多用户隔离问题####

Global作为全局的变量表,多个用户只存在一个文件。也就意味着,一个用户修改会影响到其他用户。如果要考虑多用户下隔离global的开关项,而不能影响原有的业务逻辑。只能是考虑在System,或是Secure下新增key保存当前用户下Global的开关状态。然后在用户切换的时候,用System/Secure的状态更新Global的状态。

用户切换时系统有原生的广播发出来,需要注意的是,这个广播只能是动态注册的receiver能够接收。所以需要考虑数据场景。像设置就不适宜做用户切换时修改数据的操作。只能由其他app进行操作。

    /**
     * Broadcast sent to the system when the user switches. Carries an extra EXTRA_USER_HANDLE that has
     * the userHandle of the user to become the current one. This is only sent to
     * registered receivers, not manifest receivers.  It is sent to all running users.
     * You must hold
     * {@link android.Manifest.permission#MANAGE_USERS} to receive this broadcast.
     * @hide
     */
    public static final String ACTION_USER_SWITCHED =
            "android.intent.action.USER_SWITCHED";

4.2当前用户数据异常问题####

从其他app反馈给设置这边最多的,就是读取到的值不对,需要设置进行分析。他们使用方式为getInt(cr,name,def);

/**
         * Convenience function for retrieving a single secure settings value
         * as an integer.  Note that internally setting values are always
         * stored as strings; this function converts the string to an integer
         * for you.  The default value will be returned if the setting is
         * not defined or not an integer.
         *
         * @param cr The ContentResolver to access.
         * @param name The name of the setting to retrieve.
         * @param def Value to return if the setting is not defined.
         *
         * @return The setting's current value, or 'def' if it is not defined
         * or not a valid integer.
         */
        public static int getInt(ContentResolver cr, String name, int def) {
            return getIntForUser(cr, name, def, cr.getUserId());
        }

        /** @hide */
        public static int getIntForUser(ContentResolver cr, String name, int def, int userHandle) {
            if (LOCATION_MODE.equals(name)) {
                // Map from to underlying location provider storage API to location mode
                return getLocationModeForUser(cr, userHandle);
            }
            String v = getStringForUser(cr, name, userHandle);
            try {
                return v != null ? Integer.parseInt(v) : def;
            } catch (NumberFormatException e) {
                return def;
            }
        }

从上面代码可以看到,这个方法会跳转到getIntForUser,此时的userHandler是通过ContentResolver的getUserId获取到的。但是常驻进程在用户切换的时候,上下文是否正常切换?如systemUI,就将处理逻辑都在user0创建的进程里处理。用户切换时,并没有切换到其他用户创建的进程,context也就不会切换。那么在其他用户(如当前处于userId 11)时,状态栏一直获取到user0的值,自然导致异常。那么对应的解决方案,可以使用

    getIntForUser(cr, name, def, ActivityManager.getCurrentUser());

ActivityManager.getCurrentUser()通过IPC获取当前前台活跃的userId。这样的话可以获取到前台用户的值,而不是一直获取user 0的。

4.3Android O上非主用户监听Global失效问题####

在android O上,其他用户监听Global里面值的修改,发现ContentObserver的onChange()不会被回调。

SettingsProvider.call()
->insertGlobalSetting(args...)
->mutateGlobalSetting(args...)

在做global值修改的时候,可以看到insertSettingLocked第二个参数,userId的信息被USER_SYSTEM(也就是user 0)擦除了。等到修改结束notifyForSettingsChange的时候,无论是哪个用户修改了global,onChange都被传递给了user 0。

    mSettingsRegistry.insertSettingLocked(SETTINGS_TYPE_GLOBAL,
                            UserHandle.USER_SYSTEM, name, value, tag, makeDefault,
                            callingPkg, forceNotify, CRITICAL_GLOBAL_SETTINGS); 

针对这种情况,需要将修改同步给当前用户。

            //#ifdef VENDOR_EDIT
            //[email protected], 2019/03/07, Add for Ali Dual System ,to solve Global's ContentObserver not transfer in work mode  . . .
            if (isGlobalSettingsKey(key)) {
                final long token = Binder.clearCallingIdentity();
                try {
                    notifyGlobalSettingChangeForRunningUsers(key, name);
                } finally {
                    Binder.restoreCallingIdentity(token);
                }
            } 
            //#endif /* VENDOR_EDIT */

        //#ifdef VENDOR_EDIT
        //[email protected], 2019/03/07, Add for Ali Dual System ,to solve Global's ContentObserver not transfer in work mode  . . .
        private void notifyGlobalSettingChangeForRunningUsers(int key, String name) {
            // Important: No need to update generation for each user as there
            // is a singleton generation entry for the global settings which
            // is already incremented be the caller.
            final Uri uri = getNotificationUriFor(key, name);
            final List users = mUserManager.getUsers(/*excludeDying*/ true);
            for (int i = 0; i < users.size(); i++) {
                final int userId = users.get(i).id;
                if (mUserManager.isUserRunning(UserHandle.of(userId))) {
                    mHandler.obtainMessage(MyHandler.MSG_NOTIFY_URI_CHANGED,
                            userId, 0, uri).sendToTarget();
                }
            }
        }
        //#endif /* VENDOR_EDIT */

5.多用户下界面启动异常问题###

在多用户场景下,有出现主用户界面正常切换,但是在其他用户下缺拉不起对应Activity的问题。此类问题分两种:

  1. 拉起一个常驻进程的界面。如尝试打开com.android.phone的一个界面。因为phone进程并不会在其他用户创建的时候创建一个新的进程。所以在其他用户活跃的时候,隶属于user0的phone进程的界面无法显示在上层。相应的解决方案为:
    (1).在Activity里加上android:showForAllUsers="true";
    (2).用startActivityAsUser的方式打开界面。(注意system app和权限)
    /**
     * Version of {@link #startActivity(Intent)} that allows you to specify the
     * user the activity will be started for.  This is not available to applications
     * that are not pre-installed on the system image.
     * @param intent The description of the activity to start.
     * @param user The UserHandle of the user to start this activity for.
     * @throws ActivityNotFoundException  
     * @hide
     */
    @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL)
    public void startActivityAsUser(@RequiresPermission Intent intent, UserHandle user) {
        throw new RuntimeException("Not implemented. Must override in a subclass.");
    }

需要注意的是,此时用user0打开界面,界面里的数据都是user0的。在其他用户界面展示user0的界面,需要和产品确认是否满足要求。

  1. 其他用户拉起界面失败。主用户下无异常,其他用户异常的话,需要确认是否是拉起正确userId的界面。如上面有描述到的,systemUI并没有切换到对应的用户,那么它去打开界面是,AMS会尝试用user0去打开界面。前台活跃的是其他user,所以打开失败。如关键字ActivityManager: START u0 {flg=0x10808000...
    此时可以考虑用startActivityAsUser。
    startActivityAsUser需要传入UserHandle对象。可以通过
    /** @hide */
    @SystemApi
    public static UserHandle of(@UserIdInt int userId) {
        return userId == USER_SYSTEM ? SYSTEM : new UserHandle(userId);
    }

获取到。

6.Context切换问题###

如果有一些场景,需要用到其他userId的context,该怎么办呢?
有一个方法,可以获取到对应userId的Context。

    /**
     * Similar to {@link #createPackageContext(String, int)}, but with a
     * different {@link UserHandle}. For example, {@link #getContentResolver()}
     * will open any {@link Uri} as the given user.
     *
     * @hide
     */
    @SystemApi
    public Context createPackageContextAsUser(
            String packageName, @CreatePackageOptions int flags, UserHandle user)
            throws PackageManager.NameNotFoundException {
        if (Build.IS_ENG) {
            throw new IllegalStateException("createPackageContextAsUser not overridden!");
        }
        return this;
    }

这个Context没有Theme,但是应对一些数据场景,如Service,Broadcast,ContentResolver这种场景,再试用不过了。

6.多用户数据问题的常见分析解决思路###

1.确定是否是数据不一致导致问题。从实际数据的存储位置和代码里获取的数据进行对比,确认数据是否存在不一致。
2.判断上下文的userId是否发生变化。对应的方法有Context.getUserId,ContentResolver.getUserId,ActivityManagetr.getUserId。活跃的userId以ActivityManagetr.getUserId结果为准。界面切换则搜索关键字ActivityManager: START u...
3.确认是上下文userId导致的问题,根据场景选择合适的...AsUser方法。

参考文档:
1.多用户管理UserManager
2.Android逆向之旅---Android中的sharedUserId属性详解
3.Android 安全机制(1)uid 、 gid 与 pid

你可能感兴趣的:(Android多用户下数据隔离方案与常见问题解决思路)