Skip to content

24解决打包痛点:如何统一管理Certificate和Profile?

一个优秀的 iOS 开发者所需要做的工作不仅仅是编写代码那么简单,还要管理证书(Certificates)和 Provisioning Profile、打包和签名 App、上架与分发等。你如果做过这些操作的话应该知道,单纯通过手工的方式来完成,每个步骤都需要花费大量的时间,而且十分容易出错。

那有没有什么办法能帮我们节省这些手工操作的时间呢?答案当然是肯定的。我们可以通过 fastlane 来自动化这些操作,所以,在这一讲中我们就来介绍下如何通过 fastlane 来统一管理证书和 Provisioning Profiles。

什么是证书和 Provisioning Profile

刚接触 iOS 的开发者可能都会有一个疑惑:为什么在 iOS 开发过程中需要管理私钥、证书、Provisioning Profile 以及设备列表等信息呢?

这是因为苹果要给 App 的终端用户提供安全和稳定的体验。而要达到这一效果,苹果就得要求所有开发者在用户安装之前必须为 App 进行打包和签名。有了这些签名,苹果就知道这些 App 到底是谁开发的,签名以后 App 是否被修改过。

这里的打包和签名操作就涉及私钥、证书和 Provisioning Profile 等组件,我们可以结合下面这张图看看这些组件之间的关系:

那这些组件到底都有什么作用呢?下面我们来分别说明下。

  • 苹果证书机构。世界上有好多证书机构(CA),但当我们通过 App Store Connect 发布 App 的时候,苹果公司只认它自己的证书机构。因为苹果证书机构归苹果公司所有,所以苹果公司对安装到设备上的所有 iOS App 都有最终的控制权。

  • 私钥。这是生成签名证书所需的私钥文件,通常是一个后缀名为 .p12 的文件。私钥是证明我们身份的唯一信息源,假如丢失了这个私钥,那其他人就能伪装成我们了,非常不安全。当我们手工生成证书时,会通过 Keychain Access 程序生成一个后缀为 .certSigningRequest 的 Certificate Signing Request 文件和私钥文件,然后把 .certSigningRequest 文件上传到苹果开发者网站,苹果公司就可以通过这个请求,并使用苹果证书机构来为我们发行一个证书。

  • 签名证书。签名证书是由苹果证书机构通过提供的 .certSigningRequest 文件所签发的,因此苹果公司知道这个证书的所有人。苹果公司会把这个证书作为签名主体。签名证书通常是一个后缀名为 .cer 的文件,该 .cer 文件包含了开发者 ID、团队 ID 和公钥信息。

  • 发布渠道。我们可以把 App 通过不同渠道发布出去,目前支持的渠道有 Development、Ad Hoc、Enterprise 和 App Store。当我们在 Xcode 上把 App 部署到设备进行 Debug 时,一般会使用 Development 渠道。当我们把 App 分发给内部测试用户时,可以使用 Ad Hoc 渠道。如果开发企业内的 App,可以使用 Enterprise 渠道来发布。而对于要上传到 App Store 的 App,就必须使用 App Store 渠道了。

  • App ID。每个 App 都有唯一的 ID。根据不同的用途,我们为 Moments App 建立了三个 App ID,分别用于开发与调试、内部测试和上架 App Store。

  • 设备列表。当我们通过 Ad Hoc 渠道来发布 App 时,要把需要安装 App 的设备添加到设备列表中,只有在设备列表中的设备才能安装 Ad Hoc 渠道的 App。

  • Provisioning Profile。有了证书以后,我们可以为不同的 App ID 以及不同的发布渠道来生成不同的 Provisioning profile,通常是一个后缀名为 .mobileprovision 或 .provisionprofile 的文件。该文件包含 App ID 所指向的 Entitlements 信息,以及发布渠道、团队 ID 和设备列表信息。我们通常为不同用途的 App 生成不同的 Provisioning Profile,例如我们为 Moments App 的 App Store 版本生成一个 Provisioning Profile,然后再为 Moments App 的 Internal 版本生成另外一个 Provisioning Profile。

