构建 Mac App Store 应用之必备知识

5,405 阅读10分钟

本文作者系360奇舞团前端开发工程师

引言

在日常工作中,Mac 电脑上安装的应用主要来自两个渠道:一是通过 App Store 下载,由苹果审核团队保证内容、技术、隐私的合规性;二是从网站下载应用,以 pkg、dmg 格式提供,由安装者自行决定是否信任。这两种渠道下的应用可能在权限、操作方式、数据安全等方面都存在差异,而苹果的技术文档庞杂并且同时面向这两种情况,导致作为开发者,在开发需要上架 App Store 的 Mac 应用时,可能会面临技术、设计和上线合规等挑战。确切了解哪些技术合规、哪些权限需关注变得十分重要。

本篇文章我们将一起了解,开发一款 Mac App Store 应用必须要知道的知识点。

App Sandbox

要通过Mac App Store发布macOS应用,苹果要求必须启用应用沙盒功能。

App Sandbox : 应用程序沙箱是 macOS 在内核级提供并执行的一种访问控制技术。沙箱的主要功能是在用户执行受攻击的应用程序时,限制对系统和用户数据造成的损害。虽然沙箱并不能阻止针对你的应用程序的攻击,但它可以将你的应用程序限制在正常运行所需的最低权限范围内,从而减少攻击成功可能造成的危害。

sandbox 开启沙盒权限:

image.png

开启沙盒权限转存失败,建议直接上传图片文件 如果添加了App Sandbox功能,则会将相应的权利添加到项目的配置中。.entitlements这是添加沙箱后的项目文件:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">100
<plist version="1.0">
<dict>
	<key>com.apple.security.app-sandbox</key>
	<true/>
</dict>
</plist>

当应用程序启用沙盒后,会限制其访问系统资源和执行某些操作。以下是一些主要的限制:

  1. 文件系统访问限制
  2. 网络访问限制
  3. 进程间通信限制
  4. 硬件访问限制
  5. 脚本执行限制
  6. 安装软件包限制

从App Sandbox中访问文件

应用程序沙盒通过限制应用程序对受保护资源的访问来提高Mac的整体安全性。 但 MacOS 允许你的应用程序不受限制地访问 Mac文件系统的有限部分

应用程序对其沙盒容器(~/Library/Containers)有完全的读写权限,也可以运行位于那里的程序。

image.png

对于沙盒之外文件的访问,苹果提供了标准的用户交互式访问文件的方式:NSOpenPanelNSSavePanel。操作系统会在一个单独的进程中显示打开和保存面板,并扩展我们应用程序的沙盒,以包括选定的URL,让我们的应用能够对该URL进行安全范围的访问。

首先需要配置沙盒的文件访问权限为只读或读写:

image.png

用户选择文件选项允许访问用户使用 AppKit 的 NSOpenPanel 和 NSSavePanel 选择的任意位置。

<dict>
	<key>com.apple.security.app-sandbox</key>
	<true/>
	<key>com.apple.security.files.user-selected.read-write</key>
	<true/>
</dict>

然后使用NSOpenPanel进行访问。

image.png

通过NSOpenPanelNSSavePanel实现安全范围内的文件访问,会随着程序的关闭而结束。如何让应用程序下次运行时仍旧保持对文件的访问?

Security-Scoped URL 书签持续访问文件

如果想为沙盒应用程序提供对文件系统资源的持久访问,则必须启用security-scoped bookmark

在沙盒权利文件.entitlements配置

 <key>com.apple.security.files.bookmarks.app-scope</key>
 <true/>

image.png

使用Foundation创建一个具有明确安全范围的URL书签,应用程序可以存储并检索,无论我们的应用程序是否在访问期间退出,都可以在随后访问URL上的资源。

func saveBookmarkWithFilePath(filePath: String) ->Data? {
    let url = NSURL.fileURL(withPath: filePath)
    //如果应用程序不需要在随后的访问中写入文件,
    //请在函数的`options`字段传入`securityScopeAllowOnlyReadAccess`。
    let data = try? url.bookmarkData(options: .withSecurityScope)
    if let data {
        UserDefaults.standard.set(data, forKey: filePath)
        UserDefaults.standard.synchronize()
    }
    return data
}

访问文件的URL书签,必须遵循:解析书签为Url --> 重建书签(if need) --> 开启访问 startAccessingSecurityScopedResource()--> 执行操作 --> 停止访问 stopAccessingSecurityScopedResource()。详见下列代码注释

