发行平台如何实现iOS证书更新自动化的工作流

5,235 阅读18分钟

发行平台如何实现iOS证书更新自动化的工作流

背景

在日常工作中,我们通过会遇到打包前才发现出包证书过期了的问题。虽然苹果证书,是时隔一年才会过期。但是对于发行公司来说几乎每个月都有新游戏发行意味着证书在下一年的每个月都可能遇到证书过期的情况。而我们敬业的研发CP大大,更新版本时通常打包会不定时,比如等我们下班后才出得来包。结果打包时发现证书过期了,就会火速回联系我方技术支撑要求发过去新的证书。基于这样一个需求背景,如何做到提前提醒,避免这种后知后觉的事情发生呢? 就此衍生出一个将证书管理自动化的项目。

如何将更新打包证书的更新自动化呢?苹果人家早就想到了咱需要用到这一点了,提供AppStore Connect API支持我们将工作流程自动化。只是需要我们自己将此流程串起来。此开放的API允许我们遵循协议,通过对接就可以将在AppStore Connect上的日常任务自动化,以提高工作效率和减少错误。

针对以上问题,我们创建了一个后台服务,定期查询苹果后台的证书。一旦发现某个证书快过期,就提前将证书和描述文件做一次更新编辑,再下载下来存好。然后,通过推送的方式告知对应负责证书管理的同事。那她就可以直接在手机上操作,将新的证书转发给研发了。

最初我对其他语言不是很熟悉,经过了一段时间的调研后发现,想要实现这样的功能后台服务是避免不了的,最后决定仍用自己熟悉的Swift相关框架和语言来实现。

首先,早已知晓 Swift 的服务端工作组已经提供了后台开发的解决方案Vapor。针对我这样的后端开发新手来入门,再合适不过了。不需要舍进求远学习Python、Go等后端框架来实现。

其次,还进一步了解OpenAPI 文档可直接通过 swift-openapi-generatorCreateAPI 等工具生成 Swift 客户端和服务端的代码。这些库通过将AppStore Connect API 的 openapi.json 转化为 Swift 语言封装后的接口和数据模型,大大减少接口方面的工作量,可以Quick-Start,更加聚焦于业务实现。

最后,客户端与后端是使用相同语言实现,如果今后交给其他人维护,其不用再另行学习其他技术栈了。

因此,我决定服务端选用目前流行的开发框架 Vapor + Fluent + mysql 架构、客户端采用Swift UI 进行开发,来实现这样一个证书自动化管理项目,以解决以上证书更新痛点。

项目

证书管理项目,要解决的问题是证书查看和过期管理。那么客户端App就需要展示数据展示和推送消息接收。而后端应进行权限认证、定时更新快过期证书等,然后通过APNs(Apple的远程推送服务)将证书更新这一消息推送给到客户端App。介于此,梳理出以下技术架构。

● 技术架构

根据调研确认的解决方案,输出以下技术架构图:

○ 证书管理架构

1.  配置准备阶段

首先要解决的问题就是如何调起App-Store Connect 提供的 API。那么如何进行其API接口认证、接口规范文档是怎样的,是在项目准备阶段重点解决的问题。

● 1.1 App-Store-Connect Open Api 的两种认证方式

○ 方式1:秘钥认证

App Store Connect API 需要JWT来授权向 API 发出的每个请求。可以使用从 App Store Connect 下载的 API 密钥生成 JWT。

API 密钥由两部分组成:Apple 保留的公共部分和下载的私钥。使用私钥能够签署令牌,授权访问在 App Store Connect 和 Apple Developer 网站中的数据。

调用API需要JSON Web Tokens (JWT)进行授权;可以从开发者后台的App Store Connect账户获取创建这些令牌的密钥。请参阅为App Store Connect API创建API密钥以创建你的密钥和令牌。如下图所示,点击“生成API秘钥”,按提审引导进行秘钥创建:

创建App Store Connect API秘钥后,可以获得三个必要参数:

参数说明详情
issuerID私钥 ID例如:3NLKFTPBTY
privateKeyIDAPI 密钥页面中的您的颁发者 ID例如:fc88b58a-8075-434b-a891-bd5de1169b7e
privateKey私钥文本从苹果后台下载下来的p8文件中获取,去掉---Base---信息以后的示例内容(共200个字符)。例如:MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgIJgRpoXgbjqiZkMQCqqF4LkigC2lBhSt4M6X8WCBhyegCgYIKoZIzj0DAQehRANCAASnhuEens2k1d8cHdBJv/ca5dbxhXCGI7f1CDY8octcYIb4gIhtfsHp/sbxd5kqs0hKRWC9Ur3f6v2QqoYAN6AK

以上三个参数如何在代码中使用,在下文获取证书列表时初始化 APIProvider 的 APIConfiguration

会提到。如果想要进一步研究如何生成JWT,可以访问此处提供的官方计算请求Authorization Bearer Token的文档

○ 方式2:开发者账密认证

使用账密方式可以模拟苹果后台登录。登录协议来源于,通过抓包获取到的苹果开发者后台的账密登录登录方式的协议。

以下是登录接口:

登录方式API参数
Signinidmsa.apple.com/appleauth/a…accountName:String账号password:String密码rememberMe: Bool是否记住密码

苹果开发者账密登录的API调用实现详请,参见友链项目苹果派

如果使用用此种方式实现登录,难免涉及要将账密保存在后端配置中。涉及到账密存储安全,有一定的风险。另外,此种方式涉及到双重认证,管理开发者账号的同事也很难同意提供。因此,咱们在此不采用此方式,账密方式仅提出供群友们参考。

本项目采用的是第一种方案:秘钥认证。

● 1.2 关于苹果App-Store-Connect Open Api 接口文档

由于苹果提供了App Store Connect API,使得我们可以把App Store Connect的证书操作串联起来。为便于大家验证准确性,随文附上完整版App-Store Connect API 链接如下:点击OpenAPI规范以下载规范文件。

完整的开发API文件过于庞大(3M左右),而我们只需要管理套装ID(bundleId)、签名证书、开发设备和预置描述文件。因此对此API文件中的接口进行了缩减。只保留了与打包证书和描述文件相关的API。可以将以上OpenAPI规范文件通过Postman导入,更加直观的查阅和测试相关接口。

API 如下图所示:

在实际的项目开发中,如果自己实现API往往较为低效。基于不重复造轮子的原则,我选择了经过CreateAPI导出接口方案、采用基于此进行封装的第三方库 Appstoreconnect-swift-sdk 来做 AppStore Connect API 对接。

● 1.3 关于推送服务

另外此项目涉及到远程推送服务 APNs,接下来也对推送原理进行了简要的调研:

○ 首先,实现iOS推送有以下两种方式:

■ 使用验证令牌与 APNs 通信(本文采用)

■ 使用 TLS 证书与 APNs 通信

○ 然后,创建推送证书步骤

● 通过证书助理生成一个CA请求证书

● 上传CA请求证书以创建推送证书

● 关联上某个Bundle id

● 下载p8证书

这里要提的是,推送证书系手工创建,这有违我们工作流程自动化初衷。但限于篇幅,我们不再此展开实现自动创建推送证书的步骤。此处仅做证书和描述文件过期提醒时用一下推送服务。

2.  工程创建阶段

环境说明

2.1 安装配置

2.1.1 安装 brew:

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

Homebrew 官方文档 brew.sh/

2.1.2 安装 Vapor:

brew install vapor

2.1 工程创建

2.2.1 创建服务端工程

● 创建项目
vapor new AppStoreConnectCerts
# 以下是创建时选项选择引导:
# 选择支持: Fluent
# 数据库选择: Mysql
# 选择不支持: Leaf

创建好的服务端工程目录如下:

创建时不但生成了服务端应用的启动模版,还将部署环境 Docker 所需的配置文件 docker-compose、Dockerfile 也做了默认创建。

● 添加依赖库

打开Package.swift文件,添加AppStore Connect API实现的依赖库、推送依赖等。如下所示:

dependencies: [
    //  App-Store Connect API for Swift
    .package(url: "https://github.com/AvdLee/appstoreconnect-swift-sdk.git", .upToNextMajor(from: "2.0.0")),
    // 🚀 APNSwift 
    .package(url: "https://github.com/vapor/apns.git", from: "4.0.0")
]

