Provider 权限详解

转载请标明出处:

https://blog.csdn.net/shift_wwx/article/details/82108549

 

前言:

之前有篇博文 《Android基础总结之八:ContentProvider》大概说明provider 的基础知识。对于AndroidManifest.xml中provider 的解析可以看下博文《android PMS 如何解析 APK》,本文主要对provider 权限进行详细解析。如果provider 会被其他程序使用时,需要将export 属性设为true,还需要进行readPermission 和writePermission 设置。当然,对于SDK 22以后出现的FileProvider 又是另一种情况,本文会结合source code 详细分析Provider 在使用中权限管理。

本文代码基于版本Android O。

 

实例:

之前出现一个exception 的log,本文结合这个实例进行解析。

--------- beginning of crash
08-24 18:14:43.989 E/AndroidRuntime( 2366): FATAL EXCEPTION: main
08-24 18:14:43.989 E/AndroidRuntime( 2366): Process: com.android.messaging, PID: 2366
08-24 18:14:43.989 E/AndroidRuntime( 2366): java.lang.SecurityException: UID 10098 does not have permission to content://com.android.dialer.files/my_cache/my_cache/%2B12543365555_08-11-18_0442AM.amr [user 0]
08-24 18:14:43.989 E/AndroidRuntime( 2366): 	at android.os.Parcel.readException(Parcel.java:2005)
08-24 18:14:43.989 E/AndroidRuntime( 2366): 	at android.os.Parcel.readException(Parcel.java:1951)
08-24 18:14:43.989 E/AndroidRuntime( 2366): 	at android.app.IActivityManager$Stub$Proxy.startActivity(IActivityManager.java:4352)
08-24 18:14:43.989 E/AndroidRuntime( 2366): 	at android.app.Instrumentation.execStartActivity(Instrumentation.java:1613)
08-24 18:14:43.989 E/AndroidRuntime( 2366): 	at android.app.Activity.startActivityForResult(Activity.java:4501)
08-24 18:14:43.989 E/AndroidRuntime( 2366): 	at android.app.Activity.startActivityForResult(Activity.java:4459)
08-24 18:14:43.989 E/AndroidRuntime( 2366): 	at android.app.Activity.startActivity(Activity.java:4820)
08-24 18:14:43.989 E/AndroidRuntime( 2366): 	at android.app.Activity.startActivity(Activity.java:4788)
08-24 18:14:43.989 E/AndroidRuntime( 2366): 	at com.android.messaging.ui.Q.wT(SourceFile:200)
08-24 18:14:43.989 E/AndroidRuntime( 2366): 	at com.android.messaging.ui.conversationlist.ShareIntentActivity.pR(SourceFile:179)
08-24 18:14:43.989 E/AndroidRuntime( 2366): 	at com.android.messaging.ui.conversationlist.o.onClick(SourceFile:98)
08-24 18:14:43.989 E/AndroidRuntime( 2366): 	at com.android.internal.app.AlertController$ButtonHandler.handleMessage(AlertController.java:166)
08-24 18:14:43.989 E/AndroidRuntime( 2366): 	at android.os.Handler.dispatchMessage(Handler.java:106)
08-24 18:14:43.989 E/AndroidRuntime( 2366): 	at android.os.Looper.loop(Looper.java:164)
08-24 18:14:43.989 E/AndroidRuntime( 2366): 	at android.app.ActivityThread.main(ActivityThread.java:6518)
08-24 18:14:43.989 E/AndroidRuntime( 2366): 	at java.lang.reflect.Method.invoke(Native Method)
08-24 18:14:43.989 E/AndroidRuntime( 2366): 	at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:438)
08-24 18:14:43.989 E/AndroidRuntime( 2366): 	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:807)
08-24 18:14:43.989 D/RecurrenceRule(  599): Resolving using anchor 2018-08-24T18:14:43.989+08:00[Asia/Shanghai]

Log 提示uid 为10098 的进程没有权限使用一个provider。

 

源码分析:

debug 函数startActivity(),最终会调用到AMS 中,下面将堆栈打印出来:

08-24 18:14:43.946 E/ActivityManager(  599): WJ Stack:java.lang.Throwable
08-24 18:14:43.946 E/ActivityManager(  599): 	at com.android.server.am.ActivityManagerService.checkGrantUriPermissionLocked(ActivityManagerService.java:8975)
08-24 18:14:43.946 E/ActivityManager(  599): 	at com.android.server.am.ActivityManagerService.checkGrantUriPermissionFromIntentLocked(ActivityManagerService.java:9264)
08-24 18:14:43.946 E/ActivityManager(  599): 	at com.android.server.am.ActivityManagerService.grantUriPermissionFromIntentLocked(ActivityManagerService.java:9304)
08-24 18:14:43.946 E/ActivityManager(  599): 	at com.android.server.am.ActivityStarter.startActivityUnchecked(ActivityStarter.java:1203)
08-24 18:14:43.946 E/ActivityManager(  599): 	at com.android.server.am.ActivityStarter.startActivity(ActivityStarter.java:1000)
08-24 18:14:43.946 E/ActivityManager(  599): 	at com.android.server.am.ActivityStarter.startActivity(ActivityStarter.java:577)
08-24 18:14:43.946 E/ActivityManager(  599): 	at com.android.server.am.ActivityStarter.startActivityLocked(ActivityStarter.java:283)
08-24 18:14:43.946 E/ActivityManager(  599): 	at com.android.server.am.ActivityStarter.startActivityMayWait(ActivityStarter.java:822)
08-24 18:14:43.946 E/ActivityManager(  599): 	at com.android.server.am.ActivityManagerService.startActivityAsUser(ActivityManagerService.java:4616)
08-24 18:14:43.946 E/ActivityManager(  599): 	at com.android.server.am.ActivityManagerService.startActivity(ActivityManagerService.java:4603)
08-24 18:14:43.946 E/ActivityManager(  599): 	at android.app.IActivityManager$Stub.onTransact(IActivityManager.java:121)
08-24 18:14:43.946 E/ActivityManager(  599): 	at com.android.server.am.ActivityManagerService.onTransact(ActivityManagerService.java:2971)
08-24 18:14:43.946 E/ActivityManager(  599): 	at android.os.Binder.execTransact(Binder.java:697)

在AMS 中会调用grantUriPermissionFromIntentLocked():

    void grantUriPermissionFromIntentLocked(int callingUid,
            String targetPkg, Intent intent, UriPermissionOwner owner, int targetUserId) {
        NeededUriGrants needed = checkGrantUriPermissionFromIntentLocked(callingUid, targetPkg,
                intent, intent != null ? intent.getFlags() : 0, null, targetUserId);
        if (needed == null) {
            return;
        }

        grantUriPermissionUncheckedFromIntentLocked(needed, owner);
    }

这里参数intent、targetPkg、owner、targetUserId 都是ActivityStarter 传进来。

 

最终函数会调用到checkGrantUriPermissionLocked(),这个是权限处理的核心函数。

其实,AMS中为应用需要确定Uri 权限,单独提供了一个public 接口函数checkGrantUriPermission(),通过code 会发现该函数最终也是会调用到checkGrantUriPermissionLocked()。

    public int checkGrantUriPermission(int callingUid, String targetPkg, Uri uri,
            final int modeFlags, int userId) {
        enforceNotIsolatedCaller("checkGrantUriPermission");
        synchronized(this) {
            return checkGrantUriPermissionLocked(callingUid, targetPkg,
                    new GrantUri(userId, uri, false), modeFlags, -1);
        }
    }

