diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 3215f92..50fc2c5 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -83,10 +83,52 @@ jobs: run: ./gradlew assembleDebug --no-daemon working-directory: ./ + # 配置签名(仅在发布时) + - name: 配置Release签名 + if: startsWith(github.ref, 'refs/tags/') + run: | + # 验证所有必需的签名凭据是否存在 + if [ -z "${{ secrets.KEYSTORE_BASE64 }}" ] || \ + [ -z "${{ secrets.KEYSTORE_PASSWORD }}" ] || \ + [ -z "${{ secrets.KEY_ALIAS }}" ] || \ + [ -z "${{ secrets.KEY_PASSWORD }}" ]; then + echo "Error: Missing required signing secrets" + echo "Please configure: KEYSTORE_BASE64, KEYSTORE_PASSWORD, KEY_ALIAS, KEY_PASSWORD" + exit 1 + fi + + # 创建临时目录用于存放签名文件 + mkdir -p /tmp/signing + chmod 700 /tmp/signing + + # 从GitHub Secrets解码keystore文件到临时目录 + echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > /tmp/signing/release.keystore + chmod 600 /tmp/signing/release.keystore + + # 验证keystore文件是否成功创建 + if [ ! -f /tmp/signing/release.keystore ] || [ ! -s /tmp/signing/release.keystore ]; then + echo "Error: Failed to create keystore file or file is empty" + exit 1 + fi + + echo "Keystore file created successfully in secure temporary directory" + - name: Build Release APK if: startsWith(github.ref, 'refs/tags/') run: ./gradlew assembleRelease --no-daemon working-directory: ./ + env: + KEYSTORE_FILE: /tmp/signing/release.keystore + KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }} + KEY_ALIAS: ${{ secrets.KEY_ALIAS }} + KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }} + + - name: 清理签名密钥文件 + if: always() && startsWith(github.ref, 'refs/tags/') + run: | + # 删除临时的keystore文件和目录以防止泄露 + rm -rf /tmp/signing + echo "Keystore file and temporary directory cleaned up" - name: 重命名APK文件 run: | @@ -94,7 +136,11 @@ jobs: if [ -f app/build/outputs/apk/debug/app-debug.apk ]; then cp app/build/outputs/apk/debug/app-debug.apk app/build/outputs/apk/debug/SCE-API-${VERSION}-debug.apk fi - if [ -f app/build/outputs/apk/release/app-release-unsigned.apk ]; then + # Release APK现在是已签名的 + if [ -f app/build/outputs/apk/release/app-release.apk ]; then + cp app/build/outputs/apk/release/app-release.apk app/build/outputs/apk/release/SCE-API-${VERSION}-release.apk + elif [ -f app/build/outputs/apk/release/app-release-unsigned.apk ]; then + # 如果签名失败,仍然复制未签名版本 cp app/build/outputs/apk/release/app-release-unsigned.apk app/build/outputs/apk/release/SCE-API-${VERSION}-release.apk fi @@ -127,7 +173,7 @@ jobs: ### 下载说明 - **SCE-API-${{ steps.version.outputs.version }}-debug.apk**: 调试版本,包含调试信息 - - **SCE-API-${{ steps.version.outputs.version }}-release.apk**: 发布版本(未签名) + - **SCE-API-${{ steps.version.outputs.version }}-release.apk**: 发布版本(已签名) ### 安装要求 - Android 7.0 (API 24) 或更高版本 diff --git a/.gitignore b/.gitignore index 5a8aa5e..16c81a0 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,9 @@ local.properties # Local configuration files local.properties + +# Keystore files (签名密钥文件) +*.jks +*.keystore +keystore.properties +release.keystore diff --git a/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/.gradle/buildOutputCleanup/buildOutputCleanup.lock deleted file mode 100644 index 9ffc7cb..0000000 Binary files a/.gradle/buildOutputCleanup/buildOutputCleanup.lock and /dev/null differ diff --git a/README.md b/README.md index 3d9baf5..dd011c0 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,59 @@ The app automatically grants WebView permission requests for: - versionCode 必须单调递增 - 建议 versionCode 使用公式:`主版本*10000 + 次版本*100 + 修订号`(例如 1.0.1 -> 10001) +### Release APK 签名配置 + +项目使用固定签名对 Release APK 进行签名。签名配置通过 GitHub Secrets 管理: + +#### 配置 GitHub Secrets + +在仓库的 Settings > Secrets and variables > Actions 中添加以下 secrets: + +1. **KEYSTORE_BASE64**: 密钥库文件的 base64 编码 +2. **KEYSTORE_PASSWORD**: 密钥库密码 +3. **KEY_ALIAS**: 密钥别名 +4. **KEY_PASSWORD**: 密钥密码 + +#### 生成密钥库文件 + +如果还没有密钥库文件,可以使用以下命令生成: + +```bash +keytool -genkey -v -keystore release.keystore -alias release \ + -keyalg RSA -keysize 2048 -validity 10000 +``` + +#### 将密钥库编码为 base64 + +```bash +# Linux/macOS +base64 release.keystore | tr -d '\n' > keystore.base64.txt + +# Windows (PowerShell) +[Convert]::ToBase64String([IO.File]::ReadAllBytes("release.keystore")) | Out-File -Encoding ASCII keystore.base64.txt +``` + +然后将 `keystore.base64.txt` 的内容复制到 GitHub Secrets 的 `KEYSTORE_BASE64` 中。 + +#### 本地构建签名版本 + +本地构建需要创建 `keystore.properties` 文件(参考 `keystore.properties.template`): + +```properties +storeFile=release.keystore +storePassword=你的密钥库密码 +keyAlias=release +keyPassword=你的密钥密码 +``` + +然后运行: + +```bash +./gradlew assembleRelease +``` + +**注意**: `keystore.properties` 和 `*.keystore` 文件已添加到 `.gitignore`,不会被提交到仓库。 + ## Building ```bash diff --git a/SIGNING_SETUP.md b/SIGNING_SETUP.md new file mode 100644 index 0000000..d686e86 --- /dev/null +++ b/SIGNING_SETUP.md @@ -0,0 +1,158 @@ +# APK 签名配置指南 + +本文档说明如何为项目配置 APK 签名,以便自动构建已签名的 Release APK。 + +## 概述 + +项目使用固定的签名配置对 Release APK 进行签名。签名密钥通过 GitHub Secrets 安全管理,不会暴露在代码仓库中。 + +## 一、生成签名密钥库 + +### 1.1 使用 keytool 生成密钥库 + +在命令行中运行以下命令: + +```bash +keytool -genkey -v -keystore release.keystore -alias release \ + -keyalg RSA -keysize 2048 -validity 10000 +``` + +**参数说明:** +- `release.keystore`: 密钥库文件名 +- `release`: 密钥别名 +- `RSA`: 密钥算法 +- `2048`: 密钥长度 +- `10000`: 有效期(天数,约27年) + +### 1.2 填写密钥信息 + +命令会提示你输入以下信息: + +1. **密钥库密码**(keystore password):请使用强密码 +2. **密钥密码**(key password):可以与密钥库密码相同 +3. **姓名、组织等信息**:根据实际情况填写 + +**重要提示:** +- 请妥善保管密钥库文件和密码 +- 如果丢失密钥库,将无法更新已发布的应用 +- 建议将密钥库文件和密码存储在安全的地方(如密码管理器) + +## 二、配置 GitHub Secrets + +### 2.1 将密钥库编码为 base64 + +**Linux/macOS:** + +```bash +base64 release.keystore | tr -d '\n' > keystore.base64.txt +``` + +**Windows PowerShell:** + +```powershell +[Convert]::ToBase64String([IO.File]::ReadAllBytes("release.keystore")) | Out-File -Encoding ASCII keystore.base64.txt +``` + +### 2.2 在 GitHub 中添加 Secrets + +1. 打开仓库页面 +2. 点击 **Settings** > **Secrets and variables** > **Actions** +3. 点击 **New repository secret** 添加以下 secrets: + +| Secret 名称 | 值 | 说明 | +|------------|---|------| +| `KEYSTORE_BASE64` | `keystore.base64.txt` 的内容 | 密钥库文件的 base64 编码 | +| `KEYSTORE_PASSWORD` | 你设置的密钥库密码 | 密钥库密码 | +| `KEY_ALIAS` | `release` | 密钥别名(与生成时使用的一致) | +| `KEY_PASSWORD` | 你设置的密钥密码 | 密钥密码 | + +### 2.3 验证配置 + +配置完成后,推送一个新的 tag 来触发自动构建: + +```bash +git tag v1.0.1 +git push origin v1.0.1 +``` + +检查 GitHub Actions 工作流是否成功运行,并生成已签名的 Release APK。 + +## 三、本地构建已签名版本 + +如果需要在本地构建已签名的 Release APK: + +### 3.1 创建 keystore.properties 文件 + +在项目根目录创建 `keystore.properties` 文件(参考 `keystore.properties.template`): + +```properties +storeFile=release.keystore +storePassword=你的密钥库密码 +keyAlias=release +keyPassword=你的密钥密码 +``` + +### 3.2 将密钥库文件放到项目根目录 + +将 `release.keystore` 文件复制到项目根目录。 + +### 3.3 构建 Release APK + +```bash +./gradlew assembleRelease +``` + +构建完成后,已签名的 APK 位于: +``` +app/build/outputs/apk/release/app-release.apk +``` + +**注意:** `keystore.properties` 和 `*.keystore` 文件已添加到 `.gitignore`,不会被提交到仓库。 + +## 四、安全建议 + +1. **永远不要将密钥库文件或密码提交到代码仓库** +2. **定期备份密钥库文件**(存储在安全的位置) +3. **使用强密码**(至少16位,包含大小写字母、数字和特殊字符) +4. **限制对 GitHub Secrets 的访问权限** +5. **如果密钥库泄露,立即生成新的密钥库并重新发布应用** + +## 五、故障排查 + +### 5.1 签名失败 + +如果构建时出现签名错误,检查: + +1. GitHub Secrets 是否正确配置 +2. `KEYSTORE_BASE64` 是否包含完整的 base64 编码(没有换行符) +3. 密码和别名是否正确 +4. 密钥库文件是否有效 + +### 5.2 验证 APK 签名 + +使用以下命令验证 APK 是否已正确签名: + +```bash +# 查看签名信息 +keytool -printcert -jarfile app-release.apk + +# 或使用 apksigner +apksigner verify --verbose app-release.apk +``` + +### 5.3 本地构建时找不到密钥库 + +确保: +1. `keystore.properties` 文件存在且路径正确 +2. `release.keystore` 文件在 `storeFile` 指定的位置 +3. 文件权限允许读取 + +## 六、更新签名配置 + +如果需要更换签名密钥: + +1. 生成新的密钥库文件 +2. 更新 GitHub Secrets 中的所有相关值 +3. 注意:更换签名后,用户需要卸载旧版本才能安装新版本 + +**建议:** 除非绝对必要,否则不要更换签名密钥。 diff --git a/app/build.gradle b/app/build.gradle index c687ebf..0ba6186 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,10 +12,50 @@ android { versionCode 1 versionName "1.0.0" } + + signingConfigs { + release { + // 从环境变量或 keystore.properties 文件读取签名配置 + def keystorePropertiesFile = rootProject.file("keystore.properties") + if (keystorePropertiesFile.exists()) { + def keystoreProperties = new Properties() + keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) + + storeFile file(keystoreProperties['storeFile']) + storePassword keystoreProperties['storePassword'] + keyAlias keystoreProperties['keyAlias'] + keyPassword keystoreProperties['keyPassword'] + } else { + // CI环境使用环境变量(如果未设置,签名配置将为null) + def keystoreFile = System.getenv("KEYSTORE_FILE") + def keystorePassword = System.getenv("KEYSTORE_PASSWORD") + def keyAlias = System.getenv("KEY_ALIAS") + def keyPassword = System.getenv("KEY_PASSWORD") + + // 检查签名配置是否完整 + def hasCompleteSigningConfig = [keystoreFile, keystorePassword, keyAlias, keyPassword] + .every { it?.trim() } + + // 只有当所有环境变量都存在且非空时才配置签名 + if (hasCompleteSigningConfig) { + storeFile file(keystoreFile) + storePassword keystorePassword + keyAlias keyAlias + keyPassword keyPassword + } + // 如果环境变量未设置,签名配置保持为空(允许构建debug) + } + } + } + buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + // 只有当签名配置完整时才应用 + if (signingConfigs.release.storeFile != null) { + signingConfig signingConfigs.release + } } } compileOptions { diff --git a/keystore.properties.template b/keystore.properties.template new file mode 100644 index 0000000..00a5639 --- /dev/null +++ b/keystore.properties.template @@ -0,0 +1,14 @@ +# Android 签名配置模板 +# 复制此文件为 keystore.properties 并填写实际值 + +# 密钥库文件路径(相对于项目根目录) +storeFile=release.keystore + +# 密钥库密码 +storePassword=your_keystore_password + +# 密钥别名 +keyAlias=release + +# 密钥密码 +keyPassword=your_key_password