跳到主要内容

权限系统

Tauri 的权限系统是其安全模型的核心,确保前端代码只能访问明确授权的功能。理解并正确配置权限,对于构建安全的桌面应用至关重要。

为什么需要权限系统

Web 应用 vs 桌面应用

特性Web 应用桌面应用
运行环境浏览器沙箱操作系统
文件系统受限(File API)完全访问
系统 API受限完全访问
安全风险较低较高

桌面应用拥有更高的权限,也意味着更大的安全风险。Tauri 的权限系统通过以下方式降低风险:

  1. 最小权限原则:只授予必要的权限
  2. 显式授权:所有权限必须在配置中声明
  3. 运行时检查:每次 IPC 调用都验证权限

权限系统架构

┌─────────────────────────────────────────────────────────────┐
│ Capability File │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ Window: "main" │ │
│ │ Permissions: │ │
│ │ - fs:read → Scope: ["$APP/*", "$DOCUMENT/*"] │ │
│ │ - fs:write → Scope: ["$APP/*"] │ │
│ │ - dialog:open │ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│ Permission Check │
│ │
│ Frontend: invoke("read_file", { path: "/etc/passwd" }) │
│ │ │
│ ▼ │
│ Check: path in scope? → "/etc/passwd" ∉ ["$APP/*"] │
│ │ │
│ ▼ │
│ Result: ❌ Permission Denied │
└─────────────────────────────────────────────────────────────┘

配置文件

权限配置位于 src-tauri/capabilities/ 目录,使用 JSON 格式。

默认配置

src-tauri/capabilities/default.json

{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "默认权限配置",
"windows": ["main"],
"permissions": [
"core:default",
"fs:allow-read",
{
"identifier": "fs:scope",
"allow": [{ "path": "$APP" }, { "path": "$APP/**" }]
}
]
}

配置字段说明

字段类型说明
identifierstring唯一标识符
descriptionstring配置描述
windowsstring[]适用的窗口标签
permissionsarray权限列表

核心权限

内置权限标识符

Tauri 提供了一系列内置权限:

权限说明
core:default核心默认权限
core:path:default路径相关默认权限
fs:default文件系统默认权限
fs:allow-read允许读取文件
fs:allow-write允许写入文件
fs:allow-read-meta允许读取文件元数据
dialog:default对话框默认权限
dialog:allow-open允许打开文件对话框
dialog:allow-save允许保存文件对话框
notification:default通知默认权限
shell:defaultShell 默认权限
http:defaultHTTP 默认权限

权限范围(Scope)

许多权限支持通过 scope 限制操作范围:

{
"identifier": "fs:allow-read",
"allow": [
{ "path": "$APP" },
{ "path": "$APP/**" },
{ "path": "$DOCUMENT/**" }
]
}

路径占位符

Tauri 提供了特殊的路径占位符:

占位符说明示例
$APP应用数据目录C:\Users\User\AppData\Roaming\MyApp
$APPLOG应用日志目录C:\Users\User\AppData\Roaming\MyApp\logs
$APPCONFIG应用配置目录C:\Users\User\AppData\Roaming\MyApp\config
$APPLOCALDATA应用本地数据C:\Users\User\AppData\Local\MyApp
$DOCUMENT用户文档目录C:\Users\User\Documents
$DOWNLOAD下载目录C:\Users\User\Downloads
$HOME用户主目录C:\Users\User
$TEMP临时目录C:\Users\User\AppData\Local\Temp

多窗口权限配置

不同窗口可以有不同的权限配置:

capabilities/main.json

{
"identifier": "main",
"windows": ["main"],
"permissions": [
"core:default",
"fs:allow-read",
"dialog:allow-open"
]
}

capabilities/settings.json

{
"identifier": "settings",
"windows": ["settings"],
"permissions": [
"core:default",
"fs:allow-read",
"fs:allow-write"
]
}

自定义权限

为 Command 创建权限

1. 创建权限定义文件

src-tauri/permissions/my-permissions.json

{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "myapp:allow-custom-command",
"description": "允许执行自定义命令",
"windows": ["main"],
"permissions": []
}

2. 在 Rust 代码中声明

#[tauri::command]
#[specta::specta] // 如果使用 specta 生成类型
#[tauri::command_guard(myapp:allow-custom-command)] // 关联权限
fn custom_command(data: String) -> Result<String, String> {
Ok(format!("处理结果: {}", data))
}

3. 在 Capability 中引用

{
"permissions": [
"myapp:allow-custom-command"
]
}

权限检查流程

