权限系统
Tauri 的权限系统是其安全模型的核心,确保前端代码只能访问明确授权的功能。理解并正确配置权限,对于构建安全的桌面应用至关重要。
为什么需要权限系统
Web 应用 vs 桌面应用
| 特性 | Web 应用 | 桌面应用 |
|---|---|---|
| 运行环境 | 浏览器沙箱 | 操作系统 |
| 文件系统 | 受限(File API) | 完全访问 |
| 系统 API | 受限 | 完全访问 |
| 安全风险 | 较低 | 较高 |
桌面应用拥有更高的权限,也意味着更大的安全风险。Tauri 的权限系统通过以下方式降低风险:
- 最小权限原则:只授予必要的权限
- 显式授权:所有权限必须在配置中声明
- 运行时检查:每次 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/**" }]
}
]
}
配置字段说明
| 字段 | 类型 | 说明 |
|---|---|---|
identifier | string | 唯一标识符 |
description | string | 配置描述 |
windows | string[] | 适用的窗口标签 |
permissions | array | 权限列表 |
核心权限
内置权限标识符
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:default | Shell 默认权限 |
http:default | HTTP 默认权限 |
权限范围(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())
}