在不将密钥库信息放入build.gradle文件的情况下签署APK

197

我正在尝试设置签名流程,使得密钥库、密码和密钥密码不会存储在项目的build.gradle文件中。

目前我的build.gradle文件中有以下内容:

android {
    ...
    signingConfigs {
        release {
            storeFile file("my.keystore")
            storePassword "store_password"
            keyAlias "my_key_alias"
            keyPassword "key_password"
        }
    }

    buildTypes {
        release {
            signingConfig signingConfigs.release            
        }
    }
}

它的功能完美,但我不能storePasswordkeyPassword的值放在我的存储库中。我也不想把storeFilekeyAlias放在那里。

是否有一种方法可以修改build.gradle文件,以便它可以从某些外部来源(例如仅驻留在我的计算机上的文件)获取密码?

当然,修改后的build.gradle文件应该在任何其他计算机上可用(即使该计算机没有访问密码)。

我正在使用Android Studio,在Mac OS X Mavericks上使用,如果这很重要。


当然,修改后的build.gradle应该可以在任何其他计算机上使用(即使计算机没有访问密码)。如果数据不在build.gradle中,则必须有其他东西来代替build.gradle,无论是环境变量的调整(根据一个答案),属性文件(根据另一个答案)还是其他一些手段。如果您不愿意在build.gradle之外拥有任何东西,那么根据定义,所有签名信息都必须buid.gradle内部。 - CommonsWare
2
@CommonsWare 您说得对。虽然我没有明确说明我想要在build.gradle中严格限制任何内容,但我确实说过build.gradle可以从某些外部来源(比如仅驻留在我的计算机上的文件)获取密码。 - Bobrovsky
可能是重复的问题:如何使用Gradle创建签名发布APK文件? - user2768
我已将其标记为重复问题,链接为https://dev59.com/HWMl5IYBdhLWcg3wiXZ7,基于http://meta.stackoverflow.com/questions/311044/internal-link-only-answers-are-advisable/。 - user2768
15个回答

146

或者,如果你想以更类似于自动生成的gradle代码的方式应用Scott Barta的答案,你可以在项目根目录中创建一个keystore.properties文件:

storePassword=my.keystore
keyPassword=key_password
keyAlias=my_key_alias
storeFile=store_file  

并修改您的gradle代码为:

// Load keystore
def keystorePropertiesFile = rootProject.file("keystore.properties");
def keystoreProperties = new Properties()
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))

...

android{

    ...

    signingConfigs {
        release {
            storeFile file(keystoreProperties['storeFile'])
            storePassword keystoreProperties['storePassword']
            keyAlias keystoreProperties['keyAlias']
            keyPassword keystoreProperties['keyPassword']
        }
    }

    ...

}
你可以将该属性文件存储在模块的根目录中,此时省略 rootProject 即可。你还可以修改此代码,以便为不同的密钥库和密钥别名创建多个属性集。

10
运行良好。我使用if (keystorePropertiesFile.exists())来确保在尝试获取属性和签名之前文件存在。 - Joshua Pinter
不要忘记在 keystore.properties 文件的末尾添加 .txt 扩展名。 - Levon Petrosyan
18
keystore.properties文件不需要使用.txt扩展名。 - Matt Zukowski
2
看起来此信息已被添加在这里 - https://developer.android.com/studio/publish/app-signing#secure-shared-keystore - Vadim Kotov

142

Groovy非常好的一点是可以自由混合Java代码,并且使用java.util.Properties读取键值文件非常容易。也许还有一种更符合Groovy惯用法的更简单的方法,但Java仍然相当简单。

创建一个keystore.properties文件(在本例中,在项目的根目录中与settings.gradle旁边,但您可以选择将其放在任何地方):

storePassword=...
keyPassword=...
keyAlias=...
storeFile=...

将以下内容添加到您的build.gradle中:
allprojects {
    afterEvaluate { project ->
        def propsFile = rootProject.file('keystore.properties')
        def configName = 'release'

        if (propsFile.exists() && android.signingConfigs.hasProperty(configName)) {
            def props = new Properties()
            props.load(new FileInputStream(propsFile))
            android.signingConfigs[configName].storeFile = file(props['storeFile'])
            android.signingConfigs[configName].storePassword = props['storePassword']
            android.signingConfigs[configName].keyAlias = props['keyAlias']
            android.signingConfigs[configName].keyPassword = props['keyPassword']
        }
    }
}