当前端调用 Command 时,Tauri 会执行以下检查:

1. 前端: invoke("read_file", { path: "/some/path" })

2. Tauri Core 接收请求

3. 查找对应的 Capability 配置
- 根据窗口标签找到匹配的 Capability

4. 检查权限列表
- Command 是否在 permissions 中?
- 如果是,继续
- 如果否,拒绝请求

5. 检查 Scope(如果有)
- 参数中的路径是否在 allow 列表中?
- 如果在,继续
- 如果不在,拒绝请求

6. 执行 Command

7. 返回结果给前端

常见权限配置示例

文件管理器应用

{
"identifier": "file-manager",
"windows": ["main"],
"permissions": [
"core:default",
"fs:allow-read",
"fs:allow-write",
"fs:allow-read-meta",
{
"identifier": "fs:scope",
"allow": [
{ "path": "$HOME" },
{ "path": "$HOME/**" },
{ "path": "$DOCUMENT" },
{ "path": "$DOCUMENT/**" },
{ "path": "$DOWNLOAD" },
{ "path": "$DOWNLOAD/**" }
]
},
"dialog:allow-open",
"dialog:allow-save"
]
}

笔记应用

{
"identifier": "note-app",
"windows": ["main"],
"permissions": [
"core:default",
"fs:allow-read",
"fs:allow-write",
"fs:allow-remove",
{
"identifier": "fs:scope",
"allow": [
{ "path": "$APP" },
{ "path": "$APP/**" },
{ "path": "$DOCUMENT/MyNotes" },
{ "path": "$DOCUMENT/MyNotes/**" }
]
},
"dialog:allow-open",
"dialog:allow-save",
"notification:default"
]
}

开发工具

{
"identifier": "dev-tool",
"windows": ["main"],
"permissions": [
"core:default",
"fs:default",
"shell:allow-execute",
{
"identifier": "shell:scope",
"allow": [
{ "name": "git", "cmd": "git" },
{ "name": "node", "cmd": "node" },
{ "name": "npm", "cmd": "npm" }
]
},
"http:default",
"notification:default",
"dialog:default",
"os:default"
]
}

调试权限问题

常见问题

1. 权限被拒绝

Error: failed to invoke command: permission denied

解决方法:

  • 检查 Capability 文件是否正确配置
  • 确认窗口标签匹配
  • 验证权限标识符拼写正确

2. 路径超出 Scope

Error: path not allowed

解决方法:

  • 检查路径是否在 allow 列表中
  • 使用正确的路径占位符
  • 确保路径格式正确(使用 / 而非 \

启用调试日志

tauri.conf.json 中启用详细日志:

{
"app": {
"security": {
"csp": "default-src 'self'"
}
},
"build": {
"beforeDevCommand": "RUST_LOG=debug npm run dev"
}
}

安全最佳实践

1. 最小权限原则

只授予应用必需的权限:

// ❌ 过于宽泛
{
"permissions": ["fs:default"] // 允许所有文件操作
}

// ✅ 最小权限
{
"permissions": [
"fs:allow-read",
{
"identifier": "fs:scope",
"allow": [{ "path": "$APP/**" }]
}
]
}

2. 限制 Scope

始终限制文件系统访问范围:

{
"identifier": "fs:allow-read",
"allow": [
{ "path": "$APP" },
{ "path": "$APP/**" }
],
"deny": [
{ "path": "$APP/secrets" },
{ "path": "$APP/secrets/**" }
]
}

3. 分离敏感操作

将敏感操作放在单独的 Capability 中:

// 普通操作
{
"identifier": "user-capability",
"windows": ["main"],
"permissions": ["fs:allow-read"]
}

// 管理员操作
{
"identifier": "admin-capability",
"windows": ["admin-panel"],
"permissions": ["fs:allow-read", "fs:allow-write", "shell:allow-execute"]
}

4. 验证用户输入

即使配置了权限,也要在 Command 中验证输入:

#[tauri::command]
fn read_file(app_handle: AppHandle, path: String) -> Result<String, String> {
// 解析路径
let path = PathBuf::from(&path);

// 获取应用目录
let app_dir = app_handle.path().app_dir()
.map_err(|e| e.to_string())?;

// 验证路径在允许范围内
let canonical_path = path.canonicalize()
.map_err(|_| "无效路径".to_string())?;

if !canonical_path.starts_with(&app_dir) {
return Err("路径超出允许范围".to_string());
}

// 读取文件
fs::read_to_string(&canonical_path)
.map_err(|e| e.to_string())
}

下一步