“安全书签”(Security-Scoped Bookmark)是 macOS 沙盒应用中的一种机制,允许 App 在用户授权之后,长期访问某个文件或目录,即使应用退出或重启后依然有效。
为什么需要安全书签?
当 App 启用了 App Sandbox(沙盒机制) 时:
App 默认无法访问用户文件系统中的任意文件。
即使用户通过 NSOpenPanel 选择了文件,也只能在当前会话中访问,应用关闭后权限就丢失。
示例:
struct ContentView: View {
let nsURL = URL(fileURLWithPath: "/Users/fangjunyu.com/Wallpaper/wallhaven-6og5gx.jpeg")
@State private var getURL: URL = URL(fileURLWithPath: "")
var body: some View {
ZStack {
Color.clear
.frame(width: 300,height:200)
VStack {
if let image = NSImage(contentsOf: nsURL) {
Image(nsImage: image)
.resizable()
.scaledToFit()
.frame(width: 300)
} else {
Text("默认URL没有获取到图片")
}
if let imageF = NSImage(contentsOf: getURL) {
Image(nsImage: imageF )
.resizable()
.scaledToFit()
.frame(width: 300)
} else {
Text("未使用 NSOpenPanel 获取到图片文件")
}
Button("按钮") {
let panel = NSOpenPanel()
panel.canChooseFiles = true
panel.canChooseDirectories = false
if panel.runModal() == .OK, let url = panel.url {
getURL = url
}
}
}
}
}
}
在这个代码示例中,分别通过nsURL和getURL显示两张图片,nsURL是固定的URL,getURL是通过NSOpenPanel获取的URL。
当打开应用时,没有显示任何图片。

当通过NSOpenPanel选择其他的图片时。

NSOpenPanel获取到图片的URL和权限,显示对应的图片。但是,因为nsURL是固定的URL,因为还没有固定URL的权限,所以无法显示。
当我们选择nsURL对应的图片时:

可以看到,nsURL和getURL都可以显示图片。这是因为,当我们通过NSOpenPanel获取固定URL的图片时,我们都能够获取到固定URL图片的权限,才可以显示。
否则,即使我们固定URL的路径是正确的,也会因为权限的问题导致无法获取URL对应的文件。
当我们退出应用,重新打开时,不会显示任何的图片。因为我们关闭应用后,获取的文件权限丢失,因此只能重新通过NSOpenPanel获取文件的权限。
解决方案:用安全书签(bookmark) 把用户授权的访问“记住”,下次可以还原访问权限。
安全书签是什么?
它是一个可保存的 Data 对象,用来记录访问权限信息。可以将它保存到用户的 UserDefaults 或数据库中。
工作流程概览
1、用户通过 NSOpenPanel 选择文件:系统临时授予权限。
2、创建安全书签(bookmarkData):使用 url.bookmarkData(options:) 创建。
3、将书签数据持久化:保存到磁盘、UserDefaults、数据库。
4、下次启动时读取书签并还原 URL:使用 URL(resolvingBookmarkData:) 恢复访问权限。
5、开启“访问作用域”:startAccessingSecurityScopedResource()。
6、使用文件操作。
7、完成后停止访问:stopAccessingSecurityScopedResource()。
创建和恢复安全书签
第一次用户选择文件并保存书签:
let panel = NSOpenPanel()
panel.canChooseFiles = true
panel.canChooseDirectories = false
if panel.runModal() == .OK, let url = panel.url {
do {
let bookmark = try url.bookmarkData(options: [.withSecurityScope], includingResourceValuesForKeys: nil, relativeTo: nil)
UserDefaults.standard.set(bookmark, forKey: "MyFileBookmark")
print("书签保存成功")
} catch {
print("书签创建失败: \(error)")
}
}
2、之后恢复访问:
if let bookmark = UserDefaults.standard.data(forKey: "MyFileBookmark") {
var isStale = false
do {
let url = try URL(resolvingBookmarkData: bookmark, options: [.withSecurityScope], relativeTo: nil, bookmarkDataIsStale: &isStale)
if url.startAccessingSecurityScopedResource() {
// ✅ 现在可以访问文件
let content = try String(contentsOf: url)
print("文件内容: \(content)")
url.stopAccessingSecurityScopedResource()
} else {
print("无法访问资源")
}
} catch {
print("解析书签失败: \(error)")
}
}
代码解析
创建安全书签并保存
let bookmark = try url.bookmarkData(
options: [.withSecurityScope],
includingResourceValuesForKeys: nil,
relativeTo: nil
)
UserDefaults.standard.set(bookmark, forKey: "MyFileBookmark")
1、url.bookmarkData(…)
url 是用户通过 NSOpenPanel 选中的文件路径。
bookmarkData 是一个可以序列化的二进制数据 (Data),它记录了这个文件的访问权限。
options: [.withSecurityScope]:
告诉系统创建一个具有“安全作用域”访问权限的书签,只有这样才能在 App 沙盒内持久访问该文件。
includingResourceValuesForKeys: nil:
可选项,表示是否同时缓存文件的一些元数据(比如文件大小、修改时间等),这里我们不需要,填 nil。
relativeTo: nil:
如果想创建相对路径书签,可设置一个 base URL;否则设为 nil 表示绝对路径。
2、UserDefaults.standard.set(bookmark, forKey: “MyFileBookmark”)
将上一步生成的 bookmarkData 存入用户默认设置中。
注意:bookmarkData 是 Data 类型,UserDefaults 支持存储它。
恢复安全书签并访问文件内容
if let bookmark = UserDefaults.standard.data(forKey: "MyFileBookmark") {
var isStale = false
do {
let url = try URL(
resolvingBookmarkData: bookmark,
options: [.withSecurityScope],
relativeTo: nil,
bookmarkDataIsStale: &isStale
)
1、UserDefaults.standard.data(forKey: …)
从之前保存的 UserDefaults 中读取书签数据。
2、URL(resolvingBookmarkData: …)
将 bookmarkData 反解析为一个 URL。
options: [.withSecurityScope] 表示我们打算恢复安全作用域权限。
bookmarkDataIsStale 是一个输出参数:
如果返回 true,说明书签“陈旧”,需要重新授权或重新创建书签。
var isStale = false
因此,当创建并绑定的isStale被系统修改为true时,表示书签已经失效。
3、获取权限
必须调用它,才能真正获得对该文件的读/写权限。
if url.startAccessingSecurityScopedResource() {
如果失败(返回 false),说明访问被拒绝或 URL 无效。
4、访问文件
let content = try String(contentsOf: url)
现在可以像访问 App 自己的文件一样,直接操作该 URL 对应的文件。
这里是读取文件内容并打印。
5、释放权限
url.stopAccessingSecurityScopedResource()
一定要调用 stopAccessing…() 来结束访问权限。
如果忘记释放,系统会记录访问泄漏,长时间后导致崩溃或拒绝访问。
注意事项
1、必须调用 startAccessingSecurityScopedResource(),否则无法访问资源。
2、用完调用 stopAccessingSecurityScopedResource(),释放权限。
3、支持访问文件夹,也可以创建目录书签。
4、可跨启动保留权限,书签保存后,可长期使用。
5、当调用stopAccessingSecurityScopedResource()释放权限后,url的权限就会失效,因此不要保存url,而是在获取权限后,通过url获取图片/文件的资源,然后再释放权限。
6、UserDefaults的键值存储知恩感保存一个书签(Data)。
相关文章
macOS文件选择对话框NSOpenPanel:https://fangjunyu.com/2025/06/27/macos%e6%96%87%e4%bb%b6%e9%80%89%e6%8b%a9%e5%af%b9%e8%af%9d%e6%a1%86nsopenpanel/
扩展知识
保存多个 URL 的书签数据
UserDefaults 的键值存储只能保存一个书签(Data)或一个简单数组。
可以将多个安全书签 Data 组织为字典或数组,并保存到 UserDefaults。以下是推荐的两种方案:
1、用 [String: Data] 字典保存多个书签
// 保存:每个文件 URL.path 作为 key,bookmarkData 作为 value
var bookmarks = UserDefaults.standard.dictionary(forKey: "Bookmarks") as? [String: Data] ?? [:]
let bookmark = try url.bookmarkData(options: [.withSecurityScope], includingResourceValuesForKeys: nil, relativeTo: nil)
bookmarks[url.path] = bookmark
UserDefaults.standard.set(bookmarks, forKey: "Bookmarks")
然后恢复访问:
if let bookmarks = UserDefaults.standard.dictionary(forKey: "Bookmarks") as? [String: Data] {
for (path, data) in bookmarks {
var isStale = false
let url = try URL(resolvingBookmarkData: data, options: [.withSecurityScope], relativeTo: nil, bookmarkDataIsStale: &isStale)
if url.startAccessingSecurityScopedResource() {
// 使用 url
url.stopAccessingSecurityScopedResource()
}
}}
2、用 [Data] 数组保存书签列表
// 添加书签
var bookmarks = UserDefaults.standard.array(forKey: "BookmarkArray") as? [Data] ?? []
let bookmark = try url.bookmarkData(options: [.withSecurityScope], includingResourceValuesForKeys: nil, relativeTo: nil)
bookmarks.append(bookmark)
UserDefaults.standard.set(bookmarks, forKey: "BookmarkArray")
恢复:
if let bookmarks = UserDefaults.standard.array(forKey: "BookmarkArray") as? [Data] {
for bookmark in bookmarks {
var isStale = false
let url = try URL(resolvingBookmarkData: bookmark, options: [.withSecurityScope], relativeTo: nil, bookmarkDataIsStale: &isStale)
if url.startAccessingSecurityScopedResource() {
// 使用 url
url.stopAccessingSecurityScopedResource()
}
}
}
推荐:使用 [String: Data] 字典,它有以下优点:
1、可快速查找已保存路径(去重)。
2、可以为每个图片保存书签、缩略图、文件名等元信息。
3、更适合绑定 SwiftUI 的视图列表。
完整代码
下面是通过NSOpenPanel获取多个图片并保存到安全书签中。
每次打开应用时,可以通过恢复按钮显示安全书签中的所有图片。
import SwiftUI
import AppKit
struct ContentView: View {
@State private var tmpImages = [NSImage()]
var body: some View {
ZStack {
Color.clear
.frame(width: 300,height:200)
VStack {
ForEach(tmpImages,id: \.self) { item in
Image(nsImage: item)
.resizable()
.scaledToFit()
.frame(width: 300)
}
Button("获取图片按钮") {
let panel = NSOpenPanel()
panel.canChooseFiles = true
panel.canChooseDirectories = false
panel.allowsMultipleSelection = true
if panel.runModal() == .OK, let url = panel.url {
tmpImages = []
do {
var bookmarks:[Data] = []
for file in panel.urls {
let bookmark = try file.bookmarkData(options: [.withSecurityScope], includingResourceValuesForKeys: nil, relativeTo: nil)
print("bookmarks:\(bookmarks)")
bookmarks.append(bookmark)
print("当前添加的图片文件路径:\(file)")
if let selectedImg = NSImage(contentsOf: file) {
tmpImages.append(selectedImg)
}
}
UserDefaults.standard.set(bookmarks, forKey: "BookmarkArray")
print("书签保存成功")
} catch {
print("书签创建失败: \(error)")
}
}
}
Button("恢复书签") {
if let bookmarks = UserDefaults.standard.array(forKey: "BookmarkArray") as? [Data] {
tmpImages = []
for bookmark in bookmarks { var isStale = false
do {
let url = try URL(resolvingBookmarkData: bookmark, options: [.withSecurityScope], relativeTo: nil, bookmarkDataIsStale: &isStale)
if url.startAccessingSecurityScopedResource() {
print("获取书签保存的URL:\(url)")
if let selectedImg = NSImage(contentsOf: url) {
tmpImages.append(selectedImg)
} else {
print("获取书签URL失败")
}
url.stopAccessingSecurityScopedResource()
} else {
print("无法访问资源")
}
} catch {
print("解析书签失败: \(error)")
}
}
}
}
}
}
}
}