WWDC Quick Look 💓 By SwiftGGTeam
Get to know the ManagedApp Framework

Get to know the ManagedApp Framework

观看原视频

Highlight

ManagedApp 框架在 iOS/iPadOS 18.4 与 visionOS 2.4 引入,允许 MDM 直接向托管 app 投递配置、密码、证书和支持硬件绑定密钥的 ACME 身份,并通过 async sequence 在变更时实时通知 app。


核心内容

企业里第一次启动一个新装的 app,往往是体验最糟糕的一刻。员工要输服务器地址、用户名、密码、二次验证码,然后进设置页一项项勾选,对齐组织的合规要求。Bob Whiteman 在演讲开场就把这个痛点摆了出来(00:51):步骤一多就容易出错,员工放弃使用,IT 接到工单,组织最后干脆把这个 app 从清单里删掉。开发者想自己解决,意味着要做身份联邦、搭配置网站、对接证书颁发机构、维护组织定制版本——全是和核心功能无关的活。

ManagedApp 框架的思路是把这些活全部交给系统。管理员在 MDM 端配置好,app 一启动就拿到一份现成的配置和密钥,不再需要任何首次设置流程(02:25)。框架统一承担四类数据的安全分发:app 自定义配置、密码、证书、身份(支持 PKCS #12、SCEP 和 ACME,后者还能绑定硬件密钥并做远程证明)。整套机制要求 MDM 使用 declarative device management,从 app 被管理那一刻开始生效,覆盖所有 MDM 注册类型。

最关键的设计是配置 schema 由 app 开发者自己定义,而不是 Apple 或 MDM 厂商定一套通用格式。Bob 反复强调这一点(06:44):你比任何人都懂自己的 app,只有你能给出最合适的可配置面。框架不是方案,是构建方案的平台。


详细内容

ManagedApp 框架由四个独立的 provider 组成,分别对应密码、证书、身份、自定义配置(12:20)。前三个返回系统内建类型,自定义配置则由 app 决定数据结构。

第一步是把自定义配置的 schema 写成一个 Decodable 类型。Landmarks 示例里只有一个字段 collection

// Your app's managed configuration

struct LandmarksManagedConfig: Decodable {

    private(set) var collection: LandmarkCollection?

    private enum CodingKeys: String, CodingKey {
        case collection
    }

    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        collection = try values.decode(LandmarkCollection.self, forKey: .collection)
    }
}

关键点:

  • LandmarksManagedConfig 实现 Decodable,框架会用它把 MDM 投递的字典反序列化成 Swift 类型。
  • collectionprivate(set) 暴露只读视图,配置只能由框架更新。
  • CodingKeys 显式声明键名,避免依赖属性名隐式匹配。
  • init(from:) 里用 decode 而不是 decodeIfPresent,意味着缺字段就抛错——schema 不匹配时让 app 早失败。

第二步是把配置注入 app 的运行时。ManagedAppConfigurationProvider 返回的是一个 AsyncSequence,配置变更时循环体会再次进入(0:02):

// Receiving the current configuration

import ManagedApp

// [...]

var managedCollection: LandmarkCollection?

// [...]

func loadCollections() {
    // [...]
    Task {
        let configProvider = ManagedAppConfigurationProvider()

        for await config in await configProvider.configurations(LandmarksManagedConfig.self) {
            // config's type is LandmarksManagedConfig?
            managedCollection = config?.collection
        } // Loops forever
    }
}

关键点:

  • import ManagedApp 引入框架,模块名就叫这个。
  • configurations(_:) 接受 schema 类型,返回的元素是 LandmarksManagedConfig?,没有配置或 app 处于非托管状态时为 nil
  • for await 循环永不结束,MDM 每次推新配置都会触发一次迭代,省掉手写 KVO 或通知中心。
  • Task 放在数据加载逻辑里,让配置生效和数据加载在同一个生命周期。

