美文网首页
java.lang.SecurityException:Perm

java.lang.SecurityException:Perm

作者: 梅林骑士 | 来源:发表于2023-03-06 12:44 被阅读0次

记录一个深坑

最近发的版本线上突然多了好多支付报错的异常。

其中一部分

这只是其中一部分,2天时间影响大概30-40个用户。
但是看错误的回调栈很奇怪。

image.png

光看回调栈,基本就知道了被start的Activity的清单文件Manifest的exported属性肯定设置了false。

为了验证,我特意下载了DANA的app,打开了他们的Manifest文件。

kXMbptKVGi.jpg

结果意料之中,又有一些意料之外,然后看这个app的发版时间

image.png
确实是最近才发布的新版本。但是看错误日志Intent的data里的uri是https://wsa.wallet.airpay.co.id,打开这个链接你会发现这个是shopee的网站。也就是说这个支付行为不是DANA渠道的。

我们知道https的跳转默认属于浏览器行为,这个属于系统默认行为,除非用户主动指定默认浏览器行为。

  • 指定默认浏览器行为在某些系统上可以通过app的意图选择器。
  • 主要还是可以通过系统设置->apps->选择默认app->选择默认浏览器,指定浏览器执行app。
  • 当然还有第三种情况,禁用系统浏览器,导致无可默认可执行意图,那么系统选择意图会去找可以执行该意图的app。
  • 当然还可以在app内部通过指定ComponentName指定可以执行的app,这种方式需要再清单文件里声明queries标签增加指定app包名。

很明显,第一种和第二种不太可能,因为DANA没有声明Browser行为,第四种也不可能,因为发起app是我自己写的。

所以只有第三种可能,系统浏览器被禁用了,模拟一下尝试还原崩溃场景。
禁用默认浏览器,添加启动Shopee代码。

startActivity(Intent.parseUri("https://wsa.wallet.airpay.co.id",Intent.FLAG_ACTIVITY_NEW_TASK))
img_v2_63a9d6e9-5c94-4e7d-985d-8005f96ff57g.jpg

结果意料之中,确实还原触发了线上崩溃。对于这种外部影响导致的崩溃确实有些坑爹,本质上是因为用户的不规范行为禁用系统浏览器(但是这种用户不在少数,看线上影响用户达到40+就能看出来),以及DANA app的不规范声明导致的(但DANA的清单文件声明其实是合规的,所以本质上我觉得还是android系统Bug)。

既然找到了问题,解决方案也就好说。

  • 自己在清单文件声明一个https处理activity,exported声明为true,好处是可以引流,坏处是也可能会被用户认为是流氓软件。
  • 在启动deeplink之前,判断shema是否为https,如果是则打开app内置浏览器。

第二种没什么好说的代码太简单。
第一种方式,我们通过查询

<activity
            android:name=".activity.WebViewActivity"
            android:theme="@style/Theme.BaseProject"
            android:screenOrientation="portrait"
            android:exported="true"
            android:configChanges="orientation|keyboardHidden|fontScale|navigation|layoutDirection|locale"
            android:windowSoftInputMode="stateAlwaysHidden|adjustResize" >

            <intent-filter android:autoVerify="true">
                <action android:name="android.intent.action.VIEW"/>
                <category android:name="android.intent.category.DEFAULT"/>
                <category android:name="android.intent.category.BROWSABLE"/>
                <data android:scheme="https"/>
            </intent-filter>

</activity>
image.png

通过查看出错源码我们知道最终会调用Android11以下通过ActivityStackSupervisor,Android11以上通过ActivityTaskSupervisor
的checkStartAnyActivityPermission去检查权限,代码差不多,可以从代码中看到我们崩溃的代码是因为a info.exprted=false导致的。