来看下函数checkGrantUriPermissionLocked():

    int checkGrantUriPermissionLocked(int callingUid, String targetPkg, GrantUri grantUri,
            final int modeFlags, int lastTargetUid) {
        // 要求Intent 的flags 设为FLAG_GRANT_READ_URI_PERMISSION 或FLAG_GRANT_WRITE_URI_PERMISSION
        if (!Intent.isAccessUriMode(modeFlags)) {
            return -1;
        }

        ...
        ...

        // 要求scheme 是content
        if (!ContentResolver.SCHEME_CONTENT.equals(grantUri.uri.getScheme())) {
            if (DEBUG_URI_PERMISSION) Slog.v(TAG_URI_PERMISSION,
                    "Can't grant URI permission for non-content URI: " + grantUri);
            return -1;
        }

        // 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())) {
                // Exempted authority for cropping user photos in Settings app
            } else {
                Slog.w(TAG, "For security reasons, the system cannot issue a Uri permission"
                        + " grant to " + grantUri + "; use startActivityAsCaller() instead");
                return -1;
            }
        }

        ...
        ...

        // 确定targetUid 是否有该permission
        if (targetUid >= 0) {
            // First...  does the target actually need this permission?
            if (checkHoldingPermissionsLocked(pm, pi, grantUri, targetUid, modeFlags)) {
                // No need to grant the target this permission.
                if (DEBUG_URI_PERMISSION) Slog.v(TAG_URI_PERMISSION,
                        "Target " + targetPkg + " already has full permission to " + grantUri);
                return -1;
            }
        }
		
        ...
        ...
		
        /* There is a special cross user grant if:
         * - The target is on another user.
         * - Apps on the current user can access the uri without any uid permissions.
         * In this case, we grant a uri permission, even if the ContentProvider does not normally
         * grant uri permissions.
         */
        boolean specialCrossUserGrant = UserHandle.getUserId(targetUid) != grantUri.sourceUserId
                && checkHoldingPermissionsInternalLocked(pm, pi, grantUri, callingUid,
                modeFlags, false /*without considering the uid permissions*/);

        // Second...  is the provider allowing granting of URI permissions?
        if (!specialCrossUserGrant) {
            if (!pi.grantUriPermissions) {
                throw new SecurityException("Provider " + pi.packageName
                        + "/" + pi.name
                        + " does not allow granting of Uri permissions (uri "
                        + grantUri + ")");
            }
            if (pi.uriPermissionPatterns != null) {
                final int N = pi.uriPermissionPatterns.length;
                boolean allowed = false;
                for (int i=0; i<N; i++) {
                    if (pi.uriPermissionPatterns[i] != null
                            && pi.uriPermissionPatterns[i].match(grantUri.uri.getPath())) {
                        allowed = true;
                        break;
                    }
                }
                if (!allowed) {
                    throw new SecurityException("Provider " + pi.packageName
                            + "/" + pi.name
                            + " does not allow granting of permission to path of Uri "
                            + grantUri);
                }
            }
        }

        // 确认callingUid 是否有该permission
        if (!checkHoldingPermissionsLocked(pm, pi, grantUri, callingUid, modeFlags)) {
            // 确认是否之前已经存在该grant uri permission
            if (!checkUriPermissionLocked(grantUri, callingUid, modeFlags)) {
                if (android.Manifest.permission.MANAGE_DOCUMENTS.equals(pi.readPermission)) {
                    throw new SecurityException(
                            "UID " + callingUid + " does not have permission to " + grantUri
                                    + "; you could obtain access using ACTION_OPEN_DOCUMENT "
                                    + "or related APIs");
                } else {
                    throw new SecurityException(
                            "UID " + callingUid + " does not have permission to " + grantUri);
                }
            }
        }
        return targetUid;
    }

1. Intent 的flags 要求设为 FLAG_GRANT_READ_URI_PERMISSIONFLAG_GRANT_WRITE_URI_PERMISSION,不然不处理

2. 要求Uri 的scheme 是content,不然不处理

3. 如果provider 的grantUriPermissions属性为true,会接着确认grant-uri-permission,详细看 grant-uri-permission 文档

4. checkHoldingPermissionsLocked() 函数确定Provider的所需基本权限,详细看下面

5. checkUriPermissionLocked() 是在上面check fail 时,会进一步确认是否之前在provider 中已经grant,详细看下面。

 