///访问文件的URL书签
func accessingSecurityScopedResourceWithFilePath(filePath:String)->Bool {
    let data = UserDefaults.standard.object(forKey: filePath) as? Data
    var success = false
    if let data {
        var isStale = false
        //1.解析书签
        let url = try? URL(resolvingBookmarkData: data, options: .withSecurityScope, relativeTo: nil, bookmarkDataIsStale: &isStale)
        if isStale, let url { //2.bookmarkDataIsStale == true 更新书签
            let data = try? url.bookmarkData(options: .withSecurityScope)
            if let data {
                UserDefaults.standard.set(data, forKey: filePath)
                UserDefaults.standard.synchronize()
            }
        }
        if let url {
            ///3. 开启安全范围内的文件访问
            success = url.startAccessingSecurityScopedResource()
        }
    }
    return success
}

///4. 执行文件访问操作
///.....
///5. 撤销安全范围内的文件访问
func stopAccessingSecurityScopedResourceWithFilePath(filePath:String) -> Bool {
    let data = UserDefaults.standard.object(forKey: filePath) as? Data
    var success = false
    var isStale = false
    if let data {
        //1.解析书签
        let url = try? URL(resolvingBookmarkData: data, options: .withSecurityScope, relativeTo: nil, bookmarkDataIsStale: &isStale)
        if let url {
            ///2. 撤销安全范围内的文件访问
            url.stopAccessingSecurityScopedResource()
            success = true
        }
    }
    return success
}

进程之间共享 URL 书签

应用程序也可以把这个书签传递给另一个进程,比如启动代理或XPC服务。如果要访问接收到的进程中的资源,需要遵循以下示例:

do {
    let location = try URL(resolvingBookmarkData: bookmark)
    defer {
      location.stopAccessingSecurityScopedResource()
    }
    // Use the resource at the location URL.
}
catch let error {
    // Handle any errors.
} 

接收进程在解析书签时隐式地试图扩展其沙盒以覆盖书签资源,就像它在解析期间调用startAccessingSecurityScopedResource()一样。要避免在解析书签数据时扩展进程的沙盒,请通过选项 withoutImplicitStartAccessing

URL(resolvingBookmarkData:bookmarkData, bookmarkDataIsStale: &stale, options: .withoutImplicitStartAccessing)

应用程序组之间共享文件

应用程序组允许单个开发团队开发的多个应用程序访问共享容器并使用进程间通信 (IPC) 进行通信。应用程序可能属于一个或多个应用程序组。作为同一个应用组成员的应用程序都可以访问一个共享容器~/Library/Group\ Containers/,它们可以用来交换文件。它们的标识符格式如下:

//iOS
group.<group name>
//macOS
<team identifier>.<group name>

共享容器位置的获取:

// 格式let groupId = "TEAM_ID.com.domain"
let appGroupID = "88L2Q4487U.com.qihoo"
let sharedContainerFolderURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupID)

桌面、下载、文稿文件的访问

基于苹果审核准则5.1.1 法律 - 隐私 - 数据收集和存储,当我们访问DownloadsDesktopDocuments目录,需要在info.plist中配置隐私提示,解释应用为何需要访问这些位置。

image.png

配置完毕,当我们访问对应目录时,便会弹出对应的授权弹框。

image.png

另外,Downloads还需在Target-> Signing & Capabilities -> App Sandbox中根据需要配置相应文件访问权限。

image.png

除此之外,图片、音乐和电影库的目录访问也需要在此处进行配置,这三个目录只有当应用程序需要管理图片、音乐或电影库,才可以写入,故而大多数情况下都是只读模式。 Mac App Programming Guide

硬件访问

沙盒应用程序需要访问系统上的硬件服务,USB、打印或内置摄像头和麦克风,也需要开启对应的沙盒权利。

image.png

对应的沙盒权利文件.entitlements配置

<key>com.apple.security.device.audio-input</key>
      <true/>
<key>com.apple.security.device.bluetooth</key>
      <true/>
<key>com.apple.security.device.camera</key>
      <true/>
<key>com.apple.security.device.usb</key>
      <true/>

同时也不要忘记配置隐私访问的描述字符串,解释应用程序需要访问的原因

image.png