然后将其依赖库名称和包名称添加给主程序可执行 Target 依赖中。如下所示:

.executableTarget(
        name: "App",
        dependencies: [
            .product(name: "AppStoreConnect-Swift-SDK", package: "AppStoreConnect-Swift-SDK"),
            .product(name: "VaporAPNS", package: "apns"),
            .product(name: "FluentMySQLDriver", package: "fluent-mysql-driver")
        ]
    )

2.2.2 创建客户端工程

创建一个多平台的新项目:

选择 “MultiPlatform” 标签,选择 “App” 类型的项目:AppCerts。

创建好的客户端工程目录如下:

2.3 环境运行

2.3.1 运行命令

# 在终端运行执行命令:
swift run App

2.3.2 访问测试

curl http://127.0.0.1:8080/

2.3.3 测试结果

首次启动App运行结果如下图所示:

以上表明咱们创建的环境已经没有问题了。

2.4 项目部署

Swift-Server 首选的开发环境是macOS;而生产环境推荐使用 docker + ubuntu。执行以下 docker-compose 命令,可将当前项目在docker中运行。 docker-compose 是一个编排工具,可以省略较多手工配置docker的细节,通过配置文件即可自定义配置项而无需处理众多的 docker 依赖。

cd AppStoreConnectCerts
# start
sudo docker-compose run -d app

3.  功能实现阶段

接下来,我们将通过使用Vapor、Fluent、swift-openapi-generator来实现用户登录、证书续期、下载授权和消息推送。

3.1 数据存储

实现 ORM

在 Swift - Server 中,如何实现 ORM(对象关系映射)数据库操作呢?

在 Swift 中,要实现 ORM 操作,可以使用 Fluent。它是 Swift 的 ORM 框架,它允许以一种类型安全的方式与数据库进行交互。Fluent 支持多种数据库,包括 PostgreSQL、MySQL、SQLite 和 MongoDB。Fluent 拥有类型安全、数据库无关性、易于使用的特点,提供了一套强大的查询 API.

3.1.1 数据模型

● 用户表 & 令牌表

在实际中应该把 token 信息保存到 redis.

● 证书表

在引入 Fluent 我们可以不在数据库中直接创建表而只需要实现对应的 Model, Content 协议即可使用 Migration 形式 将表自动创建出来

final class Certificate: Model, Content {
    static var schema: String = "certificates"
    init() {}

    @ID(key: .id)
    var id:UUID?
    
    @Field(key: "uuid")
    var uuid: String
    
    @Field(key: "sha1")
    var sha1: String
    
    @Field(key: "name")
    var name: String
}

创建表代码

import Fluent

struct CreateCertificate: AsyncMigration {
    func prepare(on database: Database) async throws {
        try await database.schema("certificates")
            .id()
            .field("name", .string, .required)
            .create()
    }

    func revert(on database: Database) async throws {
        try await database.schema("certificates").delete()
    }
}

● 描述文件表

同理

3.1.1 数据库初始化

添加依赖项后,在 configure.swift 中使用 app.databases.use 配置数据库的凭证。

app.databases.use(.mysql(hostname: "localhost", username: "root", password: "123456", database: "appcerts_db"), as: .mysql)

实际项目中我推荐以上数据库连接,但为了便于展示操作流程,以下以sqlite代替mysql。

所以需要再在依赖库中加多一个 sqlite 驱动依赖库配置:

dependencies: [
        .package(url: "https://github.com/vapor/fluent-sqlite-driver.git", from: "4.0.0"),    
]
targets: [
        .executableTarget(
            name: "App",
            dependencies: [
                    .product(name: "FluentSQLiteDriver", package: "fluent-sqlite-driver"),
            ])
     ]
)

3.1.2 数据迁移

为了便于维护和管理应用程序的数据库结构,确保数据的一致性和完整性。在使用Vapor框架开发的Swift应用程序中设置和管理数据库的数据迁移。要运行迁移,这通常是在应用启动后进行的,以确保数据库结构是最新的。这个过程可能包括创建新的表、更新现有表结构或删除旧表等操作。以下是操作步骤

在 configure.swift 中使用 app.migrations.add 配置数据库的数据表。