先来看下checkHoldingPermissionsLocked():

    private final boolean checkHoldingPermissionsLocked(
            IPackageManager pm, ProviderInfo pi, GrantUri grantUri, int uid, final int modeFlags) {
        if (DEBUG_URI_PERMISSION) Slog.v(TAG_URI_PERMISSION,
                "checkHoldingPermissionsLocked: uri=" + grantUri + " uid=" + uid);
        if (UserHandle.getUserId(uid) != grantUri.sourceUserId) {
            if (ActivityManager.checkComponentPermission(INTERACT_ACROSS_USERS, uid, -1, true)
                    != PERMISSION_GRANTED) {
                return false;
            }
        }
        Slog.v(TAG_URI_PERMISSION,
                "enter checkHoldingPermissionsInternalLocked");
        return checkHoldingPermissionsInternalLocked(pm, pi, grantUri, uid, modeFlags, true);
    }

对于多用户需要最开始check INTERACT_ACROSS_USERS 权限,接着调用checkHoldingPermissionsInternalLocked():

    private final boolean checkHoldingPermissionsInternalLocked(IPackageManager pm, ProviderInfo pi,
            GrantUri grantUri, int uid, final int modeFlags, boolean considerUidPermissions) {
        if (pi.applicationInfo.uid == uid) {
            return true;
        } else if (!pi.exported) {
            return false;
        }

        boolean readMet = (modeFlags & Intent.FLAG_GRANT_READ_URI_PERMISSION) == 0;
        boolean writeMet = (modeFlags & Intent.FLAG_GRANT_WRITE_URI_PERMISSION) == 0;

        try {
            // check if target holds top-level <provider> permissions
            if (!readMet && pi.readPermission != null && considerUidPermissions
                    && (pm.checkUidPermission(pi.readPermission, uid) == PERMISSION_GRANTED)) {
                readMet = true;
            }
            if (!writeMet && pi.writePermission != null && considerUidPermissions
                    && (pm.checkUidPermission(pi.writePermission, uid) == PERMISSION_GRANTED)) {
                writeMet = true;
            }

            // track if unprotected read/write is allowed; any denied
            // <path-permission> below removes this ability
            boolean allowDefaultRead = pi.readPermission == null;
            boolean allowDefaultWrite = pi.writePermission == null;

            // check if target holds any <path-permission> that match uri
            final PathPermission[] pps = pi.pathPermissions;
            if (pps != null) {
                final String path = grantUri.uri.getPath();
                int i = pps.length;
                while (i > 0 && (!readMet || !writeMet)) {
                    i--;
                    PathPermission pp = pps[i];
                    if (pp.match(path)) {
                        if (!readMet) {
                            final String pprperm = pp.getReadPermission();
                            if (DEBUG_URI_PERMISSION) Slog.v(TAG_URI_PERMISSION,
                                    "Checking read perm for " + pprperm + " for " + pp.getPath()
                                    + ": match=" + pp.match(path)
                                    + " check=" + pm.checkUidPermission(pprperm, uid));
                            if (pprperm != null) {
                                if (considerUidPermissions && pm.checkUidPermission(pprperm, uid)
                                        == PERMISSION_GRANTED) {
                                    readMet = true;
                                } else {
                                    allowDefaultRead = false;
                                }
                            }
                        }
                        if (!writeMet) {
                            final String ppwperm = pp.getWritePermission();
                            if (DEBUG_URI_PERMISSION) Slog.v(TAG_URI_PERMISSION,
                                    "Checking write perm " + ppwperm + " for " + pp.getPath()
                                    + ": match=" + pp.match(path)
                                    + " check=" + pm.checkUidPermission(ppwperm, uid));
                            if (ppwperm != null) {
                                if (considerUidPermissions && pm.checkUidPermission(ppwperm, uid)
                                        == PERMISSION_GRANTED) {
                                    writeMet = true;
                                } else {
                                    allowDefaultWrite = false;
                                }
                            }
                        }
                    }
                }
            }

            // grant unprotected <provider> read/write, if not blocked by
            // <path-permission> above
            if (allowDefaultRead) readMet = true;
            if (allowDefaultWrite) writeMet = true;

        } catch (RemoteException e) {
            return false;
        }

        return readMet && writeMet;
    }