当然地址簿、位置、日历这些个人信息与硬件信息一样同属沙盒下受保护的资源,也需要开启权利并配置对应的隐私描述。

image.png

网络安全

苹果使用ATS(应用程序传输安全)来提高所有应用程序和应用程序扩展的隐私和数据完整性,要求应用所进行的网络连接必须通过传输层安全(TLS)协议,并使用可靠的证书和密码来确保安全 (HTTPS) 。ATS 会阻止不符合最低安全要求的连接。

ATS 在 iOS 9.0 或 macOS 10.11 或 更高版本的应用中是默认开启的。当应用程序链接到旧版的SDK 时,无论应用程序运行在哪个版本的操作系统上,ATS 都会被禁用。

如果在Mac应用中使用网络请求,则需要先配置权利:

image.png

<!-- 允许传出连接 ->
<key>com.apple.security.network.client</key> 
<true/>
<!-- 允许传入连接 ->
<key>com.apple.security.network.server</key>
<true/>

如果在应用中要使用HTTP,可以配置应用的元数据info.plist:

  1. 对所有网络连接禁用ATS,完全使用HTTP进行网络连接

image.png 2. 针对部分内容连接禁用ATS,如:Web ContentMedia

image.png

  1. 针对特定域名禁用ATS

image.png

应用采用上述方式,提交到App Store审核时,需要提供无法建立安全链接的理由。Developer Forums

进程

在 Mac OS 应用的沙盒环境下,进程间通信(IPC)会变得更加复杂,因为传统的IPC方法可能会受到限制。以下是一些进程间通信的方案:

  1. XPC

    XPC 是 Apple 提供的一种基于消息传递的进程间通信机制。XPC 支持在应用间创建安全的连接。 在沙盒环境中,XPC 可以通过权限分离进一步提高安全性,将一个应用程序分成更小的部分,负责应用程序的一部分行为。每个XPC服务都有自己的沙盒,所以XPC服务可以更容易实现适当的权限分离。这使得它在沙盒环境中非常安全可行。同时,XPC 也是苹果官方推荐的进程间通信方法。Creating XPC services

  2. NSDistributedNotificationCenter

    NSDistributedNotificationCenter 允许应用之间发送和接收通知。这种方法在沙箱环境中是有限制的,在苹果文档 Protecting user data with App Sandbox 与沙盒不兼容的条款中有提到:沙盒环境是不能使用NSDistributedNotificationCenter向其他任务发送 userInfo。

  3. App Groups 和共享容器

    使用App Groups和共享容器,可以在沙盒应用之间共享文件和数据。虽然不是直接的IPC,无法实时通信,但可以用于共享信息。这个在上文有提到。

  4. Mach Ports

    Mach ports 是底层的进程间通信机制,用于在Mac OS内核中进行进程通信。它们可以在沙盒应用之间使用,前提是多个进程必须是同一个应用程序组,同时需要谨慎处理权限和隔离。缺点是 Mach ports 过于底层,编程复杂。且苹果文档 Mac Technology Overview 有提到:

Note: Mach ports are another technology for transferring messages between processes. However, messaging with Mach port objects is the least desirable way to communicate between processes. Mach port messaging relies on knowledge of the kernel interfaces, which might change in a future version of OS X. The only time you might consider using Mach ports directly is if you are writing software that runs in the kernel.

  1. Sockets

    UNIX Domain Sockets 也是一种用于进程间通信的方式,与 Mach ports 情况一样,IPC的进程必须是同一个应用程序组,同时需要谨慎处理苹果关心的沙盒下权限和安全问题。

除了上述五种方式还有共享内存、AppleEvents、Pasteboard等,具体的实现细节可以参考书籍:【Interprocess Communication with macOS - Apple IPC Methods】

最后在引用一段苹果文档 App Groups Entitlement,关于沙盒进程IPC的表述:

Apps within a group can communicate with other members in the group using IPC mechanisms including Mach IPC, POSIX semaphores and shared memory, and UNIX domain sockets. In macOS, use app groups to enable IPC communication between two sandboxed apps, or between a sandboxed app and a non-sandboxed app.

参考

App Sandbox

App Groups Entitlement

Preventing Insecure Network Connections

IOS Using App Groups for communication between macOS/iOS apps from the Same Vendor

App Store Review Guideline

Best Practices for Submitting Scriptable and AppleScript Apps to the Mac App Store