搭建管理证书和 Provisioning Profile 的环境

假如你手工生成过私钥、证书和 Provisioning Profile 文件,并在苹果开发者网站上进行过上传、下载和安装的话,就知道这些操作过程有多麻烦。

  • 有些团队会为每个成员生成多个不同的证书来进行签名,这将导致大量证书和 Provisioning Profile 文件的出现,十分难管理。

  • 证书都是有期限的,每次延展期限都需要手工更新所有的 Provisioning Profiles。

  • 当添加新增设备时,也需要更新 Ad Hoc 的 Provisioning Profiles。

  • 当搭建 CI 的时候,又需要花大量时间来下载、安装私钥、证书和 Provisioning Profiles 文件。

那有没有什么办法来简化证书和 Provisioning Profiles 的管理工作呢?幸运的是fastlane 为我们提供了一个名叫 match 的 Action 来为整个团队统一管理并共享所有证书和 Provisioning Profile

下面我们就来看一下如何使用 fastlane 的 match Action 搭建所需的环境吧。

建 GitHub 私有 Repo

为了把证书共享给整个团队使用,fastlane match 需要把私钥和证书保存在云端的存储服务上。目前支持的云存储服务有亚马逊的 S3、谷歌云和微软的 Azure 等。但我推荐使用 GitHub 私有 Repo 来存储私钥和证书,因为 GitHub 私有 Repo 是免费的,而且有详细的修改历史。Moments App 的证书就保存在 GitHub 的私有 Repo 里面,下面我们讲一下如何搭建 GitHub 私有 Repo。

我们可以点击 GitHub 网站右上角的加号(+)按钮,然后选择 New repository 菜单来新建私有 Repo。因为该 Repo 用于签名,所以我会以"<项目名称>-codesign"的方式来命名,例如叫 moments-codesign。具体页面情况如下图所示:

这里需要注意:我们必须把 Repo 设置为 Private,因为该 Repo 保存了私钥等关键信息,一旦设置为 Public 的话,所有人都可以访问它了。

生成 GitHub Access Token

那怎样才能让整个团队都能访问这个私有 Repo 呢?答案是使用 GitHub Access Token。

我推荐的做法是为每一个 App 新建一个 GitHub 账户,例如新建一个叫作 momentsci 的账户,然后把该账户添加到私有 Repo 的贡献者列表里面。如下图所示:

这样子,momentsci 用户就能访问和更新该私有 Repo 了。

下一步是为 momentsci 用户生成 GitHub Access Token。当我们通过 momentsci 登录到 GitHub 以后,点击 Settings -> Developer settings -> Personal access tokens 来打开来配置页面,接着再点击 Generate new token 按钮,在 Note 输入框填写 Token 的用途,比如写上"用于 Moments App 的 CI",然后在 Select scopes 选上 repo,如下图所示:

因为我们选择了 Full controll of private repositories(能完全控制所有私有 Repo),所以使用该 Token 的应用程序(例如 fastlane)就有权限访问 momentsci 用户所能访问的所有 Repo,并且能 push commit 到这些 Repo 去。当我们点击 Generate token 按钮以后就生成一个如下图所示的 Token:

这里需要注意,我们一定要好好保存这个 Token,因为一旦关闭该页面以后就无法再从 GitHub 上找到该 Token 了。为了使得团队所有人都可以使用到这个 Token,我推荐把它存放在团队密码共享服务里面,目前比较流行的密码共享服务有 LastPass、OnePassword 等。

有了这个 Token 以后,我们还需要生成一个 BASE64 字符串提供给 fastlane 使用,命令如下:

java
$> echo -n your_github_username:your_personal_access_token | base64

不过需要把 your_github_username 替换为 GitHub 用户名,例如 momentsci 用户,然后把 your_personal_access_token 替换成刚才所生成的 Token。

接着就可以在 Shell 里把 BASE64 赋值给环境变量MATCH_GIT_BASIC_AUTHORIZATION,如下所示:

java
$> export MATCH_GIT_BASIC_AUTHORIZATION=<YOUR BASE64 KEY>

为了提高安全性,我们还可以配置环境变量MATCH_PASSWORD来加密私钥、证书和 Provisioning Profile 文件。但是需要注意:一定要记住这个密码,因为使用这些文件的机器都需要使用到该密码

java
$> export MATCH_PASSWORD=<YOUR MATCH PASSWORD>

生成 App Store Connect API Key

因为生成证书和 Provisioning Profile 的过程需要与苹果开发者网站进行交互,所以 fastlane 也需要具备访问苹果开发者网站的权限。

目前 fastlane 提供了几种办法来访问苹果开发者网站,例如,输入登录所需的用户名和密码等。但我推荐使用 App Store Connect API Key 的方式,因为 API Key 既能有效控制访问权限,也可以随时让该 Key 失效。

我们可以在 App Store Connect 网站上生成该 Key,在网站上选择 Users and Access -> Keys -> App Store Connect API,然后点击加号(+)来生成 Key,会弹出下面的输入框:

Key 的名称可以填写其用途,例如使用到 CI 上我们就填 "Moments CI",然后在 Access 里选择 App Manager。需要注意:必须选择 App Manager 以上的权限,因为使用 App Manager 以下的权限,fastlane 在执行过程中会出错。这是 fastlane 的一个已知的问题,假如以后解决了该问题,我们就可以选择 Developer 权限,原则上是该 Key 的 Access 权限越低就越安全。

当 Key 生成完毕后,我们需要把它保存起来,并在 Shell 里把该 Key 赋值给环境变量APP_STORE_CONNECT_API_CONTENT,如下所示:

java
$> export APP_STORE_CONNECT_API_CONTENT=<App Store Connect API>

到这里,管理证书和 Provisioning Profile 的环境就配置完了。配置的步骤虽然有点多,但是每个项目只需配置一次就好了,其他项目成员无须重复配置。为了进一步简化环境变量的赋值操作,我推荐在项目根目录下建立一个名叫 local.keys 的文件,然后把所有环境变量都放在该文件里面,如下所示:

java
APP_STORE_CONNECT_API_CONTENT=<App Store Connect API for an App Manager>
GITHUB_API_TOKEN=<GitHub API token for accessing the private repo for certificates and provisioning profiles>
MATCH_PASSWORD=<Password for certificates for App signing on GitHub private repo>

接着在根目录执行以下的命令:

java
$> source ./scripts/export_env.sh

这样就能把所有环境变量一次性导入当前的 Shell 里面,不过注意,这里需要使用source命令,否则环境变量只会导出到子 Shell 里面。

这里还需要提醒一下,因为我们不应该把机密信息上传到 Git 服务器上,所以该 local.keys 文件需要配置到 .gitignore 文件里面。

使用 fastlane 管理证书和 Provisioning Profile

有了上述的环境搭建与配置,我们就可以使用 fastlane 来统一管理证书和 Provisioning Profile 了。

生成证书和 Provisioning Profile

第一步是生成证书和 Provisioning Profile,每个项目也只需执行一次这样的操作。

为了简化,我把生成证书和 Profile 的操作都封装在create_new_profilesLane 里面,只需要执行bundle exec fastlane create_new_profiles命令即可,该 Lane 的具体代码如下:

ruby
desc "Create all new provisioning profiles managed by fastlane match"
lane :create_new_profiles do
  api_key = get_app_store_connect_api_key
  keychain_name = "TemporaryKeychain"
  keychain_password = "TemporaryKeychainPassword"
  create_keychain(
    name: keychain_name,
    password: keychain_password,
    default_keychain: false,
    timeout: 3600,
    unlock: true,
  )
  match(
    type: "adhoc",
    keychain_name: keychain_name,
    keychain_password: keychain_password,
    storage_mode: "git",
    git_url: "https://github.com/JakeLin/moments-codesign",
    app_identifier: "com.ibanimatable.moments.internal",
    team_id: "6HLFCRTYQU",
    api_key: api_key
  )
  match(
    type: "appstore",
    keychain_name: keychain_name,
    keychain_password: keychain_password,
    storage_mode: "git",
    git_url: "https://github.com/JakeLin/moments-codesign",
    app_identifier: "com.ibanimatable.moments",
    team_id: "6HLFCRTYQU",
    api_key: api_key
  )