1. 应用uid 和provider uid 相同时,check 通过

2. provider 的exported 属性如果为false,check 直接不通过

3. 在provider 的exported 属性为true 时,为了保护provider 有时候需要加上read permission 和write permission,如果provider 设定了这两个permission,应用在使用过的时候需要保证有这两个权限,如code 中会通过函数checkUidPermission()。

当然,如果provider 没有加这两个权限的保护,系统认为read 和write 都是允许的,如code:

if (allowDefaultRead) readMet = true;
if (allowDefaultWrite) writeMet = true;

结论:

如果希望checkHoldingPermissionsLocked() 通过,必须满足下面其中一点:

1. 应用的uid 和provider uid 相同

2. provider 的exported 设为true,而且应用必须同时拥有read permission 和write permission,如果provider 没有加这个保护,默认情况下应用是有这两个权限

第二种情况比较特殊,如果是FileProvider 那么exported 必须是false,code 如下:(详细看 FileProvider文档

    public void attachInfo(Context context, ProviderInfo info) {                                    
        super.attachInfo(context, info);                                                                                                              
                                                                                                       
        // Sanity check our security                                                                   
        if (info.exported) {                                                                           
            throw new SecurityException("Provider must not be exported");                              
        }                                                                                              
        if (!info.grantUriPermissions) {                                                               
            throw new SecurityException("Provider must grant uri permissions");                        
        }                                                                                              
                                                                                                       
        mStrategy = getPathStrategy(context, info.authority);                                          
    }

 

上面checkGrantUriPermissionLocked() 函数我们知道最终需要两个条件中一个满足就可以check 通过:

1. checkHoldingPermissionsLocked() 函数能check pass

2. checkUriPermissionLocked() 函数能check pass

上面解析了checkHoldingPermissionsLocked() 的check 过程,详细看该函数后面的结论。对于FileProvider 比较特殊,要求exported 属性必须为false,函数check 肯定是fail,如果该函数check 不过,只需要保证checkUriPermissionLocked() 函数能check pass 就可以,也就是说即使provider 的exported 的属性值为false,也有可能check pass的。下面来详细解析checkUriPermissionLocked() 函数:

    private final boolean checkUriPermissionLocked(GrantUri grantUri, int uid,
            final int modeFlags) {
        final boolean persistable = (modeFlags & Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) != 0;
        final int minStrength = persistable ? UriPermission.STRENGTH_PERSISTABLE
                : UriPermission.STRENGTH_OWNED;

        // Root gets to do everything.
        if (uid == 0) {
            return true;
        }

        final ArrayMap<GrantUri, UriPermission> perms = mGrantedUriPermissions.get(uid);
        if (perms == null) return false;

        // First look for exact match
        final UriPermission exactPerm = perms.get(grantUri);
        if (exactPerm != null && exactPerm.getStrength(modeFlags) >= minStrength) {
            return true;
        }

        // No exact match, look for prefixes
        final int N = perms.size();
        for (int i = 0; i < N; i++) {
            final UriPermission perm = perms.valueAt(i);
            if (perm.uri.prefix && grantUri.uri.isPathPrefixMatch(perm.uri.uri)
                    && perm.getStrength(modeFlags) >= minStrength) {
                return true;
            }
        }

        return false;
    

函数主要确认mGrantedUriPermissions 是否有对应uid 额ArrayMap 存在,也就是说之前必须要创建这样的ArrayMap 并且是将其add 到mGrantedUriPermissions 中。

    private final SparseArray<ArrayMap<GrantUri, UriPermission>>
            mGrantedUriPermissions = new SparseArray<ArrayMap<GrantUri, UriPermission>>();

而这个mGrantedUriPermissions 是在函数findOrCreateUriPermissionLocked() 中创建:

    private UriPermission findOrCreateUriPermissionLocked(String sourcePkg,
            String targetPkg, int targetUid, GrantUri grantUri) {
        ArrayMap<GrantUri, UriPermission> targetUris = mGrantedUriPermissions.get(targetUid);
        if (targetUris == null) {
            targetUris = Maps.newArrayMap();
            mGrantedUriPermissions.put(targetUid, targetUris);
        }

        UriPermission perm = targetUris.get(grantUri);
        if (perm == null) {
            perm = new UriPermission(sourcePkg, targetPkg, targetUid, grantUri);
            targetUris.put(grantUri, perm);
        }

        return perm;
    }

 

其实从 FileProvider 文档 中可以知道一般grant uri permission 有两种选择:

1. 通过接口grantUriPermission() 

Call the method Context.grantUriPermission(package, Uri, mode_flags) for the content:// Uri, using the desired mode flags. This grants temporary access permission for the content URI to the specified package, according to the value of the the mode_flags parameter, which you can set toFLAG_GRANT_READ_URI_PERMISSIONFLAG_GRANT_WRITE_URI_PERMISSION or both. The permission remains in effect until you revoke it by calling revokeUriPermission() or until the device reboots.

 

2. 通过Intent 配置

Permissions granted in an Intent remain in effect while the stack of the receiving Activity is active. When the stack finishes, the permissions are automatically removed. Permissions granted to one Activity in a client app are automatically extended to other components of that app.

 

实例结论:

实例中出现的exception 问题,应该是Android 自身的问题,在分享到短信的时候,短信自身又启动了一个Activity,这样就导致了checkHoldingPermissionsLocked() 传入的 targetUid 和 callingUid都是短信自身的uid,所以最后导致check 都不能pass。在第一次startActivity的时候会进入checkGrantUriPermissionLocked(),但是碰到条件给过滤掉了,我们需要修改该过滤条件使其满足:

        if ((callingAppId == SYSTEM_UID) || (callingAppId == ROOT_UID)) {
            if ("com.android.settings.files".equals(grantUri.uri.getAuthority())) {
                // Exempted authority for cropping user photos in Settings app
            } else {
                Slog.w(TAG, "For security reasons, the system cannot issue a Uri permission"
                        + " grant to " + grantUri + "; use startActivityAsCaller() instead");
                return -1;
            }
        }

其实通过code 可以看到Android 源生也针对com.android.settings.files 进行了特殊的过滤。我们只需要参考这个即可。

 

 

参考文献:

https://developer.android.com/guide/topics/manifest/provider-element

https://developer.android.com/guide/topics/manifest/grant-uri-permission-element

https://developer.android.com/reference/android/support/v4/content/FileProvider#SpecifyFiles

  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
AndroidManifest.xml 是 Android 应用程序的核心清单文件,它在应用程序打包和安装时自动包含在 APK 文件中。它包含了应用程序所有组件的描述信息,包括活动(Activity)、服务(Service)、广播接收器(BroadcastReceiver)和内容提供器(Content Provider)等。在 Android 应用程序运行时,系统会根据该文件中的信息来决定如何启动、暂停或停止应用程序中的各个组件。 下面是 AndroidManifest.xml 中常用的标签及其解释: 1. manifest 标签:表示整个清单文件。 2. uses-sdk 标签:指定应用程序所需的 Android SDK 版本。 3. application 标签:表示应用程序的主要配置信息,包括应用程序的名称、图标、主题、权限、进程等。 4. activity 标签:表示一个活动(Activity),即一个界面,用于与用户交互。其中包括活动的名称、标签、主题、启动模式、导航栏、窗口大小等信息。 5. service 标签:表示一个服务(Service),即在后台长期运行的组件,用于执行一些耗时任务。其中包括服务的名称、进程、权限等信息。 6. receiver 标签:表示一个广播接收器(BroadcastReceiver),用于接收系统或其他应用程序发送的广播消息。其中包括广播接收器的名称、权限、过滤器等信息。 7. provider 标签:表示一个内容提供器(Content Provider),用于为应用程序提供数据访问接口。其中包括内容提供器的名称、权限URI 等信息。 8. uses-permission 标签:表示应用程序所需的权限,用于访问受保护的系统资源或执行某些敏感操作。 以上是常用的 AndroidManifest.xml 标签,每个标签都有其特定的属性和用法,需要根据实际情况进行配置。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

私房菜

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值