// docs:https://docs.vapor.codes/zh/fluent/overview/#migrate
app.migrations.add(CreateUser())
app.migrations.add(CreateCertificate())

在命令行中调用 App migrate 命令如下:

swift run App migrate

执行此命令是对数据库进行了建表操作(为了演示以Sqlite为例),输出日志如下:

# Run
%> swift run App migrate

# Create ‘User’ table
[ DEBUG ] CREATE TABLE IF NOT EXISTS "_fluent_migrations"(
                    "id" UUID PRIMARY KEY, 
                    "name" TEXT NOT NULL, 
                    "batch" INTEGER NOT NULL, 
                    "created_at" REAL, 
                    "updated_at" REAL, 
                    CONSTRAINT "uq:_fluent_migrations.name" 
                    UNIQUE ("name")) [] [database-id: sqlite]

[ DEBUG ] CREATE TABLE "certificates"("id" UUID PRIMARY KEY, "name" TEXT NOT NULL) [] [database-id: sqlite]

3.1.3 应用Fluent配置

接下来,我们将实现用户登录功能。实现过程如下:

// 创建一个登录请求的数据模型
 struct LoginRequest: Content {
     var email: String
     var password: String
 }
 // 创建一个登录响应的数据模型
 struct LoginResponse: Content {
     var token: String
 }
 // 创建一个登录的路由处理器
 app.post("login") { req -> EventLoopFuture<LoginResponse> in
     // 解析登录请求
     let loginRequest = try req.content.decode(LoginRequest.self)
     // 验证用户名和密码
     let user = try User.query(on: req.db).filter(.$email == loginRequest.email).first().unwrap(or: Abort(.unauthorized))
     // 验证密码
     guard try Bcrypt.verify(loginRequest.password, created: user.password) else {
         throw Abort(.unauthorized)
 }
 // 生成令牌
 let token = try Token.generate(for: user)
 // 返回登录响应
 return token.save(on: req.db).map { LoginResponse(token: token.value) }

以上代码中,用户登录功能的实现过程包括创建登录请求和响应的数据模型,创建登录的路由处理器,解析登录请求,再验证用户名和密码,再生成令牌,最后返回登录响应等步骤。

3.2 服务端功能实现

● 拉取证书列表

在这功能中,首先创建一个APIEndpoint.v1.certificates.get的请求,用来获取所有的证书。然后,使用provider实例来发送这个请求,并获取响应数据。最后,返回获取到的证书。

import Vapor
import AppStoreConnect_Swift_SDK
struct CertificateController: RouteCollection {
    static let configuration = try APIConfiguration(issuerID: issuerID, 
                                                 privateKeyID: privateKeyID, 
                                                   privateKey: privateKey)
    let provider: APIProvider = APIProvider(configuration: configuration)
    // Impl Route Collection
    func boot(routes: RoutesBuilder) throws {
        let certificateRoute = routes.grouped("certificates")
        certificateRoute.get(use: certificate)    }
    func certificate(req: Vapor.Request) async throws -> [Certificate] {
        let request = APIEndpoint.v1.certificates.get(parameters: .init())
        let certificates = try await provider.request(request).data
        return certificates
    }
}

以下是代码实现:使用了AppStoreConnect_Swift_SDK库。主要功能是获取App Store Connect API的证书。

首先,导入了所需的库,Vapor和AppStoreConnect_Swift_SDK。

然后,定义了一个名为CertificateController的结构体,它实现了RouteCollection协议。在Vapor中,RouteCollection是一个可以组织和配置路由的协议。

在CertificateController中,有两个主要的部分:

1.  API配置

APIConfiguration是AppStoreConnect_Swift_SDK中的一个结构体,用于配置API的访问权限。为了确保程序有权限访问API,这里需要传入上一章节‘私钥认证’中的三个参数:

● issuerID(发行者ID)

● privateKeyID(私钥ID)

● privateKey(私钥)

2.  API提供者:

APIProvider是AppStoreConnect_Swift_SDK中的一个类,它使用APIConfiguration来发送请求到API,并创建了一个名为provider的实例。

最后,定义了一个名为certificate的异步函数,它接受一个Request类型的参数,表示一个HTTP请求,并返回一个Certificate数组。Certificate是AppStoreConnect_Swift_SDK中定义的一个模型,代表App Store Connect API的证书。

接口测试:

// Test: 
curl 'http://127.0.0.1:8080/certificates'
# Response: 
[
    {
        "id": "DUXXXXXZT6",
        "links": {
            "self": "https://api.appstoreconnect.apple.com/v1/certificates/DUXXXXXZT6"
        },
        "type": "certificates",
        "attributes": {
            "serialNumber": "3F5B88FB0E01B6479FCFHOXXXXXXXER",
            "certificateContent": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXDDDtBcHBsZSBXb3JsZHdpZGUgRGV2ZWxvcGVyIFJlbGF0aW9ucyBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTELMAkGA1UECwwCRzMxEzARBgNVBAoMCkFwcGxlIEluYy4xCzAJBgNVBAYTAlVTMB4XDTIyMTIyMDA5NDY1M1oXDTIzMTIyMDA5NDY1MlowgZUxGjAYBgoJkiaJk/IsZAEBDAozVTI0UFc1VzkzMTgwNgYDVQQDDC9BcHBsZSBEZXZlbG9wbWVudDogS0lSSyBIT0ZNRUlTVEVSIChBRjc2UThDR0ZCKTETMBEGA1UECwwKUUtCNU05ODZCOTEbMBkGA1UECgwSSEVBUlRRVUFLRSBMSU1JVEVEMQswCQYDVQQGEwJVUzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOqpNtt8QpCnfvwTCt+t5Ugvlnob/A+zhDUtcN9cF49IYXkmqLsJJLYXsHkLpmeM5xwlRHIhCfHwZhsZqTqJls95myFzWXno43zsx4Gtbnq0oIrZCv4B9M5EvByyx7h9ew2MZEF9sZ+5R+IfWOu/GHY8l/UX5XUUD5Ic74epSVknRZaeGd1eg2Xn62rM+Lbw01RF+myv40XBJ1JsmeG57aeiC+7mnq8+mHsoqr0LnVTvrGlfhx8SDN23zNEmh+2tSWiWo5odM3AxwOlxfmW59KlcDMKgMW9ArJE/srbyYA5wlWHo3NS9cnNeGiaphsZbvU1hkxt73TSOB3jAlod0T7sCAwEAAaOCAjgwggI0MAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUCf7AFZD5r2QKkhK5JihjDJfsp7IwcAYIKwYBBQUHAQEEZDBiMC0GCCsGAQUFBzAChiFodHRwOi8vY2VydHMuYXBwbGUuY29tL3d3ZHJnMy5kZXIwMQYIKwYBBQUHMAGGJWh0dHA6Ly9vY3NwLmFwcGxlLmNvbS9vY3NwMDMtd3dkcmczMDQwggEeBgNVHSAEggEVMIIBETCCAQ0GCSqGSIb3Y2QFATCB/zCBwwYIKwYBBQUHAgIwgbYMgbNSZWxpYW5jZSBvbiB0aGlzIGNlcnRpZmljYXRlIGJ5IGFueSBwYXJ0eSBhc3N1bWVzIGFjY2VwdGFuY2Ugb2YgdGhlIHRoZW4gYXBwbGljYWJsZSBzdGFuZGFyZCB0ZXJtcyBhbmQgY29uZGl0aW9ucyBvZiB1c2UsIGNlcnRpZmljYXRlIHBvbGljeSBhbmQgY2VydGlmaWNhdGlvbiBwcmFjdGljZSBzdGF0ZW1lbnRzLjA3BggrBgEFBQcCARYraHR0cHM6Ly93d3cuYXBwbGUuY29tL2NlcnRpZmljYXRlYXV0aG9yaXR5LzAWBgNVHSUBAf8EDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQU7pQRUfQlY//FnhJcRFjnVDS3bkowDgYDVR0PAQH/BAQDAgeAMBMGCiqGSIb3Y2QGAQIBAf8EAgUAMBMGCiqGSIb3Y2QGAQwBAf8EAgUAMA0GCSqGSIb3DQEBCwUAA4IBAQCKZyHAGJFsaYmtSKoqQ6de0VLh5GR9ZnKQRCobjm6N7HQz7UF2OlojgWc/c2DbOIEk/zSI9CNDXPqk9eDqrQrm9XfAHg/74WMwbVhSRlm+cHDiQqvC+ucmb+beT4MhyNBNawGiU4b4nfBYlH2S5vhXcoxajLYWsTR6kdNLv6otFA8xSi6L47OURDZ9T0Ln9ZHGpC9vISnvcNTxK7BSPPEmzwQBMFJ0R9DxjemiOs11rQ2GW6D4LP5/K4DcYnsQD/LmSqvf+J1PgJoOqlh5sFG2BSiXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
            "expirationDate": "2023-12-20T09:46:52Z",
            "certificateType": "DEVELOPMENT",
            "name": "Apple Development: KXXK HOXXXXXXXER",
            "displayName": "KXXK HOXXXXXXXER"
        }
    }
]

○ 展示一个月内即将过期的证书

■ 后端轮训判断是否续期

○ 下载证书

■ 证书解决下载时,有未授权问题,后续解决了在代码仓库中提交。

● 描述文件列表

与拉取以上证书列表相似,实现代码如下:

import Vapor
import AppStoreConnect_Swift_SDK

struct ProfileController: RouteCollection {
    static let configuration = try APIConfiguration(issuerID: issuerID, 
                                                 privateKeyID: privateKeyID, 
                                                   privateKey: privateKey)
    let provider: APIProvider = APIProvider(configuration: configuration)
    
    func boot(routes: RoutesBuilder) throws {
        let profilesRoute = routes.grouped("profiles")
        profilesRoute.get(use: profile)
    }
    func profile(req: Vapor.Request) async throws -> [Profile] {
        let request = APIEndpoint.v1.profiles.get(parameters: .init())
        let profiles = try await provider.request(request).data
        return profiles
    }
}

接口测试:

// Test: 
curl 'http://127.0.0.1:8080/profiles'
[
    {
        "id": "XXXXX6CUXK",
        "relationships": {
            "devices": {
                "links": {
                    "related": "https://api.appstoreconnect.apple.com/v1/profiles/XXXXX6CUXK/devices",
                    "self": "https://api.appstoreconnect.apple.com/v1/profiles/XXXXX6CUXK/relationships/devices"
                },
                "meta": {
                    "paging": {
                        "total": 0,
                        "limit": 2147483647
                    }
                }
            },
            "certificates": {
                "links": {
                    "related": "https://api.appstoreconnect.apple.com/v1/profiles/XXXXX6CUXK/certificates",
                    "self": "https://api.appstoreconnect.apple.com/v1/profiles/XXXXX6CUXK/relationships/certificates"
                },
                "meta": {
                    "paging": {
                        "total": 0,
                        "limit": 2147483647
                    }
                }
            },
            "bundleId": {
                "links": {
                    "related": "https://api.appstoreconnect.apple.com/v1/profiles/XXXXX6CUXK/bundleId",
                    "self": "https://api.appstoreconnect.apple.com/v1/profiles/XXXXX6CUXK/relationships/bundleId"
                }
            }
        },
        "links": {
            "self": "https://api.appstoreconnect.apple.com/v1/profiles/XXXXX6CUXK"
        },
        "type": "profiles",
        "attributes": {
            "expirationDate": "2024-05-17T02:26:54Z",
            "createdDate": "2023-05-18T02:26:54Z",
            "profileContent": "MIJIegYxxxxxxxxF3r6...7Hhhs7mtunqFXwvvWg/CF3ICaBqYF2xTntrMnRtS3di",
            "platform": "IOS",
            "uuid": "c756cc03-a3f1-4b3d-8e8a-2e341a5baa70",
            "profileType": "IOS_APP_DEVELOPMENT",
            "profileState": "INVALID",
            "name": "xxxxxDevTest"
        }
    }
]

● 推送消息实现

推送架构如下图所示:

后端推送的核心代码如下:

○ 推送参数初始化
import Vapor
import APNS
public func configure(_ app: Application) async throws {
    static let appleECP8PrivateKey = ""
    let keyIdentifier = "K6U6H66666"
    let teamIdentifier = "QKB6M66666"
    let apnsConfig = APNSClientConfiguration(
        authenticationMethod: .jwt(
            privateKey: try .loadFrom(string: appleECP8PrivateKey),
            keyIdentifier: keyIdentifier,
            teamIdentifier: teamIdentifier
        ),
        environment: .sandbox
    )
    app.apns.containers.use(
        apnsConfig,
        eventLoopGroupProvider: .shared(app.eventLoopGroup),
        responseDecoder: JSONDecoder(),
        requestEncoder: JSONEncoder(),
        as: .default
    )
    try routes(app)
}

以上代码使用 Vapor 和 APNS (Apple Push Notification Service) 来配置一个应用的推送通知服务的。

static let appleECP8PrivateKey = "":这是一个静态常量,用于存储你的 Apple ECP8 私钥。这个私钥是用于 APNS 验证的。

let keyIdentifier = "K6U6H66666" 和 let teamIdentifier = "QKB6M66666":这两个是你的 key identifier 和 team identifier。这些都是用于 APNS 验证的。

接下来的几行代码,是创建一个 APNSClientConfiguration 对象。这个对象包含了 APNS 验证所需的所有信息,包括认证方法(这里使用的是 JWT)、私钥、key identifier、team identifier 和沙箱环境。

最后, app.apns.containers.use():这行代码是配置你的应用使用上面创建的 APNSClientConfiguration 对象。

○ 消息推送
import Vapor
import APNS
import APNSCore
import VaporAPNS

struct PushController: RouteCollection {
    func boot(routes: RoutesBuilder) throws {
        let todos = routes.grouped("push")
        todos.get(use: push)
    }

    func push(req: Request) async throws -> HTTPStatus {
        // Create push notification Alert
        let dt = "66ccf6a66a6c66a6d6c66af66e66e666edc6666a573495925c8af29c9c4db7"
        let payload = Payload(acme1: "Hey", acme2: 2)
        let alert = APNSAlertNotification(
            alert: .init(
                title: .raw("Certificate is almost expired"),
                subtitle: .raw("chuanba ‘s Certificate is almost expired! Plz Update as soon as possible.")
            ),
            expiration: .immediately,
            priority: .immediately,
            topic: "com.sy.sq.appcerts", // Bundle Id
            payload: EmptyPayload()
        )
        // Send the notification
        do {
            try! await req.apns.client.sendAlertNotification(alert, deviceToken: dt)
        } catch {
            print(error.localizedDescription)
        }
        return .ok
    }
}

首先,这段代码定义了一个名为PushController的结构体,它遵循了RouteCollection协议,这是Vapor框架的一部分,用于定义路由。

在 boot 函数中,设置了一个路由组,该组的路径为"push",并且当这个路由接收到GET请求时,会调用 push(req: Request) 函数。

push 函数是一个异步函数,它的主要任务是发送一个推送通知。在函数中,首先创建了一个推送通知的有效载荷(Payload),然后创建了一个APNSAlertNotification对象,用于指定推送通知的各种参数,如标题、副标题、过期时间、优先级、主题等。

然后,使用 req.apns.client.sendAlertNotification 方法发送推送通知。这里的deviceToken是设备的推送唯一标识,用于指定接收推送通知的设备。

如果在发送推送通知过程中发生错误,那么会捕获这个错误,并打印错误信息。

最后,如果推送通知发送成功,那么返回HTTP状态码200(.ok)。

以上代码就是使用APNS和VaporAPNS扩展,实现证书即将过期的推送通知功能。不过以上代码还省略了保存客户端上传的推送令牌的步骤。

● 用户验证

以下是用户认证的流程:

1.1.   实现权限认证

接下来,我们将实现权限认证功能。实现过程如下:

// 创建一个认证用户的中间件
struct AuthMiddleware: ServerMiddleware {
    func intercept(
        _ request: Request,
        metadata: ServerRequestMetadata,
        operationID: String,
        next: (Request, ServerRequestMetadata) async throws -> Response
    ) async throws -> Response {
        // 获取令牌
        let hasAuthorization = request.headerFields.contains { field in
            field.name == "Authorization"
        }
        if !hasAuthorization {
            print("认证: 未认证 (AuthenticationError.notAuthenticated.description)")
            throw AuthenticationError.notAuthenticated
        }
        do {
            // TODO: 查库可以是查Redis等验证令牌
            //       当前示例使用查库代替
            
            // 调用下一个中间件
            let response = try await next(request, metadata)
            print("认证后: 调用下一个中间件")
            return response
        } catch {
            print("认证: Error (error.localizedDescription)")
            throw error
        }
    }
}

中间件注册:

// Craete server Middleware
let authMiddleware = AuthMiddleware()        

// Call the generated protocol function on the handler to configure the Vapor application.
try handler.registerHandlers(on: transport, serverURL: Servers.server1(), middlewares: [permissionMiddleware, authMiddleware, loggingMiddleware])

3.3 客户端实现

以下客户端功能待项目更新至 GitHub 以后在代码仓库中查看。功能列表如下:

● 证书列表

● 授权文件列表

● 推送初始化

● 推送消息接收

四、  测试

如何判断写的服务是否有一定的抗压能力呢?

通过查阅介绍其他后端语言框架的开发模式,了解到用AB命令可以做到压力测试。

以下是我使用AB命令对缩写的服务进行压力测试。(ab是一个HTTP服务器的基准化分析工具,使用灵活,输出的报告也简单易懂)

1.  使用ab命令做一次接口压测

执行以下命令:

ab -n 10000 -c 10 -p ~/Desktop/post.txt -T 'application/json; charset=utf-8' -H 'Authorization: Bearer DFERFER23454323bbwer456' "http://127.0.0.1:8080/auth/login" 

2.  ab测试报告

ab测试输出日下:

以上测试结果表示在Release模式下的平均响应时间在10毫秒,最长79毫秒。

3.  系统资源实时耗费

以上测试报告显示内存消耗稳定,压测是CPU消耗高达到672%,另一个角度看其充分利用了多核CPU资源。

待解决的问题

● 编译耗时长

在开发机开编译了11.6 分钟(MBP 2.2 GHz Intel i7 Core 6 Build Release Cost(1st))

项目未来的开发方向

● 支持扫码获取设备号(UDID)

获取设备号,然后自动将UDID自动添加到苹果后台,编辑描述文件并下来,再将描述文件发送至打包平台使用,完成持续的集成。

● 财务结算数据同步

除了出包相关的业务外,还可以通过App-Store Connect API 将每月的财务结算数据导出,将其发送给统计端,将结算工作流也自动化起来。

● 推送功能强化

推送微服务化。只需根据bundleid,就可以将想要转发的消息,发送给指定的客户端。可用于承载更多需要不定时提醒的业务场景。

总结

以上就是iOS证书管理自动化的项目的实现方案。项目使用了Vapor、Fluent、App-Store Connect API 、Swift OpenAPI Generator等技术来实现。包括用户登录、权限认证、证书管理等功能。

● 知识点总结:

○ Swift拥有现代流行的编程语言书写方式,完善的后端基础组件,是后端语言中拥有强大潜力的开发语言。

○ Swift-Server 基础库逐渐丰富。

○ Fluent是Swift-Server端的一种ORM(对象关系映射)框架,可以让开发者更方便地操作数据库。它使用Swift特性:属性包装器。

○ Swift OpenAPI Generator是一个能够自动生成Swift 代码的API工具,它可以帮助开发者快速生成接口数据模型代码和快速生成。

○ async/await "异步""等待"。在 Swift 5.5 中,Apple 引入了 async/await 模式,这是一种新的方式来处理异步操作。这种新的模式使得编写异步代码变得更加简单和直观。

通过输出文章记录,比实现项目本身更需要查阅大量的资料。这个过程使我对如何自学不熟悉的领域有了自己的方式方法。同时对App推送实现原理、后端服务可扩展性、以及如何进行异步编程(async/await)有了更加深入的认识。同时,认识到写作本身就是一种更高效的学习。

希望这篇文章能够帮助你更好地理解和使用这个开发框架。感谢阅读。

附录:

Github:

客户端: github.com/37MobileTea…

服务端:github.com/37MobileTea…

(持续更新中..)

参考资料:

1.  Getting Started with Vapor 4 Lesson 2

2.  Apple Developer Documentation - App Store Connect API

3.  借助 App Store Connect API 实现工作流程自动化

4.  JWT OpenApi3 实现认证授权

5.  如何使用ab做接口压力测试