end

该 Lane 主要由三部分组成。

第一部分是调用create_keychainAction 来生成 Keychain。因为 fastlane 所生成的私钥和证书都需要保存在 Keychain 里,所以我们要生成一个 Keychain 来保存它们。为了不影响默认的 Keychain,我们把false传递给default_keychain参数,表示生成的 Keychain 不是默认的 Keychain。

第二部分是通过指定 Ad Hoc 作为发布渠道来为 Internal App 生成证书和 Provisioning Profile,并把它们上传到 GitHub 私有 Repo,这样我们就能使用这个 Provisioning Profile 为 Internal App 进行签名和打包。

第三部分与第二部分非常类似,也是用于生成证书和 Provisioning Profile。不同的是它生成了发布渠道为 Appstore 类型的 Provisioning Profile,有了该 Provisioning Profile,我们就能为 AppStore 版本的 App 进行签名和打包。

你可能发现,我们调用了私有 Laneget_app_store_connect_api_key来获取api_key变量的值。该私有 Lane 的定义如下:

ruby
desc 'Get App Store Connect API key'
  private_lane :get_app_store_connect_api_key do
    key_content = ENV["APP_STORE_CONNECT_API_CONTENT"]
    api_key = app_store_connect_api_key(
      key_id: "D9B979RR69",
      issuer_id: "69a6de7b-13fb-47e3-e053-5b8c7c11a4d1",
      key_content: "-----BEGIN EC PRIVATE KEY-----\n" + key_content + "\n-----END EC PRIVATE KEY-----",
      duration: 1200,
      in_house: false
    )
    api_key 
  end

该私有 Lane 从环境变量中读取了APP_STORE_CONNECT_API_CONTENT的值,然后通过调用app_store_connect_api_keyAction 来获取临时的 App Store Connect API Key。其中,key_idissuer_id的值都可以在 App Store Connect 的 Keys 配置页面上找到。

如果你没有为 GitHub 配置全局的用户名和邮箱,那么在执行bundle exec fastlane create_new_profiles命令时可能会出错。你可以通过下面的命令来解决这个问题,在命令执行完之后还可通过git config --global --edit命令把这些配置删掉。

java
$> git config --global user.email "MomentsCI@lagou.com"
$> git config --global user.name "Moments CI"

create_new_profiles命令成功执行以后,你可以在私有 Repo 上看到两个新的文件夹,如下图所示:

其中,certs 文件夹用于保存私钥(.p12)和证书(.cer)文件,而 profiles 文件夹则用来保存 adhoc 和 appstore 两个 Provisioning Profile 文件

你也可以在苹果开发者网站查看新的证书文件:

同时还可以看到 Provisioning Profile 文件:

除此之外,你还可以在 Keychain App 里面找到新增的私钥和证书,如下图所示:

下载证书和 Provisioning Profile

一个项目只需要执行一次生成证书和 Provisioning Profile 的操作,其他团队成员可通过bundle exec fastlane download_profiles命令来下载证书和 Provisioning Profile。该 Lane 的代码如下:

ruby
desc "Download certificates and profiles"
lane :download_profiles do
  keychain_name = "TemporaryKeychain"
  keychain_password = "TemporaryKeychainPassword"
  create_keychain(
    name: keychain_name,
    password: keychain_password,
    default_keychain: false,
    timeout: 3600,
    unlock: true,
  )
  match(
    type: "adhoc",
    readonly: true,
    keychain_name: keychain_name,
    keychain_password: keychain_password,
    storage_mode: "git",
    git_url: "https://github.com/JakeLin/moments-codesign",
    app_identifier: "com.ibanimatable.moments.internal",
    team_id: "6HLFCRTYQU"
  )
  match(
    type: "appstore",
    readonly: true,
    keychain_name: keychain_name,
    keychain_password: keychain_password,
    storage_mode: "git",
    git_url: "https://github.com/JakeLin/moments-codesign",
    app_identifier: "com.ibanimatable.moments",
    team_id: "6HLFCRTYQU"
  )