34
我需要从我的keystore.properties文件中删除引号。 - Jacob Tabak
6
在0.9.+版本的插件中,它没有为我生成签名版本。那么我应该如何处理signingConfigs块和buildTypes.release.signingConfig项?是将它们删除吗? - Fernando Gallego
6
构建时出现错误 "Error:(24, 0) Could not find property 'android' on root project 'RootProjectName'",其中第 24 行是 if 块的行。在根 build.gradle 中添加 "apply plugin: 'com.android.application'" 也无法解决构建失败的问题。我做错了什么? - PhilLab
2
与ferdy182不同,我无法生成对齐的apk。正如Xavier Ducrohet所说,ZipAlign必须在签名之后进行。将上述代码放入signingConfigs.release块中可以解决我的问题。这里是代码示例。 - Weekend
2
这在2018年不起作用。这必须被弃用了吗?一直抛出错误,指出“无法为根项目获取未知属性'android'”。 - Neon Warge
显示剩余9条评论

42

最简单的方法是创建一个 ~/.gradle/gradle.properties 文件。

ANDROID_STORE_PASSWORD=hunter2
ANDROID_KEY_PASSWORD=hunter2

然后你的 build.gradle 文件可以像这样:

android {
    signingConfigs {
        release {
            storeFile file('yourfile.keystore')
            storePassword ANDROID_STORE_PASSWORD
            keyAlias 'youralias'
            keyPassword ANDROID_KEY_PASSWORD
        }
    }
    buildTypes {
        release {
            signingConfig signingConfigs.release
        }
    }
}

2
我应该在gitignore中忽略~/.gradle/gradle.properties吗? - vzhen
完整的指令也在React Native文档中。 - Pencilcheck
1
@vzhen 不行,因为该文件不在你的项目目录中。 - Alexander Suraphel
1
不管你使用什么字符,都不要在~/.gradle/gradle.properties中引用这些字符串。 - duanev
它如何安全?gradle.properties也可以被访问。 - mohit arora
“~/.gradle/gradle.properties” 是您的主目录,而不是项目目录中的 “gradle.properties”,因此无法将其推送到您的存储库中。这是两个不同的文件。 - Piotr Wieleba

25

阅读了几个链接之后:

http://blog.macromates.com/2006/keychain-access-from-shell/ https://www.thoughtworks.com/en-gb/insights/blog/signing-open-source-android-apps-without-disclosing-passwords

由于您正在使用Mac OSX,您可以使用钥匙串访问来存储您的密码。

How to add password in Keychain Access

然后在你的gradle脚本中:
/* Get password from Mac OSX Keychain */
def getPassword(String currentUser, String keyChain) {
    def stdout = new ByteArrayOutputStream()
    def stderr = new ByteArrayOutputStream()
    exec {
        commandLine 'security', '-q', 'find-generic-password', '-a', currentUser, '-gl', keyChain
        standardOutput = stdout
        errorOutput = stderr
        ignoreExitValue true
    }
    //noinspection GroovyAssignabilityCheck
    (stderr.toString().trim() =~ /password: '(.*)'/)[0][1]
}

使用方法如下:

getPassword(currentUser, "Android_Store_Password")

/* Plugins */
apply plugin: 'com.android.application'

/* Variables */
ext.currentUser = System.getenv("USER")
ext.userHome = System.getProperty("user.home")
ext.keystorePath = 'KEY_STORE_PATH'

/* Signing Configs */
android {  
    signingConfigs {
        release {
            storeFile file(userHome + keystorePath + project.name)
            storePassword getPassword(currentUser, "ANDROID_STORE_PASSWORD")
            keyAlias 'jaredburrows'
            keyPassword getPassword(currentUser, "ANDROID_KEY_PASSWORD")
        }
    }
    
    buildTypes {
        release {
            signingConfig signingConfigs.release
        }
    }
}

2
尽管你的答案只适用于Mac OSX,但我非常喜欢它!请注意,你提供的第二个链接包含了其他平台的备份解决方案,以防有人需要实现多平台支持。 - Delblanco
请您能否提供适用于Linux和Windows的相同解决方案?谢谢。 - Jay Mungara
感谢@Jared Burrows!你可能想知道现在你可以传递“-w”而不是“-g”,只有密码会被输出。 - Brad Turek

22

