构建可扩展的企业应用套件
Build scalable enterprise app suites
2020年6月24日
一句话判断
如果你的企业有多个 App 需要共享数据和状态,iOS 14 的 App Group、Shared Keychain、和 UserDefaults Suite 让你的 App 套件像一个统一的平台一样运作。
这场 Session 讲了什么
大型企业通常有多款面向员工的 App——OA、审批、通讯录、报销等。这些 App 之间需要共享用户登录状态、基础配置和部分数据。Session 详细介绍了如何构建这样的”App 套件”。
核心内容包括:App Group 的配置和使用,允许多个 App 共享文件系统和 UserDefaults;Keychain Sharing,允许多个 App 共享安全凭据;URL Scheme 和 Universal Links 用于 App 之间的跳转和通信;MDM 的 Managed App Configuration 用于批量部署企业 App 的配置。Session 还讨论了如何通过 Shared Framework 来在多个 App 之间共享代码。
值得深挖的点
App Group 的正确使用
App Group 是 iOS 上多个 App 之间共享数据的主要机制。你在 Apple Developer Portal 中创建一个 App Group(如 group.com.enterprise.suite),然后在每个 App 的 entitlement 中声明这个 Group。共享的数据包括:UserDefaults(通过 UserDefaults(suiteName:))、文件系统(通过 FileManager.containerURL(forSecurityApplicationGroupIdentifier:))、和 CoreData 数据库。但要注意,App Group 中的数据没有额外的加密保护,不要存储敏感信息。
Shared Keychain 的安全共享
与 App Group 不同,Keychain Sharing 使用了 Secure Enclave 的硬件级保护。多个 App 可以通过 kSecAttrAccessGroup 共享 Keychain 条目。敏感数据(如认证令牌、加密密钥)应该存储在 Shared Keychain 中而非 App Group 中。
代码片段
配置 App Group 共享数据
场景:在多个企业 App 之间共享用户配置和登录状态。
import Foundation
class SharedConfiguration {
// App Group 标识符(所有参与共享的 App 必须一致)
private let appGroupIdentifier = "group.com.enterprise.suite"
// 共享的 UserDefaults
private var sharedDefaults: UserDefaults {
return UserDefaults(suiteName: appGroupIdentifier)!
}
// 共享的文件目录
private var sharedContainerURL: URL {
return FileManager.default.containerURL(
forSecurityApplicationGroupIdentifier: appGroupIdentifier
)!
}
// MARK: - 共享用户配置
var currentUserID: String? {
get { sharedDefaults.string(forKey: "currentUserID") }
set { sharedDefaults.set(newValue, forKey: "currentUserID") }
}
var serverEnvironment: String {
get { sharedDefaults.string(forKey: "serverEnv") ?? "production" }
set {
sharedDefaults.set(newValue, forKey: "serverEnv")
// 通知其他 App 配置已更新
sharedDefaults.set(Date(), forKey: "configUpdateTime")
}
}
// MARK: - 共享文件存储
func saveSharedData(_ data: Data, filename: String) throws {
let fileURL = sharedContainerURL.appendingPathComponent(filename)
try data.write(to: fileURL)
}
func loadSharedData(filename: String) throws -> Data {
let fileURL = sharedContainerURL.appendingPathComponent(filename)
return try Data(contentsOf: fileURL)
}
}
使用 Keychain Sharing 共享认证令牌
场景:让用户登录一次后所有企业 App 自动获得认证。
import Security
class SharedKeychainManager {
// Keychain Access Group(所有 App 必须在 entitlement 中声明)
private let accessGroup = "ABCDE12345.com.enterprise.shared"
/// 保存认证令牌到共享 Keychain
func saveAuthToken(_ token: String, for user: String) throws {
let tokenData = token.data(using: .utf8)!
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: "auth_token_\(user)",
kSecAttrAccessGroup as String: accessGroup,
kSecValueData as String: tokenData,
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
]
// 先删除旧的
SecItemDelete(query as CFDictionary)
// 添加新的
let status = SecItemAdd(query as CFDictionary, nil)
guard status == errSecSuccess else {
throw KeychainError.saveFailed(status: status)
}
}
/// 从共享 Keychain 读取认证令牌
func loadAuthToken(for user: String) -> String? {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: "auth_token_\(user)",
kSecAttrAccessGroup as String: accessGroup,
kSecReturnData as String: true,
kSecMatchLimit as String: kSecMatchLimitOne
]
var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)
guard status == errSecSuccess,
let data = result as? Data,
let token = String(data: data, encoding: .utf8) else {
return nil
}
return token
}
/// 删除认证令牌(用户注销时)
func deleteAuthToken(for user: String) {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: "auth_token_\(user)",
kSecAttrAccessGroup as String: accessGroup
]
SecItemDelete(query as CFDictionary)
}
}
enum KeychainError: Error {
case saveFailed(status: OSStatus)
}
App 之间的跳转和数据传递
场景:从 OA App 跳转到审批 App 并传递审批 ID。
class AppNavigationManager {
/// 跳转到其他企业 App
func openApprovalApp(approvalId: String) {
// 方法一:使用 URL Scheme
if let url = URL(string: "enterprise-approval://open?id=\(approvalId)") {
if UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url)
} else {
// App 未安装,引导用户安装
promptInstall(appName: "审批")
}
}
}
/// 使用 Universal Links 跳转(更安全)
func openApprovalAppUniversal(approvalId: String) {
if let url = URL(string: "https://app.enterprise.com/approval/\(approvalId)") {
UIApplication.shared.open(url)
}
}
/// 通过 Shared UserDefaults 传递复杂数据
func passDataToOtherApp(data: [String: Any]) {
let sharedConfig = SharedConfiguration()
// 写入共享数据
let jsonData = try? JSONSerialization.data(withJSONObject: data)
try? sharedConfig.saveSharedData(jsonData!, filename: "pending_approval.json")
// 设置标记
sharedConfig.sharedDefaults.set(true, forKey: "has_pending_data")
// 跳转到目标 App
openApprovalApp(approvalId: data["id"] as? String ?? "")
}
private func promptInstall(appName: String) {
// 提示用户安装企业 App
if let url = URL(string: "https://apps.apple.com/app/idXXXXXXXXX") {
UIApplication.shared.open(url)
}
}
}
读取 MDM 托管配置
场景:在 App 中读取 IT 管理员通过 MDM 下发的配置。
import Foundation
class ManagedConfigReader {
/// 读取 MDM 下发的配置
func readManagedConfiguration() -> [String: Any]? {
// MDM 配置存储在 UserDefaults 的特定 key 中
let defaults = UserDefaults.standard
// 读取 managed configuration
if let config = defaults.dictionary(forKey: "com.apple.configuration.managed") {
return config
}
return nil
}
/// 获取企业服务器地址
func getEnterpriseServerURL() -> String {
if let config = readManagedConfiguration(),
let serverURL = config["server_url"] as? String {
return serverURL
}
// 没有托管配置时使用默认值
return "https://api.enterprise.com"
}
/// 检查是否是托管设备
func isManagedDevice() -> Bool {
return UserDefaults.standard.dictionary(forKey: "com.apple.configuration.managed") != nil
}
}
最佳实践
已有项目:如果你有多个企业 App 但它们目前独立运行,规划一个统一的 App Group 和 Keychain Sharing 方案。先从共享登录状态开始——用户在一个 App 登录后,其他 App 自动获得认证。然后逐步共享配置、缓存和常用数据。
新项目:从架构设计阶段就考虑 App 套件的共享策略。创建一个 Shared Framework 包含公共的网络层、数据模型和工具类。所有企业 App 都依赖这个 Shared Framework,确保行为一致。
还有什么值得关注
- App Group 的共享文件有容量限制(通常几十 MB),不要用它存储大文件。
- Keychain Sharing 需要所有参与 App 使用相同的 Team ID。
- MDM 的 Managed App Configuration 支持
Install、Remove、Settings三种配置类型。