end

你会发现download_profilescreate_new_profiles两个 Lane 的实现非常类似,都是由三部分组成,包括生成 Keychain、下载 Internal App 的证书和 Provisioning Profile 以及 AppStore 版本 App 的证书和 Provisioning Profile。不同的地方是download_profilesLane 不需要更新 App Store Connect,所以无须使用 App Store Connect 的 API Key;并且download_profilesLane 也不需要更新私有 Repo 的内容,所以在调用matchAction 时,我们会把true传递给readonly参数。

新增设备

当我们通过 Ad Hoc 的方式来分发 App 时,必须把需要安装 App 的设备 ID 都添加到设备列表里面,你可以在苹果开发者网站的"Certificates, Identifiers & Profiles"的 Devices 下查看所有设备信息。如下图所示:

但是手工更新设备列表的操作比较麻烦,而且更新完以后还需要再更新 Provisioning Profile。幸运的是 fastlane 能帮我们自动化这些操作,我们把这些操作都封装在add_deviceLane 里面,具体代码如下:

ruby
desc "Add a new device to provisioning profile"
lane :add_device do |options|
  name = options[:name]
  udid = options[:udid]
  # Add to App Store Connect
  api_key = get_app_store_connect_api_key
  register_device(
    name: name,
    udid: udid,
    team_id: "6HLFCRTYQU",
    api_key: api_key
  )
  # Update the profiles to Git private repo
  match(
    type: "adhoc",
    force: true,
    storage_mode: "git",
    git_url: "https://github.com/JakeLin/moments-codesign",
    app_identifier: "com.ibanimatable.moments.internal",
    team_id: "6HLFCRTYQU",
    api_key: api_key
  )
end

首先调用register_deviceAction 把设备更新到苹果开发者网站上的设备列表里面,然后把true传递给force参数来调用matchAction,这个操作能强制更新 Ad Hoc 的 Provisioning Profile 并上传到私有 Repo 里。这样当其他机器在调用download_profiles命令的时候,就能获取最新的 Provisioning Profile 了。

总结

在这一讲中,我们讲述了如何使用 fastlane 的 match Action 来帮我们统一管理签名和打包所需的私钥、证书和 Provisioning Profile 文件。

在实际项目中,我们只需要一次性完成搭建的任务 ,例如生成私钥 Repo、导出 Github Access Token 和 App Store Connect API Key,以及调用create_new_profiles来生成所需的证书和 Provisioning Profile。其他团队成员和 CI 服务器就可以通过调用download_profiles来下载证书。当需要为 Ad Hoc 发布渠道添加新设备时,只需要执行add_device即可。

有了download_profilesadd_device等命令,团队里任何人都可以轻松地下载打包和签名所需的私钥、证书和 Provisioning Profile 文件,无须手工使用 Keychain Access 程序来管理私钥,无须登录到苹果开发者网站下载和安装证书,无须到苹果开发者网站上手工添加设备,无须重新生成 Provisioning Profile 等。这样能减少大量无聊而且容易出错的手工操作工作,让我们把有效的时间都花在功能开发与迭代上。

思考题

在 Moments App 中,我们为 Debug Target 使用了 Automatically manage signing 的方式来管理证书和 Provisioning Profile。这里请你思考一下,这样做有什么好处呢?

可以把你的答案写到留言区哦。下一讲我将介绍如何使用自动化构建来解决大量重复性工作的问题。

源码地址

Fastfile 文件地址:https://github.com/lagoueduCol/iOS-linyongjian/blob/main/fastlane/Fastfile#L72-L207