这是我做事的方式。使用环境变量。

  signingConfigs {
    release {
        storeFile file(System.getenv("KEYSTORE"))
        storePassword System.getenv("KEYSTORE_PASSWORD")
        keyAlias System.getenv("KEY_ALIAS")
        keyPassword System.getenv("KEY_PASSWORD")
    }

5
不幸的是,这需要为每个项目在每台计算机上创建系统环境。否则,我会收到以下错误消息:“路径和baseDir都不能为空或为空字符串。路径='null'”。 - Bobrovsky
1
@Bobrovsky 我知道这个问题已经有答案了,但是你可以使用系统环境变量或者gradle.properties文件。你可能想要使用gradle.properties文件,它可以用于多个项目。 - Jared Burrows
3
除非您从命令行运行Android Studio,否则此方法无法在MacOSX上运行。 - Henrique de Sousa
我同意以上所有内容。我的配置也是一样的,你无法在Android Studio中编译它。你需要从命令行运行才能使其工作。我正在寻找更好的方法,这样我就不必在Android Studio中运行时注释这些行了。 - Sayooj Valsan
@Bobrovsky:它在Windows上可以工作吗?我们需要在系统环境中提到所有这些吗? - DKV
如何在zshrc中设置环境变量?我尝试了以下方法,但不幸的是没有抛出任何错误:export KEYSTORE_PASS="password" export ALIAS_NAME="password" export ALIAS_PASS="password"错误信息:
  • 出现了什么问题: 执行任务':app:packageRelease'失败。
    在执行com.android.build.gradle.internal.tasks.Workers$ActionFacade时发生故障 SigningConfig“release”缺少必需属性“storePassword”。
- Chandru

20

可以通过命令行构建/签名任何现有的Android Studio Gradle项目,无需编辑任何文件。这使得在将项目存储在版本控制中时非常方便,同时保持密钥和密码的分离,不会出现在build.gradle文件中:

./gradlew assembleRelease -Pandroid.injected.signing.store.file=$KEYFILE -Pandroid.injected.signing.store.password=$STORE_PASSWORD -Pandroid.injected.signing.key.alias=$KEY_ALIAS -Pandroid.injected.signing.key.password=$KEY_PASSWORD

1
还有相关的:android.injected.signing.v1-enabledandroid.injected.signing.v2-enabled。来源:https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-master-dev:build-system/builder-model/src/main/java/com/android/builder/model/AndroidProject.java;l=88 - Irfan Latif

15

如果你想把你的凭据放在外部的JSON文件中,并从 gradle 读取,这是我所做的:

my_project/credentials.json:

{
    "android": {
        "storeFile": "/path/to/acuity.jks",
        "storePassword": "your_store_password",
        "keyAlias": "your_android_alias",
        "keyPassword": "your_key_password"
    }
}

我的项目/android/app/build.gradle

// ...
signingConfigs {
        release {

            def credsFilePath = file("../../credentials.json").toString()
            def credsFile = new File(credsFilePath, "").getText('UTF-8')
            def json = new groovy.json.JsonSlurper().parseText(credsFile)
            storeFile file(json.android.storeFile)
            storePassword = json.android.storePassword
            keyAlias = json.android.keyAlias
            keyPassword = json.android.keyPassword
        }
        ...
        buildTypes {
            release {
                signingConfig signingConfigs.release //I added this
                // ...
            }
        }
    }
// ...
}
我选择使用 .json 文件类型而不是 .properties 文件类型(如被接受的答案中所述),是因为我还想将其他数据(我需要的其他自定义属性)存储到同一个文件中 (my_project/credentials.json),并且仍然希望 gradle 从该文件中解析签名信息。

对我来说,这似乎是最好的解决方案。 - Aspiring Dev

9

所接受的答案使用文件来控制位于项目相同根目录下的APK签名时要使用哪个密钥库。当我们使用vcsGit这样的工具时,如果忘记将属性文件添加到忽略列表中,可能会导致泄露密码。问题仍然存在。

相反,我们应该在项目外部创建属性文件,通过使用gradle.properties文件将其放在外部。

以下是步骤:

1.编辑或创建位于根项目上的gradle.properties文件并添加以下代码,记得使用自己的路径进行编辑:

AndroidProject.signing=/your/path/androidproject.properties  

2. 在/your/path/中创建androidproject.properties文件并添加以下代码,不要忘记将/your/path/to/android.keystore更改为您的密钥库路径:

STORE_FILE=/your/path/to/android.keystore  
STORE_PASSWORD=yourstorepassword  
KEY_ALIAS=yourkeyalias  
KEY_PASSWORD=yourkeypassword  

3.在您的应用程序模块 build.gradle 文件中(不是项目根 build.gradle),如果不存在,请添加以下代码或进行调整:

signingConfigs {  
     release  
   }  
   buildTypes {  
   debug {  
     debuggable true  
   }  
   release {  
     minifyEnabled true  
     proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'  
     signingConfig signingConfigs.release  
   }  
 }  

4.在步骤3的代码下面添加以下代码:

if (project.hasProperty("AndroidProject.signing")  
     && new File(project.property("AndroidProject.signing").toString()).exists()) {  
     def Properties props = new Properties()  
     def propFile = new File(project.property("AndroidProject.signing").toString())  
     if(propFile.canRead()) {  
      props.load(new FileInputStream(propFile))  
      if (props!=null && props.containsKey('STORE_FILE') && props.containsKey('STORE_PASSWORD') &&  
         props.containsKey('KEY_ALIAS') && props.containsKey('KEY_PASSWORD')) {  
         android.signingConfigs.release.storeFile = file(props['STORE_FILE'])  
         android.signingConfigs.release.storePassword = props['STORE_PASSWORD']  
         android.signingConfigs.release.keyAlias = props['KEY_ALIAS']  
         android.signingConfigs.release.keyPassword = props['KEY_PASSWORD']  
      } else {  
         println 'androidproject.properties found but some entries are missing'  
         android.buildTypes.release.signingConfig = null  
      }  
     } else {  
            println 'androidproject.properties file not found'  
          android.buildTypes.release.signingConfig = null  
     }  
   }  

这段代码将在第一步中的gradle.properties文件中搜索AndroidProject.signing属性。如果找到该属性,它将把属性值翻译为androidproject.properties的文件路径,该文件是我们在第二步中创建的。然后,其中的所有属性值都将用作我们的build.gradle的签名配置。
现在我们不再需要担心暴露密钥库密码的风险了。
阅读更多:在build.gradle中签署Android apk而无需放置密钥库信息

这对我来说很好用。只是想知道为什么他们使用storeFile file(System.getenv("KEYSTORE"))。 - DKV

8
这个问题已经收到了许多有价值的答案,但我想分享我的代码,这可能对库维护人员很有用,因为它可以保持原始的build.gradle非常干净。
我在模块目录中添加了一个gitignore文件夹。它看起来像这样:
/signing
    /keystore.jks
    /signing.gradle
    /signing.properties

keystore.jkssigning.properties应该很容易理解。而signing.gradle的内容如下:

def propsFile = file('signing/signing.properties')
def buildType = "release"

if (!propsFile.exists()) throw new IllegalStateException("signing/signing.properties file missing")

def props = new Properties()
props.load(new FileInputStream(propsFile))

def keystoreFile = file("signing/keystore.jks")
if (!keystoreFile.exists()) throw new IllegalStateException("signing/keystore.jks file missing")

android.signingConfigs.create(buildType, {
    storeFile = keystoreFile
    storePassword = props['storePassword']
    keyAlias = props['keyAlias']
    keyPassword = props['keyPassword']
})

android.buildTypes[buildType].signingConfig = android.signingConfigs[buildType]

原始的 build.gradle 文件如下:

apply plugin: 'com.android.application'
if (project.file('signing/signing.gradle').exists()) {
    apply from: 'signing/signing.gradle'
}

android {
    compileSdkVersion 27
    defaultConfig {
        applicationId ...
    }
}

dependencies {
    implementation ...
}

如您所见,如果用户可以访问有效的 signing 目录,则无需指定 buildTypes,他只需将其放在模块中就可以构建有效的已签名发布应用程序,否则它将正常工作,不需要任何额外操作。


我真的很喜欢这个解决方案。但是请注意,apply from 应该放在 android 块之后。 - mgray88

5

参考https://dev59.com/r2Ij5IYBdhLWcg3wKiLs#33218300并进行了一些改进

优点:

  • 密钥库属性(文件名、别名和密码)存储在git之外
  • 如果没有密钥库文件或keystore.properties,调试模式仍可正常工作
  • 密钥库及其属性位于根目录旁边

在项目的根目录下(或在React Native中的android文件夹中):

  • 添加生成的密钥库文件,如:app.keystore
  • 创建一个名为keystore.properties的文件
  • 将这两个文件添加到.gitignore

keystore.properties中添加以下内容:

STORE_FILE=app.keystore
KEY_ALIAS=app_alias
STORE_PASSWORD=your_password
KEY_PASSWORD=your_password

app/build.gradle 中添加:
// Load keystore
def keystoreProperties = new Properties()
try {
    def keystorePropertiesFile = rootProject.file("keystore.properties");
    keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
} catch(IOException e) {
    // We don't have release keys, ignoring
}

...

android {
  ...
  signingConfigs {
    release {
      if (keystoreProperties['STORE_FILE']) {
        storeFile rootProject.file(keystoreProperties['STORE_FILE'])
        storePassword keystoreProperties['STORE_PASSWORD']
        keyAlias keystoreProperties['KEY_ALIAS']
        keyPassword keystoreProperties['KEY_PASSWORD']
      }
    }
  }
  buildTypes {
    release {
      signingConfig signingConfigs.release
    }
  }
}

PS: 欢迎进行编辑以改进groovy逻辑


我只是不得不重新构建项目,然后Android Studio才停止抱怨。 - Daniel
这怎么安全?凭据可以从keystore.properties文件中读取。 - mohit arora

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接