boolean checkStartAnyActivityPermission(Intent intent, ActivityInfo aInfo, String resultWho,
            int requestCode, int callingPid, int callingUid, String callingPackage,
            @Nullable String callingFeatureId, boolean ignoreTargetSecurity,
            boolean launchingInTask, WindowProcessController callerApp, ActivityRecord resultRecord,
            Task resultRootTask) {
        final boolean isCallerRecents = mService.getRecentTasks() != null
                && mService.getRecentTasks().isCallerRecents(callingUid);
        final int startAnyPerm = mService.checkPermission(START_ANY_ACTIVITY, callingPid,
                callingUid);
        if (startAnyPerm == PERMISSION_GRANTED || (isCallerRecents && launchingInTask)) {
            // If the caller has START_ANY_ACTIVITY, ignore all checks below. In addition, if the
            // caller is the recents component and we are specifically starting an activity in an
            // existing task, then also allow the activity to be fully relaunched.
            return true;
        }
        final int componentRestriction = getComponentRestrictionForCallingPackage(aInfo,
                callingPackage, callingFeatureId, callingPid, callingUid, ignoreTargetSecurity);
        final int actionRestriction = getActionRestrictionForCallingPackage(
                intent.getAction(), callingPackage, callingFeatureId, callingPid, callingUid);
        if (componentRestriction == ACTIVITY_RESTRICTION_PERMISSION
                || actionRestriction == ACTIVITY_RESTRICTION_PERMISSION) {
            if (resultRecord != null) {
                resultRecord.sendResult(INVALID_UID, resultWho, requestCode,
                        Activity.RESULT_CANCELED, null /* data */, null /* dataGrants */);
            }
            final String msg;
            if (actionRestriction == ACTIVITY_RESTRICTION_PERMISSION) {
                msg = "Permission Denial: starting " + intent.toString()
                        + " from " + callerApp + " (pid=" + callingPid
                        + ", uid=" + callingUid + ")" + " with revoked permission "
                        + ACTION_TO_RUNTIME_PERMISSION.get(intent.getAction());
            } 
//
//看的出来我们的出错源码msg来自于这里
//aInfo就是ActivityInfo,ActivityInfo中含有exported标志
else if (!aInfo.exported) {
                msg = "Permission Denial: starting " + intent.toString()
                        + " from " + callerApp + " (pid=" + callingPid
                        + ", uid=" + callingUid + ")"
                        + " not exported from uid " + aInfo.applicationInfo.uid;
            } else {
                msg = "Permission Denial: starting " + intent.toString()
                        + " from " + callerApp + " (pid=" + callingPid
                        + ", uid=" + callingUid + ")"
                        + " requires " + aInfo.permission;
            }
            Slog.w(TAG, msg);
            throw new SecurityException(msg);
        }

        if (actionRestriction == ACTIVITY_RESTRICTION_APPOP) {
            final String message = "Appop Denial: starting " + intent.toString()
                    + " from " + callerApp + " (pid=" + callingPid
                    + ", uid=" + callingUid + ")"
                    + " requires " + AppOpsManager.permissionToOp(
                            ACTION_TO_RUNTIME_PERMISSION.get(intent.getAction()));
            Slog.w(TAG, message);
            return false;
        } else if (componentRestriction == ACTIVITY_RESTRICTION_APPOP) {
            final String message = "Appop Denial: starting " + intent.toString()
                    + " from " + callerApp + " (pid=" + callingPid
                    + ", uid=" + callingUid + ")"
                    + " requires appop " + AppOpsManager.permissionToOp(aInfo.permission);
            Slog.w(TAG, message);
            return false;
        }

        return true;
    }

所以我们的最终的启动deeplink的逻辑就可以通过查询过滤进行。

            //不过首先需要判断是否有默认启动app
            var intent = Intent.parseUri("https://wsa.wallet.airpay.co.id",Intent.FLAG_ACTIVITY_NEW_TASK)
            var defaultStartApp =  packageManager.resolveActivity(intent,
                PackageManager.MATCH_DEFAULT_ONLY)
            if(defaultStartApp == null){
                val enableStartList = packageManager.queryIntentActivities(intent,
                    PackageManager.MATCH_DEFAULT_ONLY)
                if(enableStartList != null && enableStartList.isEmpty().not()){
                    defaultStartApp = enableStartList[0]
                }
            }
            defaultStartApp?.let {
                startActivity(intent.apply {
                    setClassName(it.activityInfo.packageName,it.activityInfo.name) 
                })
            }

queryIntentActivities的第二个参数表示过滤标志位,传0则默认返回所有,也可以通过MATCH_DIRECT_BOOT_AUTO查询可自动启动的app行为,这里返回的值主要是在清单文件里加了这个属性的,以及系统默认可执行schema行为app包信息。


image.png

我们可以通过MATCH_DEFAULT_ONLY标志位过滤掉exported为空的情况,保险起见,在返回结果集合中对非exported进行过滤。

packageManager.queryIntentActivities(
Intent.parseUri("https://wsa.wallet.airpay.co.id",Intent.FLAG_ACTIVITY_NEW_TASK),
PackageManager.MATCH_DIRECT_BOOT_AUTO)
.filter { it.activityInfo.exported }

好了,以上就是解决android系统坑问题的过程了。

相关文章

网友评论

      本文标题:java.lang.SecurityException:Perm

      本文链接:https://www.haomeiwen.com/subject/moqpldtx.html