美文网首页Android开发Android技术知识Android开发
Gradle实现自动化加固与多渠道打包

Gradle实现自动化加固与多渠道打包

作者: Find_A_Way | 来源:发表于2020-04-26 11:07 被阅读0次
    image

    研究这个Gradle自动化脚本初衷是为了实现自动化打包、加固和增加多渠道,从而一键完成应用发布上架应用市场前的所有操作,以达到解放双手和节约时间成本的效果。后期有考虑配合curl指令将打包好的apk自动上传到服务器或者托管平台,亦或可结合Jenkins自动化构建、打包、上传等,从而实现整个流程的自动化目的。

    App打包发布前准备

    通常我们App上架到应用市场基本上都经历过以下流程,先本地打一个release包,然后通过在线加固或者下载加固工具进行加固,由于加固会先剔除签名信息,所以加固后要进行再次签名,然后生成多渠道包,这样基本上整个流程就结束了,画了个思维导图如下:


    image

    加固介绍

    我的简单理解就是给原有的apk进行加密和套壳,产生一个新的apk,然后运行的时候会进行解密相关的动作,所以加固后的app一般会影响启动时间,网上也有很多加固平台的对比,主要涉及启动时间、包体积大小、兼容性、安全性等等。本次研究只是讨论如何实现自动化加固与多渠道打包思想,360加固并非最好选择,加固主要是为了防止应用在上线后被反编译、调试、破解、二次打包和内存截取等多种威胁。

    下载360加固保

    本次Gradle自动化实践的步骤主要是基于360加固+腾讯的VasDolly多渠道打包。

    • 手动下载
      官方主要提供Windows、Mac、Linux三种版本,下载地址
    • 自动下载
      如果是Mac系统的话,在Gradle中直接使用curl命令即可,如果是Windwos需要下载安装curl,curl主要是文件传输工具,可以通过命令行可支持文件下载及传输。
    /**
     * 自动下载360加固保,也可以自己下载然后放到根目录
     */
    def download360jiagu() {
        // 下载360压缩包
        File zipFile = file(packers["zipPath"])
        if (!zipFile.exists()) {
            if (!zipFile.parentFile.exists()) {
                zipFile.parentFile.mkdirs()
                println("packers===create parentFile jiagu ${zipFile.parentFile.absolutePath}")
            }
            // 加固保的下载地址
            def downloadUrl = isWindows() ? packers["jiagubao_windows"] : packers["jiagubao_mac"]
            // mac自带curl命令 windows需要下载curl安装
            def cmd = "curl -o ${packers["zipPath"]} ${downloadUrl}"
            println cmd
            cmd.execute().waitForProcessOutput(System.out, System.err)
        }
        File unzipFile = file(packers["unzipPath"])
        if (!unzipFile.exists()) {
            //解压 Zip 文件
            ant.unzip(src: packers["zipPath"], dest: packers["unzipPath"], encoding: "GBK")
            println 'packers===unzip 360jiagu'
            //将解压后的文件开启读写权限,防止执行 Jar 文件没有权限执行,windows若没有权限需要自己手动改
            if (!isWindows()) {
                def cmd = "chmod -R 777 ${packers["unzipPath"]}"
                println cmd
                cmd.execute().waitForProcessOutput(System.out, System.err)
            }
        }
    }
    

    打一个release包

    gradle其实为我们提供了一系列相关的任务,如下图

    image
    我们执行加固前是需要拿到一个release包的,所以我们可以利用assembleRelease在加固前先执行assembleRelease这个Task。
    task packersNewRelease {
        group 'packers'
        //可以利用task的依赖关系先执行打包
        dependsOn 'assembleRelease'
        }
    

    自动执行加固

    所谓自动执行加固,无非就是几行命令,360加固保提供了一套命令行进行加固

    image
    特别提醒,此处360配置可选项的增强服务有bug,已经跟官方沟通,他们需要在下个版本修复,当前存在bug的版本3.2.2.3(2020-03-16),命令行目前无法只选择盗版监测
    
    /**
     *  对于release apk 进行360加固
     */
    def packers360(File releaseApk) {
        println 'packers===beginning 360 jiagu'
        def packersFile = file(app["packersPath"])
        if (!packersFile.exists()) {
            packersFile.mkdir()
        }
        exec {
            // 登录360加固保
            executable = 'java'
            args = ['-jar', packers["jarPath"], '-login', packers["account"], packers["password"]]
            println 'packers===import 360 login'
        }
        exec {
            // 导入签名信息
            executable = 'java'
            args = ['-jar', packers["jarPath"], '-importsign', signing["storeFile"],
                    signing["storePassword"], signing["keyAlias"], signing["keyPassword"]]
            println 'packers===import 360 sign'
        }
        exec {
            // 查看360加固签名信息
            executable = 'java'
            args = ['-jar', packers["jarPath"], '-showsign']
            println 'packers===show 360 sign'
        }
        exec {
            // 初始化加固服务配置,后面可不带参数
            executable = 'java'
            args = ['-jar', packers["jarPath"], '-config']
            println 'packers===init 360 services'
        }
        exec {
            // 执行加固,然后自动签名,若不采取自动签名,需要自己通过build-tools命令自己签名
            executable = 'java'
            args = ['-jar', packers["jarPath"], '-jiagu', releaseApk.absolutePath, app["packersPath"], '-autosign']
            println 'packers===excute 360 jiagu'
        }
        println 'packers===360 jiagu finished'
        println "packers===360 jiagu path ${app["packersPath"]}"
    }
    

    自动签名

    关于自动签名,其实360在加固的时候提供了自动签名的配置选项,如果你不想将签名文件上传给360,在加固后可以自己选择手动签名,因为这涉及到安全性的问题,此版本我采取的是360自动签名,如果大家想自己手动签名,下面我给出一套方案,主要是利用 zipalignapksigner命令
    他们都是位于SDK文件中的build-tools目录中,我们执行自动化签名需要gradle配置好路径。

    • 对齐未签名的apk
    zipalign -v -p 4 my-app-unsigned.apk my-app-unsigned-aligned.apk
    
    • 使用你的私钥为apk签名
    apksigner sign --ks my-release-key.jks --out my-app-release.apk my-app-unsigned-aligned.apk
    
    • 验证apk是否已经被签名
    apksigner verify my-app-release.apk
    

    基于加固Apk自动实现多渠道

    关于多渠道打包,我们之前项目一直使用的是腾讯的VasDolly,故我们此次是采取VasDolly命令,但是需要先下载VasDolly.jar,至于放在什么位置没有要求,只需要gradle配置好路径即可,我直接是放在项目根目录。

    image
    /**
     * 腾讯channel重新构建渠道包
     */
    def reBuildChannel() {
        File channelFile = file("${app["channelPath"]}/new")
        if (!channelFile.exists()) {
            channelFile.mkdirs()
        }
        def cmd = "java -jar ${app["vasDollyPath"]} put -c ${"../channel.txt"} ${outputpackersApk()} ${channelFile.absolutePath}"
        println cmd
        cmd.execute().waitForProcessOutput(System.out, System.err)
        println 'packers===excute VasDolly reBuildChannel'
    }
    
    

    敏感信息存取

    我们都知道,签名需要签名文件,密码、别名等等文件,360加固需要配置账号与密码,这些都属于敏感信息,google官方不建议直接放在gradle中,它是以纯文本记录在gradle中的,建议存储在properties文件中。

    // 把敏感信息存放到自定义的properties文件中
    def propertiesFile = rootProject.file("release.properties")
    def properties = new Properties()
    properties.load(new FileInputStream(propertiesFile))
    
    ext {
        // 签名配置
        signing = [keyAlias     : properties['RELEASE_KEY_ALIAS'],
                   keyPassword  : properties['RELEASE_KEY_PASSWORD'],
                   storeFile    : properties['RELEASE_KEYSTORE_PATH'],
                   storePassword: properties['RELEASE_STORE_PASSWORD']
        ]
    
        // app相关的配置
        app = [
                //默认release apk的文件路径,因为加固是基于release包的
                releasePath : "${project.buildDir}/outputs/apk/release",
                //对release apk 加固后产生的加固apk地址
                packersPath : "${project.buildDir}/outputs/packers",
                //加固后进行腾讯多渠道打包的地址
                channelPath : "${project.buildDir}/outputs/channels",
                //腾讯VasDolly多渠道打包jar包地址
                vasDollyPath: "../VasDolly.jar"
        ]
    
        // 360加固配置
        packers = [account          : properties['ACCOUNT360'], //账号
                   password         : properties['PASSWORD360'],  //密码
                   zipPath          : "${project.rootDir}/jiagu/360jiagu.zip",  //加固压缩包路径
                   unzipPath        : "${project.rootDir}/jiagu/360jiagubao/",  //加固解压路径
                   jarPath          : "${project.rootDir}/jiagu/360jiagubao/jiagu/jiagu.jar",  //执行命令的jar包路径
                   channelConfigPath: "${project.rootDir}/jiagu/Channel.txt",  //加固多渠道
                   jiagubao_mac     : "https://down.360safe.com/360Jiagu/360jiagubao_mac.zip",  //加固mac下载地址
                   jiagubao_windows : "https://down.360safe.com/360Jiagu/360jiagubao_windows_64.zip" //加固widnows下载地址
        ]
    

    gradle相关基础

    • gradle脚本插件的引用
    apply from: "${project.rootDir}/packers.gradle"
    
    • 局部变量
     def dest = "A"
    
    • 扩展属性
    使用ext扩展块,一次扩展多个属性
    ext {
        account = "XXXX"
        password = "XXXXX"
    }
    
    • 字符串相关
    单引号不支持插值
    def name = '张三'
    双引号支持插值
    def name = "我是${'张三'}"
    三个单引号支持换行
    def name = """
    张三
    李四
    """
    
    • 可有可无的圆括号
    // 这两种写法等价
    println('A')
    println 'A'
    
    • 闭包作为方法的最后一个参数
    repositories {
        println "A"
    }
    repositories() { println "A" }
    repositories({println "A" })
    
    • task依赖
    task B {
        // TaskB依赖TaskA,故会先执行TaskA
        dependsOn A
        //其次执行packersRelease
        doLast {
            println "B"
        }
    }
    
    • task排序
    //taskB必须总是在 taskA 之后运行, 无论 taskA 和 taskB 是否将要运行
    taskB.mustRunAfter(taskA)
    //没有msut那么严格
    taskB.shouldRunAfter (taskA)
    
    • 文件定位
    // 使用一个相对路径
    File configFile = file('src/config.xml')
    // 使用一个绝对路径
    configFile = file(configFile.absolutePath)
    // 使用一个项目路径的文件对象 
    configFile = file(new File('src/config.xml'))`
    
    • 文件遍历
    // 对文件集合进行迭代
    collection.each {File file ->
        println file.name
    }
    
    • 文件复制重命名
     copy {
            from 源文件地址
            into 目标目录地址
            rename(“原文件名”, "新文件名字")  
        }
    

    自动上传到服务器

    这个功能准备在下篇文章更新,我们可以通过curl命令上传到自己的服务器,如果你在测试阶段可以上传到蒲公英或者fir.im托管平台,目前他们都提供了相关的操作方式,这样基本上整个自动化的目的就完成了,当然你也可以选择Jenknis自动化构建、打包及上传。

    • 发布应用到fir.im托管平台 入口
    方式一:fir-CLI 命令行工具上传  
    $ fir p path/to/application -T YOUR_FIR_TOKEN
    方式二:API 上传
    通过curl命令调用相关的api
    1.获取凭证
    curl -X "POST" "http://api.bq04.com/apps" \
         -H "Content-Type: application/json" \
         -d "{\"type\":\"android\", \"bundle_id\":\"xx.x\", \"api_token\":\"aa\"}"
    2.上传apk
    curl   -F "key=xxxxxx"              \
           -F "token=xxxxx"             \
           -F "file=@aa.apk"            \
           -F "x:name=aaaa"             \
           -F "x:version=a.b.c"         \
           -F "x:build=1"               \
           -F "x:release_type=Adhoc"   \  #type=ios 使用
           -F "x:changelog=first"       \
           https://up.qbox.me
    
    • 发布应用到蒲公英 入口
    curl -F "file=@/tmp/example.ipa" -F "uKey=" -F "_api_key=" https://upload.pgyer.com/apiv1/app/upload
    
    image

    整体效果

    我们的需求是需要打两批包,用于老后台与新后台,老后台的包必须加上app-前缀,所以有三个任务packersNewRelease执行正常的加固打包用于新后台,packersOldRelease用于打包加前缀app-名称用于老后台,packersRelease这个任务用于一键同时打包成老后台与新后台。

    image
    同时可以在gradle控制台查看打包任务的输出日志,如下: image

    gradle自动化源码

    为了能够让大家尝试自动化gradle脚本带来的便利之处,下面我贡献上自己的整个gradle源码,需要的可以拿走去研究,如存在问题也希望多多交流。

    /**
     * @author hule
     * @date 2020/04/15 13:42
     * description:360自动加固+Vaslloy多渠道打包
     */
    
    // 把敏感信息存放到自定义的properties文件中
    def propertiesFile = rootProject.file("release.properties")
    def properties = new Properties()
    properties.load(new FileInputStream(propertiesFile))
    
    ext {
        // 签名配置
        signing = [keyAlias     : properties['RELEASE_KEY_ALIAS'],
                   keyPassword  : properties['RELEASE_KEY_PASSWORD'],
                   storeFile    : properties['RELEASE_KEYSTORE_PATH'],
                   storePassword: properties['RELEASE_STORE_PASSWORD']
        ]
    
        // app相关的配置
        app = [
                //默认release apk的文件路径,因为加固是基于release包的
                releasePath : "${project.buildDir}/outputs/apk/release",
                //对release apk 加固后产生的加固apk地址
                packersPath : "${project.buildDir}/outputs/packers",
                //加固后进行腾讯多渠道打包的地址
                channelPath : "${project.buildDir}/outputs/channels",
                //腾讯VasDolly多渠道打包jar包地址
                vasDollyPath: "../VasDolly.jar"
        ]
    
        // 360加固配置
        packers = [account          : properties['ACCOUNT360'], //账号
                   password         : properties['PASSWORD360'],  //密码
                   zipPath          : "${project.rootDir}/jiagu/360jiagu.zip",  //加固压缩包路径
                   unzipPath        : "${project.rootDir}/jiagu/360jiagubao/",  //加固解压路径
                   jarPath          : "${project.rootDir}/jiagu/360jiagubao/jiagu/jiagu.jar",  //执行命令的jar包路径
                   channelConfigPath: "${project.rootDir}/jiagu/Channel.txt",  //加固多渠道
                   jiagubao_mac     : "https://down.360safe.com/360Jiagu/360jiagubao_mac.zip",  //加固mac下载地址
                   jiagubao_windows : "https://down.360safe.com/360Jiagu/360jiagubao_windows_64.zip" //加固widnows下载地址
        ]
    }
    
    /**
     *  360加固,适用于新后台打包
     */
    task packersNewRelease {
        group 'packers'
        dependsOn 'assembleRelease'
        doLast {
            //删除加固后的渠道包
            deleteFile()
            // 下载360加固文件
            download360jiagu()
            // 寻找打包文件release apk
            def releaseFile = findReleaseApk()
            if (releaseFile != null) {
                //执行加固签名
                packers360(releaseFile)
                //对加固后的apk重新用腾讯channel构建渠道包
                reBuildChannel()
            } else {
                println 'packers===can\'t find release apk and can\'t excute 360 jiagu'
            }
        }
    }
    
    /**
     * 适用于老后台,老后台需要在渠道apk的名称增加前缀 app-
     */
    task packersOldRelease {
        group 'packers'
        doLast {
            File channelFile = file("${app["channelPath"]}/new")
            if (!channelFile.exists() || !channelFile.listFiles()) {
                println 'packers==== please excute pakcersNewRelease first!'
            } else {
                File oldChannelFile = file("${app["channelPath"]}/old")
                if (!oldChannelFile.exists()) {
                    oldChannelFile.mkdirs()
                }
                // 对文件集合进行迭代
                channelFile.listFiles().each { File file ->
                    copy {
                        from file.absolutePath
                        into oldChannelFile.absolutePath
                        rename(file.name, "app-${file.name}")
                    }
                }
                println 'packers===packersOldRelease sucess'
            }
        }
    }
    
    /**
     *  加固后,打新版本的渠道包时,同时生成老版本的渠道包
     */
    task packersRelease {
        group 'packers'
        dependsOn packersNewRelease
        dependsOn packersOldRelease
        packersOldRelease.mustRunAfter(packersNewRelease)
        doLast {
            println "packers===packersRelease finished"
        }
    }
    
    /**
     *  对于release apk 进行360加固
     */
    def packers360(File releaseApk) {
        println 'packers===beginning 360 jiagu'
        def packersFile = file(app["packersPath"])
        if (!packersFile.exists()) {
            packersFile.mkdir()
        }
        exec {
            // 登录360加固保
            executable = 'java'
            args = ['-jar', packers["jarPath"], '-login', packers["account"], packers["password"]]
            println 'packers===import 360 login'
        }
        exec {
            // 导入签名信息
            executable = 'java'
            args = ['-jar', packers["jarPath"], '-importsign', signing["storeFile"],
                    signing["storePassword"], signing["keyAlias"], signing["keyPassword"]]
            println 'packers===import 360 sign'
        }
        exec {
            // 查看360加固签名信息
            executable = 'java'
            args = ['-jar', packers["jarPath"], '-showsign']
            println 'packers===show 360 sign'
        }
        exec {
            // 初始化加固服务配置,后面可不带参数
            executable = 'java'
            args = ['-jar', packers["jarPath"], '-config']
            println 'packers===init 360 services'
        }
        exec {
            // 执行加固
            executable = 'java'
            args = ['-jar', packers["jarPath"], '-jiagu', releaseApk.absolutePath, app["packersPath"], '-autosign']
            println 'packers===excute 360 jiagu'
        }
        println 'packers===360 jiagu finished'
        println "packers===360 jiagu path ${app["packersPath"]}"
    }
    
    /**
     * 自动下载360加固保,也可以自己下载然后放到根目录
     */
    def download360jiagu() {
        // 下载360压缩包
        File zipFile = file(packers["zipPath"])
        if (!zipFile.exists()) {
            if (!zipFile.parentFile.exists()) {
                zipFile.parentFile.mkdirs()
                println("packers===create parentFile jiagu ${zipFile.parentFile.absolutePath}")
            }
            // 加固保的下载地址
            def downloadUrl = isWindows() ? packers["jiagubao_windows"] : packers["jiagubao_mac"]
            // mac自带curl命令 windows需要下载curl安装
            def cmd = "curl -o ${packers["zipPath"]} ${downloadUrl}"
            println cmd
            cmd.execute().waitForProcessOutput(System.out, System.err)
        }
        File unzipFile = file(packers["unzipPath"])
        if (!unzipFile.exists()) {
            //解压 Zip 文件
            ant.unzip(src: packers["zipPath"], dest: packers["unzipPath"], encoding: "GBK")
            println 'packers===unzip 360jiagu'
            //将解压后的文件开启读写权限,防止执行 Jar 文件没有权限执行,windows需要自己手动改
            if (!isWindows()) {
                def cmd = "chmod -R 777 ${packers["unzipPath"]}"
                println cmd
                cmd.execute().waitForProcessOutput(System.out, System.err)
            }
        }
    }
    
    /**
     * 腾讯channel重新构建渠道包
     */
    def reBuildChannel() {
        File channelFile = file("${app["channelPath"]}/new")
        if (!channelFile.exists()) {
            channelFile.mkdirs()
        }
        def cmd = "java -jar ${app["vasDollyPath"]} put -c ${"../channel.txt"} ${outputpackersApk()} ${channelFile.absolutePath}"
        println cmd
        cmd.execute().waitForProcessOutput(System.out, System.err)
        println 'packers===excute VasDolly reBuildChannel'
    }
    
    /**
     *  是否是windows系统
     * @return
     */
    static Boolean isWindows() {
        return System.properties['os.name'].contains('Windows')
    }
    
    /**
     * 寻找本地的release  apk
     * @return true
     */
    def deleteFile() {
        delete app["channelPath"]
        delete app["packersPath"]
        println 'packers===delete all file'
    }
    
    /**
     * 首先打一个release包,然后找到当前的文件进行加固
     * @return releaseApk
     */
    def findReleaseApk() {
        def apkDir = file(app["releasePath"])
        File releaseApk = apkDir.listFiles().find { it.isFile() && it.name.endsWith(".apk") }
        println "packers===find release apk ${releaseApk.name}"
        return releaseApk
    }
    /**
     *  加固输出并且重新命名
     * @return packersApk
     */
    def outputpackersApk() {
        File oldApkDir = file(app["packersPath"])
        File oldApk = oldApkDir.listFiles().find { it.isFile() && it.name.contains("jiagu") }
        println "packers===output pacckers sourceApk ${oldApk.name}"
        copy {
            from app["packersPath"] + File.separator + oldApk.name
            into app["packersPath"]
            rename(oldApk.name, "release.apk")
            println 'packers===output pacckers renameApk release.apk'
        }
        File newApk = oldApkDir.listFiles().find { it.isFile() && it.name.equals("release.apk") }
        println "packers===output packers renameApk${newApk.absolutePath}"
        return newApk.absolutePath
    }
    
    

    结语

    本篇文章分享是基于360加固与腾讯VasDolly多渠道打包的自动化实践,提供的只是一种方式,不限于这两个平台,其他平台无非也就是更换一下加固与多渠道打包的命令,喜欢这篇gradle自动化加固与多渠道打包就随手点个赞吧,你的点赞是我写作的动力!

    相关文章

      网友评论

        本文标题:Gradle实现自动化加固与多渠道打包

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