第三步处理身份认证。VPN 扩展和企业服务调用经常需要客户端证书,框架把 SecIdentity 直接喂给 URLSession 委托:

// Using an identity

final class MyURLSessionDelegate: NSObject, URLSessionDelegate {

    func urlSession(_ session: URLSession,
                    didReceive challenge: URLAuthenticationChallenge)
        async -> (URLSession.AuthChallengeDisposition, URLCredential?) {
        switch challenge.protectionSpace.authenticationMethod {
        case NSURLAuthenticationMethodClientCertificate:

            // Look up the identity
            let provider = ManagedAppIdentitiesProvider()
            let id = "AssetDownloadClient"
            guard let identity = try? await provider.identity(withIdentifier: id) else {
                // No identity, cancel the challenge
                return (.cancelAuthenticationChallenge, nil)
            }

            // Use the identity to authenticate.
            return (.useCredential, URLCredential(identity: identity,
                                                  certificates: nil,
                                                  persistence: .forSession))
        default:
            return (.performDefaultHandling, nil)
        }
    }
}

关键点:

  • URLSessionDelegate 的 async 版本在 challenge 到来时挂起,等身份取回来再回调。
  • ManagedAppIdentitiesProvider().identity(withIdentifier:) 按 schema 里约定的标识符取身份,标识符由 app 自己定义。
  • 取不到身份就调用 cancelAuthenticationChallenge,让请求直接失败而不是回退到弱认证。
  • URLCredentialpersistence.forSession,私钥不会被复制到 Keychain 之外。

许可证场景是这个框架最直观的升级。Bob 在 09:00 对比了旧方案:分发字符串 token 会面临泄漏风险,而 ManagedApp 框架可以分发由组织 CA 签发的加密身份,私钥在设备上生成,永远不出设备。VPN 扩展走得更远,可以拿到带硬件证明的 ACME 身份(10:46),从根上挡住中间人接管 VPN 凭据的攻击路径。


核心启发

  • 把旧的 ManagedAppConfiguration 迁到 ManagedApp 框架:为什么值得做——旧方案不支持 app 扩展、不支持证书和身份,且没有变更通知;新框架在 iOS/iPadOS 18.4 之后是更完整的替代品(09:46)。怎么开始——先定义一个最小 schema,跑通”MDM 推送 → app 收到”的链路,再把旧 UserDefaults 读取逻辑逐项搬过去,每搬一项就删掉旧代码。

  • 给 VPN 扩展接入硬件绑定的 ACME 身份:为什么值得做——VPN 是攻击面最高的入口之一,把私钥钉在 Secure Enclave 上并附带远程证明,能让组织的 VPN 准入策略真正可信(10:30)。怎么开始——在 schema 里声明一个身份字段,要求 MDM 用 ACME 颁发;在 NEVPNManager 的认证回调里复用上面那段 ManagedAppIdentitiesProvider 的代码。

  • 用许可证身份替换许可证 token:为什么值得做——token 一旦泄漏组织就要继续付费,而设备生成的密钥对永远不离开设备,泄漏面接近零(08:30)。怎么开始——服务端先支持 mTLS,发证流程接组织 CA;客户端用 ManagedAppIdentitiesProvider 拿身份做客户端证书认证。

  • 为 SSO 扩展接收一次性密码和初始凭据:为什么值得做——身份提供商通常自己设计认证协议,过去只能让用户手输凭据;现在可以让管理员通过 MDM 直接预置初始密码或绑定令牌(11:42)。怎么开始——在 SSO 扩展里加 ManagedAppPasswordsProvider,启动时尝试读取,存在则跳过手动输入流程。

  • 配置变更走 async sequence 做整体重载:为什么值得做——for await 循环天然处理多次更新,避免逐字段监听导致的中间不一致状态(06:00)。怎么开始——在 Task 内迭代 configurations(_:),每次收到新值就走一遍完整的”应用配置”流程,而不是 diff 单个字段。


关联 Session

评论

GitHub Issues · utterances