记录一次 Android 权限删除经历

作者: bfc7f634299a | 来源:发表于2019-07-29 14:49 被阅读7次

    1.事发经过

    近期google play发布了新的政策,其中一部分是限制权限使用,只允许满足条件的使用场景才能申请权限,小编所在的项目被检测出使用了RECEIVE_SMS权限,但是从app下的Androidmanifest文件中并未发现有该权限的注册,所以该权限是哪里来的呢?

    2.初步定位

    首先使用android studio查看了打包出来的apk中的Androidmanifest文件,发现其中确实存在RECEIVE_SMS权限,也就是说打包到apk中的Androidmanifest 文件并不是app下的该文件,从 android开发者官网中合并多个manifest文件的文档来看,实际上打包到apk中的 manifest文件是由多个menifest文件合并而来的,其合并顺序如下:

    manifest文件合并规则

    优先级由低到高分别是:
    第三方库中 < app模块 < app模块的源集

    当合并发生冲突的时候,可以使用合并规则标记来处理冲突,所以我这里使用了tools:node="remove"来处理移除RECEIVE_SMS权限,重新打包查看结果发现,仍然存在该权限,也就是说该标记失效了吗?再思考一下,是否有记录合并过程的文件呢?答案是有的,如下:

    合并规则结束生成的android manifest文件:app/build/intermediates/manifests/full/googleplay/debug/AndroidManifest.xml

    合并操作记录文件:
    app/build/outputs/logs/manifest-merger-googleplay-debug-report.txt

    上述的googleplay是自定义的productFlavors,如果未定义就是app。

    查看 AndroidManifest.xml 发现确实存在 RECEIVE_SMS 权限,但是查看manifest-merger-googleplay-debug-report.txt 却找不到该权限的合并记录;也就是说,在正常的合并流程中,并没有 RECEIVE_SMS 权限的写入,会不会有人破坏了正常的合并流程呢?没办法,需要追踪到具体是哪一个第三方库引入了该权限,仔细对比了下apk中的 Androidmenifest 文件和app下的该文件,发现每次都是在该文件末尾多出了RECEIVE_SMS和其他一些东西,仔细一看发现是mobsdk相关的,于是剔除了该库,再编译发现还是存在该权限。。仔细想下,有点不对,因为要破坏manifest文件的合并,那么普通的第三方库是不行的,至少需要第三方 gradle 插件,于是移除了 “com.mob.sdk” 插件,再打包发现确实没有 RECEIVE_SMS 了

    3.原理解析

    到此已经找到了问题的制造者,接下来就是看下他是怎么实现的,面向google编程,搜索com.mob.sdk source code 找到maven仓库,可以找到其实现核心如下:

    project.afterEvaluate {
                def android = project.extensions.getByName("android")
                if (globalVariants.autoConfig == null) {
                    globalVariants.autoConfig = true
                }
                if (globalVariants.autoConfig) {
                    if (android != null) {
                        configShareSDKXML(android)
    
                        def variants = null
                        boolean appModel = false
                        try {
                            variants = android.applicationVariants
                            appModel = true
                        } catch (Throwable t) {
                            try {
                                variants = android.libraryVariants
                            } catch (Throwable tt) {}
                        }
                        if (variants != null) {
                            variants.all { variant ->
                                variant.outputs.each { output ->
                                    output.processManifest.doLast {
                                        configManifest(output, appModel, variant)
                                    }
                                }
                            }
                        }
                    }
                }
            }
    
    private void configManifest(def output, boolean appModel, def variant) {
                ...
            manifestFiles.add(new File(output.processManifest.manifestOutputDirectory, "AndroidManifest.xml"))
            manifestFiles.each { manifestFile->
                if (manifestFile != null && manifestFile.exists()) {
                    ...
                    shouldAdd.each { per ->
                        String lastPermission = "<uses-permission ${ns} android:name=\"${per}\" />"
                        if (packageName != null && lastPermission.contains('${applicationId}')) {
                            lastPermission = lastPermission.replace('${applicationId}', packageName)
                        }
                        def permission = parser.parseText(lastPermission)
                        manifest.appendNode(permission)
                    }
                    ...
                    def nsCustom = 'xmlns:android="http://schemas.android.com/apk/res/android"'
                    def level = 'android:protectionLevel="signature"'
                    shouldAddCoustom.each { per ->
                        String lastPermission = "<permission ${nsCustom} android:name=\"${per}\" ${level}/>"
                        if (packageName != null && lastPermission.contains('${applicationId}')) {
                            lastPermission = lastPermission.replace('${applicationId}', packageName)
                        }
                        def permission = parser.parseText(lastPermission)
    
                        manifest.appendNode(permission)
                    }
    
                    ...
    
                    manifestFile.setText(XmlUtil.serialize(manifest), "utf-8")
                }
    

    可以看出其hook了gradle的解析了配置之后注入了gradle任务(任务相关可以参考官网),详细的gradle的构建周期函数可以参考这个https://www.jianshu.com/p/0acdb31eef2d 。在 processManifest 任务执行之后执行了他自己的动作,也就是更改 androidmanifest 文件的内容

    4.修复方案

    了解了其实现原理之后,开始整理其修复方案,主要需要解决的是,在合适的时间点去移除权限,也就是需要在其修改完Androidmanifest文件之后,和Androidmanifest文件被打包到apk中之前这段时间,这里涉及到gradle打包中的各个函数调用顺序,详细的打包流程参考这里,详细的任务在这里,我这里选择的切入点在processResources的任务执行之前,详细代码如下:

    project.afterEvaluate {
            project.android.applicationVariants.all { variant ->
                variant.outputs.each { output ->
                    output.processResources.doFirst { pm->
                        String manifestPath = output.processResources.manifestFile;
                        def manifestContent = file(manifestPath).getText()
                        manifestContent = manifestContent.replace('<uses-permission android:name="android.permission.RECEIVE_SMS"/>', '')
                        file(manifestPath).write(manifestContent)
                    }
                }
            }
        }
    

    主要的处理就是剔除其中的RECEIVE_SMS权限相关。当然,前提是项目中确实没有使用该权限,所以移除不会导致相关问题。

    5.复盘

    解决该问题主要涉及到 Androidmanifest.xml 的合并,gradle构建生命周期,android打包流程和相关的gradle知识,当前对gradle的了解不够,导致阅读和理解比较耗时,接下来需要多关注,此外还有一个问题没解决,就是采用 .processManifest.finalizedBy 这种方式时,发现 androidmanifest 文件经历了如下情况:

    没有注入权限->注入权限->删除权限->又注入了权限

    不知道是在哪一步又被注入了权限,还是其他情况?

    相关文章

      网友评论

        本文标题:记录一次 Android 权限删除经历

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