阅读视图

发现新文章,点击刷新页面。

Tauri 的 Capabilities 权限管理系统

一、为什么需要 Capabilities?

Tauri 应用的前端运行在系统 WebView 中,而后端则是 Rust 编写的原生代码。前端通过 Tauri 提供的 API 与后端通信,从而访问文件系统、窗口管理、系统托盘等原生能力。

问题在于:如果前端代码被攻破(比如 XSS 攻击),攻击者就可能利用这些 API 对用户系统造成危害。Capabilities 系统正是为了应对这类场景而设计的——它让开发者可以精确控制每个窗口或 WebView 能使用哪些权限,将"最小权限原则"落到实处。

二、核心概念

Capabilities 本质上是一组声明式的权限配置,用来定义哪些窗口(window)或 WebView 被授予或拒绝了哪些权限。几个关键特性值得注意:

  • 一个 Capability 可以同时作用于多个窗口或 WebView。
  • 一个窗口也可以被多个 Capability 引用。当窗口属于多个 Capability 时,所有相关 Capability 的权限会合并生效——这意味着安全边界会扩大,配置时需要格外小心。

三、配置方式

Capability 文件以 JSON 或 TOML 格式存放在 src-tauri/capabilities 目录下。Tauri 提供了两种主要的配置方式。

方式一:独立文件 + 引用标识符

这是推荐的做法。在 capabilities 目录下定义独立的 Capability 文件,然后在 tauri.conf.json 中通过标识符引用它们。

首先,定义一个 Capability 文件:

// src-tauri/capabilities/default.json
{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "main-capability",
  "description": "Capability for the main window",
  "windows": ["main"],
  "permissions": [
    "core:path:default",
    "core:event:default",
    "core:window:default",
    "core:app:default",
    "core:resources:default",
    "core:menu:default",
    "core:tray:default",
    "core:window:allow-set-title"
  ]
}

然后在配置文件中引用:

// src-tauri/tauri.conf.json
{
  "app": {
    "security": {
      "capabilities": ["my-capability", "main-capability"]
    }
  }
}

这种方式的好处是保持 tauri.conf.json 的简洁,同时让权限配置模块化、易于维护。

方式二:内联定义

对于简单场景,也可以直接在 tauri.conf.json 中内联定义 Capability,甚至将内联定义和引用混合使用:

{
  "app": {
    "security": {
      "capabilities": [
        {
          "identifier": "my-capability",
          "description": "My application capability used for all windows",
          "windows": ["*"],
          "permissions": ["fs:default", "allow-home-read-extended"]
        },
        "my-second-capability"
      ]
    }
  }
}

需要注意的是,capabilities 目录下的所有 Capability 文件默认自动启用。但一旦在 tauri.conf.json 中显式指定了 Capability,就只有被指定的那些会生效。

四、自定义命令的权限控制

默认情况下,通过 tauri::Builder::invoke_handler 注册的所有命令对所有窗口开放。如果你希望更精细地控制,可以在 build.rs 中使用 AppManifest::commands 来声明:

// src-tauri/build.rs
fn main() {
    tauri_build::try_build(
        tauri_build::Attributes::new()
            .app_manifest(
                tauri_build::AppManifest::new()
                    .commands(&["your_command"])
            ),
    )
    .unwrap();
}

五、平台特定配置

Capabilities 支持通过 platforms 字段限定作用的目标平台。可选值包括 linuxmacOSwindowsiOSandroid

一个面向桌面端的配置示例:

// src-tauri/capabilities/desktop.json
{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "desktop-capability",
  "windows": ["main"],
  "platforms": ["linux", "macOS", "windows"],
  "permissions": ["global-shortcut:allow-register"]
}

以及面向移动端的配置:

// src-tauri/capabilities/mobile.json
{
  "$schema": "../gen/schemas/mobile-schema.json",
  "identifier": "mobile-capability",
  "windows": ["main"],
  "platforms": ["iOS", "android"],
  "permissions": [
    "nfc:allow-scan",
    "biometric:allow-authenticate",
    "barcode-scanner:allow-scan"
  ]
}

这种设计让你可以为不同平台启用不同的插件能力,同时避免在不支持某些硬件的平台上引入无意义的权限。

六、远程 API 访问

默认情况下,Tauri API 只对随应用打包的本地代码开放。但在某些场景下,你可能需要让远程加载的页面也能调用部分 Tauri 命令。这可以通过 remote 配置实现:

// src-tauri/capabilities/remote-tags.json
{
  "$schema": "../gen/schemas/remote-schema.json",
  "identifier": "remote-tag-capability",
  "windows": ["main"],
  "remote": {
    "urls": ["https://*.tauri.app"]
  },
  "platforms": ["iOS", "android"],
  "permissions": ["nfc:allow-scan", "barcode-scanner:allow-scan"]
}

这里有一个重要的安全提示:在 Linux 和 Android 上,Tauri 无法区分来自嵌入式 <iframe> 的请求和窗口本身的请求。因此在使用远程 API 访问功能时,务必仔细评估安全影响。

七、安全边界:能做什么,不能做什么

理解 Capabilities 系统的安全边界至关重要。

它能防护的场景包括:最小化前端被攻破后的影响、防止或减少本地系统接口和数据的意外暴露、防止从前端到后端/系统的权限提升。

它无法防护的场景包括:恶意或不安全的 Rust 后端代码、过于宽松的 scope 配置、命令实现中未正确检查 scope、来自 Rust 代码的故意绕过、系统 WebView 的零日漏洞、供应链攻击或开发者环境被入侵。

另外,安全边界依赖于窗口的 label(标签),而非 title(标题)。建议只对高权限窗口开放窗口创建功能。

八、Schema 文件与 IDE 支持

Tauri 通过 tauri-build 自动生成 JSON Schema 文件,其中包含了应用可用的所有权限定义。在 Capability 配置文件中设置 $schema 属性后,你的 IDE 就能提供自动补全,大幅提升开发体验:

{
  "$schema": "../gen/schemas/desktop-schema.json"
}

Schema 文件位于 gen/schemas 目录下,通常使用 desktop-schema.jsonmobile-schema.json,也可以为特定平台定义专属的 Schema。

九、项目结构概览

一个典型的 Tauri 应用目录结构如下:

tauri-app
├── index.html
├── package.json
├── src/
├── src-tauri/
│   ├── Cargo.toml
│   ├── capabilities/
│   │   └── <identifier>.json/toml
│   ├── src/
│   └── tauri.conf.json

capabilities 目录存放所有的权限配置文件,每个文件以其 identifier 命名,职责清晰,便于团队协作和代码审查。

十、最佳实践总结

在实际项目中使用 Capabilities 系统时,有几条经验值得参考。首先,遵循最小权限原则,只为每个窗口授予它实际需要的权限。其次,善用独立文件管理——将 Capability 定义为独立文件,通过标识符引用,保持配置清晰。第三,谨慎处理多 Capability 窗口,因为权限会合并,可能意外扩大安全边界。第四,利用平台特定配置,避免在不适用的平台上暴露无意义的权限。最后,对远程 API 访问保持警惕,仔细评估安全影响,尤其是在 Linux 和 Android 上。

Tauri 的 Capabilities 系统体现了"安全默认"的设计哲学——默认情况下,前端的能力是受限的,开发者需要显式地授予权限。这种设计虽然增加了一些配置工作,但换来的是更可控、更安全的应用架构。对于任何关注用户安全的桌面/移动应用项目来说,花时间理解和正确配置这套系统,都是值得的。

Tauri 命令作用域(Command Scopes)精细化控制你的应用权限

一、为什么需要命令作用域?

试想这样一个场景:你的 Tauri 应用需要读取用户 $APPLOCALDATA 目录下的某些配置文件,于是你开放了文件读取命令。但问题来了——这个目录下同时存放着 WebView 的运行时数据(如 Cookies、IndexedDB、Session 信息),一旦被恶意前端代码读取,将造成严重的隐私泄露。

如果权限粒度只能精确到"命令级别",你只能在"全部放开"和"全部禁止"之间二选一。命令作用域(Command Scopes) 正是为解决这一问题而生——它允许你在开放某个命令的同时,精确约束这个命令能操作的资源边界。

二、作用域的核心概念

2.1 allow 与 deny

作用域分为两类,规则简洁而明确:

类型 含义
allow 显式允许命令操作的资源范围
deny 显式拒绝命令操作的资源范围

核心规则:deny 的优先级永远高于 allow 无论 allow 范围有多宽泛,只要资源命中了 deny 规则,访问就会被拒绝,没有任何例外。

2.2 作用域的类型系统

作用域的值类型必须是可被 serde 序列化的 Rust 类型,具体类型由各插件或应用自行定义。不同插件使用不同类型来描述"资源"的概念:

  • fs 插件:使用 glob 路径字符串(如 $HOME/**)描述文件系统路径
  • http 插件:使用 URL 字符串描述允许访问的网络地址

作用域由命令实现层接收并强制执行。这意味着命令开发者必须自行实现作用域校验逻辑,框架本身不会自动过滤。

⚠️ 安全警告:命令开发者有责任确保作用域校验逻辑不存在绕过漏洞(例如路径穿越攻击)。所有校验代码都应经过安全审计。

三、实战:fs 插件的作用域配置

下面以 Tauri 官方 fs 插件为例,完整演示作用域的配置方式。在这个插件中,作用域类型统一为 glob 路径字符串

3.1 定义允许范围:递归访问 APPLOCALDATA

# plugins/fs/permissions/autogenerated/base-directories/applocaldata.toml

[[permission]]
identifier = "scope-applocaldata-recursive"
description = '''
This scope recursive access to the complete $APPLOCALDATA folder,
including sub directories and files.
'''

[[permission.scope.allow]]
path = "$APPLOCALDATA/**"

这里有两个细节值得注意:

  • $APPLOCALDATA 是 Tauri 内置的路径变量,会在运行时被解析为平台对应的目录(Windows 下为 %LOCALAPPDATA%,Linux 下为 ~/.local/share
  • /** 是 glob 通配符,表示递归匹配该目录下所有子目录和文件。若只写 /*,则只匹配一层,不会深入子目录

3.2 定义拒绝范围:保护 WebView 敏感数据

WebView 引擎会在 $APPLOCALDATA 下存储用户会话、缓存等敏感数据,不同平台的存储路径有所差异,因此需要分平台配置拒绝规则:

# plugins/fs/permissions/deny-webview-data.toml

# ---- Linux 平台 ----
[[permission]]
identifier = "deny-webview-data-linux"
description = '''
This denies read access to the $APPLOCALDATA folder on linux as the webview
data and configuration values are stored here.
Allowing access can lead to sensitive information disclosure.
'''
platforms = ["linux"]

[[scope.deny]]
path = "$APPLOCALDATA/**"

# ---- Windows 平台 ----
[[permission]]
identifier = "deny-webview-data-windows"
description = '''
This denies read access to the $APPLOCALDATA/EBWebView folder on windows
as the webview data and configuration values are stored here.
'''
platforms = ["windows"]

[[scope.deny]]
path = "$APPLOCALDATA/EBWebView/**"

platforms 字段是这里的关键——同一个 .toml 文件中可以定义多条权限,每条权限可以通过 platforms 声明其生效的操作系统,做到跨平台差异化配置,无需为每个平台单独维护文件。

两条规则的差异体现了 Linux 和 Windows WebView 实现的不同:

  • Linux:整个 $APPLOCALDATA 都用于存储 WebView 数据,因此整体拒绝
  • Windows:只有 EBWebView 子目录存储 Edge WebView2 的数据,精准拒绝即可

四、分层组合:用权限集构建作用域体系

单个作用域权限如同零件,真正的工程实践是将它们有机组合。Tauri 推荐通过权限集(Permission Set) 进行分层组合,每一层都应有清晰的语义。

第一层:合并拒绝规则,建立安全基线

# plugins/fs/permissions/deny-default.toml

[[set]]
identifier = "deny-default"
description = '''
This denies access to dangerous Tauri relevant files and
folders by default.
'''
permissions = [
    "deny-webview-data-linux",
    "deny-webview-data-windows"
]

deny-default 将两个平台的拒绝规则合并,形成一个平台无关的安全基线。无论应用运行在哪个系统,引用这一个标识符就能自动应用正确的拒绝规则。

第二层:allow + deny 合并,形成合理的访问策略

[[set]]
identifier = "scope-applocaldata-reasonable"
description = '''
This scope set allows access to the APPLOCALDATA folder and subfolders
except for linux, while it denies access to dangerous Tauri relevant
files and folders by default on windows.
'''
permissions = [
    "scope-applocaldata-recursive",  # 允许递归访问
    "deny-default"                   # 但屏蔽危险路径
]

scope-applocaldata-reasonable 的命名本身就是一种设计表达——"合理的(reasonable)APPLOCALDATA 访问策略",在放开访问的同时内置了安全保障,引用者无需关心底层细节。

第三层:作用域 + 命令权限合并,形成完整功能单元

[[set]]
identifier = "read-files-applocaldata"
description = '''
This set allows file read access to the APPLOCALDATA folder and
subfolders except for linux, while it denies access to dangerous
Tauri relevant files and folders by default on windows.
'''
permissions = [
    "scope-applocaldata-reasonable",  # 作用域策略
    "allow-read-file"                 # 开放读取命令
]

read-files-applocaldata 是最终对外暴露的功能级权限集,语义完整、开箱即用:调用者只需引用这一个标识符,就能获得"在 APPLOCALDATA 下安全读取文件"的完整能力。

五、整体设计思路图解

在这里插入图片描述

这种分层设计的好处在于:

  • 关注点分离:allow 规则和 deny 规则各自独立维护
  • 复用性强deny-default 可被所有涉及 APPLOCALDATA 的权限集复用
  • 语义清晰:每一层的命名都能准确表达其意图
  • 易于审计:安全相关的拒绝规则集中管理,不会散落在各处

六、作用域的两种应用场景

配置好的作用域权限集,可以用于两种不同的作用范围:

场景一:全局作用域

将作用域权限集应用于插件的全局 scope,该插件的所有命令都会受到约束。适用于对整个插件统一设定资源访问边界的场景。

场景二:命令级作用域

将作用域权限与特定命令权限组合(如上文 read-files-applocaldata 的做法),仅对该命令生效。适用于不同命令需要不同资源访问策略的场景。

七、实践建议

设计作用域时:

  • glob 路径要严谨/* 只匹配当前层,/** 才会递归,根据实际需要选择,避免无意间开放过宽的权限
  • 始终配套 deny:任何开放系统目录访问的 allow 规则,都应搭配针对敏感子路径的 deny 规则
  • 平台差异显式化:用 platforms 字段将平台逻辑内聚在权限文件中,不要依赖外部条件判断

实现命令时:

  • 作用域校验不能省:框架传入 scope 数据,但校验必须由命令实现层主动执行
  • 防止路径穿越:对用户传入的路径参数进行规范化(canonicalize)后再与 scope 比对
  • 安全审计要落实:校验逻辑上线前应经过独立的代码审查,尤其是涉及文件系统和网络的命令

总结

Tauri 的命令作用域机制提供了远超传统"开/关"粒度的访问控制能力。其核心设计哲学可以归纳为三点:

  1. 精确授权allow 明确放行,deny 兜底屏蔽,两者组合实现精准的资源边界
  2. 分层复用:从原子作用域到安全基线,再到功能权限集,每一层都可独立复用
  3. 平台感知platforms 字段让同一套配置体系优雅地处理跨平台差异

对于构建安全 Tauri 应用的开发者来说,命令作用域是不可忽视的核心机制。合理设计作用域体系,不仅能提升应用安全性,也能让权限配置本身成为一份清晰的"资源访问说明书"。

Isolation Pattern(隔离模式)在前端与 Core 之间加一道“加密网关”,拦截与校验所有 IPC

一、Isolation Pattern 是什么

Isolation Pattern 的核心目标是:用 JavaScript 在 WebView 内部实现一层安全拦截器,把前端到 Core 的 IPC 通道变成“必须先过隔离层”的受控通道。

在这个模式下,前端应用并不是直接把消息交给 Core,而是:

  1. 先交给隔离应用(Isolation application)
  2. 隔离应用运行 hook,对消息做检查/改写
  3. 隔离应用将消息用 AES-GCM 加密(密钥每次启动动态生成)
  4. 加密后的消息再被转交给 Core 解密并处理

隔离应用本质上就是一段 被注入的安全 JS,它是 Isolation Pattern 的“执行载体”。

二、为什么要 Isolation:解决“前端不可信内容”的威胁模型

Isolation Pattern 诞生的动机非常现实:现代前端项目往往依赖庞大且深层嵌套的依赖树,风险来源包括但不限于:

  1. 构建期工具链(几十到上百个依赖)
  2. 运行时代码(打包进产物的依赖同样可能很多)
  3. 外部内容或第三方脚本(如果你加载了外部资源,风险更高)

这些都可能导致“前端执行环境不完全可信”。Isolation 的价值就在于:即使前端被污染,仍然能在进入 Core(拥有完整系统权限)之前做集中拦截与约束

三、什么时候用:官方强烈建议“能用就用”

Isolation Pattern 的一个特点是:它拦截的是“所有 IPC 消息”,因此几乎在任何应用里都能启用。

更进一步,官方建议当你使用外部 Tauri API(比如文件系统、HTTP、shell、窗口管理等)时,最好同时做更严格的“锁定(lockdown)”策略,并利用隔离应用进行输入验证,例如:

  1. 文件读写:限制路径必须落在应用允许的目录内,禁止越权访问
  2. HTTP 请求:限制 Origin、URL 白名单、Header 约束等
  3. 事件(Events):即使是“常开 API”,也能在隔离层拦截与验证,因为事件触发可能导致 Rust 执行敏感动作

四、怎么工作:sandbox iframe + AES-GCM 加密通道

Isolation Pattern 的机制可以概括为“三件事”:

  1. 隔离运行环境
    利用 <iframe sandbox> 的沙箱能力,让隔离应用在更受限、更可控的环境中运行,减少被主前端影响的概率。
  2. 强制消息经由隔离层
    Tauri 在页面加载时强制所有 IPC 调用先路由到隔离应用。
  3. 加密与动态密钥
    隔离应用使用浏览器 SubtleCrypto 对消息进行 AES-GCM 加密,再传回主前端并转交给 Core;Core 解密后像正常 IPC 一样处理。密钥每次应用启动都会重新生成,避免攻击者复用旧密钥对消息做离线篡改。

IPC 消息的大致流程(按步骤理解)

  1. IPC handler 收到前端消息
  2. IPC handler → 隔离应用
  3. 隔离应用 hook 执行(可修改/校验 payload)
  4. 隔离应用使用运行时生成密钥 AES-GCM 加密
  5. 隔离应用 → IPC handler(传回加密消息)
  6. IPC handler → Tauri Core(Core 解密并处理)

五、性能影响:有开销,但通常可忽略

Isolation 相比默认模式会增加:

  1. 每条 IPC 的加解密开销(AES-GCM)
  2. 启动时生成一次安全随机密钥(依赖系统熵池)

大多数应用不会明显感知这点开销,因为 AES-GCM 很快;真正需要关注的是“极致性能敏感应用”,它们往往依赖更少、攻击面更小,也更容易接受其它模式。

如果你在无头环境做 WebDriver 集成测试,可能会遇到熵不足导致启动慢的问题;这时候需要在系统层补足熵源(例如 Linux 环境的相关服务/机制)。

六、限制与坑点:Windows 的 sandbox iframe 外部文件加载限制

一个最重要的限制来自平台不一致:Windows 上 sandbox iframe 对外部文件加载存在问题。因此 Tauri 在构建时做了“脚本内联(script inlining)”步骤:

  • 传统 <script src="index.js"></script> 这种相对路径脚本会被内联处理,从而正常工作
  • 但较新的机制例如 ES Modulestype="module" / import ...)在隔离应用里可能无法正常加载

结论是:隔离应用尽量用“老实的、最简单的脚本加载方式”,别追求花哨打包玩法。

七、官方建议:隔离应用要极简、低依赖

Isolation 的目标是抵御“开发期/供应链威胁”,所以隔离应用本身也要避免成为新的供应链入口。

推荐做法:

  1. 逻辑尽量少:只做校验、白名单、简单转换
  2. 依赖尽量少:最好零依赖或极少依赖
  3. 构建步骤尽量少:减少额外工具链带来的风险

八、手把手示例:创建最小 Isolation Application

假设你主前端的 frontendDist../dist,隔离应用输出到 ../dist-isolation

1)隔离应用:../dist-isolation/index.html

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Isolation Secure Script</title>
  </head>
  <body>
    <script src="index.js"></script>
  </body>
</html>

2)隔离应用:../dist-isolation/index.js

window.__TAURI_ISOLATION_HOOK__ = (payload) => {
  // 不做校验,仅打印并原样返回
  console.log('hook', payload);
  return payload;
};

这个 hook 是你真正写安全逻辑的地方:你可以在这里做参数校验、路径约束、Origin 校验、命令白名单等。

九、启用 Isolation Pattern:tauri.conf.json 配置

{
  "build": {
    "frontendDist": "../dist"
  },
  "app": {
    "security": {
      "pattern": {
        "use": "isolation",
        "options": {
          "dir": "../dist-isolation"
        }
      }
    }
  }
}

含义非常直观:

  1. 主前端资源在 ../dist
  2. security pattern 使用 isolation
  3. 隔离应用目录在 ../dist-isolation

从 Tauri 2.0 Beta 升级到 2.0 Release Candidate Capabilities 权限前缀与内置 Dev Server 网络策略变

1. 一句话结论:这次升级你要重点改哪两处

Capabilities(权限配置)
旧写法:path:defaultwindow:default ……
新写法:要么统一加 core: 前缀,例如 core:path:default
要么直接用一个更省事的集合权限:core:default

内置开发服务器(尤其 iOS 真机)
以前你可能通过 TAURI_ENV_PLATFORM 判断 android/ios,再去暴露 0.0.0.0、算内网 IP、配 HMR
现在更推荐直接用 TAURI_DEV_HOST:能连 localhost 就别搞全网暴露;需要暴露时,Tauri 会告诉你应该用哪个 host

2. 自动迁移优先:先跑 migrate,再做人工复核

官方建议的升级方式是先用 v2 CLI 的 migrate 做自动改动,然后再手动处理剩余 breaking changes。

cargo install tauri-cli --version "^2.0.0" --locked
cargo tauri migrate

建议你把这条命令放在一个独立分支上跑(比如 chore/tauri-rc-migrate),这样 diff 会非常清晰。

迁移后你至少要人工检查两件事:

  • src-tauri/capabilities/*.json(或相应能力文件)里的 permissions 是否符合新规则
  • 前端 dev server 配置是否已切换到 TAURI_DEV_HOST 逻辑

3. Breaking Change 1:核心插件权限标识统一加 “core:” 前缀

3.1 发生了什么

从 beta 到 RC,Tauri 调整了“内置核心插件(core plugins)”在 capabilities 里被引用的方式。
你原来 capability 里写的这些:

"permissions": [
  "path:default",
  "event:default",
  "window:default",
  "app:default",
  "image:default",
  "resources:default",
  "menu:default",
  "tray:default"
]

需要改成加 core: 前缀:

"permissions": [
  "core:path:default",
  "core:event:default",
  "core:window:default",
  "core:app:default",
  "core:image:default",
  "core:resources:default",
  "core:menu:default",
  "core:tray:default"
]

3.2 更推荐的写法:直接用 core:default

为了减少样板代码,RC 增加了一个“特殊集合权限”core:default,它包含所有 core plugins 的默认权限。

也就是说,上面那一长串可以直接写成:

"permissions": [
  "core:default"
]

推荐策略:

  • 你只是需要 core 插件的默认能力:用 core:default,最省心
  • 你项目对权限非常敏感,需要精确控制:继续按需列出 core:* 的细粒度权限

迁移验收点:

  • 升级后如果出现“前端调用 core API 失败 / 权限不足”的报错,第一时间就去看 capability 文件里是不是还残留 path:default 这种旧标识

4. Breaking Change 2:内置开发服务器网络暴露策略改变(移动端调试更安全)

4.1 发生了什么

RC 对内置移动端开发服务器做了网络暴露策略调整:
移动端开发服务器不再默认“全网暴露并转发流量”,而是更倾向于让设备通过更安全、更直接的方式连接到本机服务。

这会直接影响你的前端 dev server 配置方式,因为以前很多模板都在做这些事:

  • host: '0.0.0.0'(让局域网设备都能连)
  • internal-ip 算出本机 IPv4 作为 HMR host
  • TAURI_ENV_PLATFORM 判断 android/ios 就强制启用“移动端模式”

RC 的思路是:能用 localhost 就用 localhost,只有在必要时才用特定 host,并且把这个 host 通过 TAURI_DEV_HOST 传给前端工具链。

4.2 iOS 真机特别说明:TAURI_DEV_HOST + Xcode TUN 地址

目前这个“更安全的连接方式”在 iOS(直接连真机或从 Xcode 跑)场景下不一定能自动生效。官方给的解决方案思路是:
1)打开 Xcode,让 macOS 和 iOS 设备建立连接通道
2)运行 tauri ios dev --force-ip-prompt
3)选择 iOS 设备的 TUN 地址(通常结尾是 ::2

这部分你不需要把细节写死在 Vite 配置里,关键是:让前端 dev server 读取 TAURI_DEV_HOST

5. Vite 迁移:从 “internal-ip + TAURI_ENV_PLATFORM” 到 “TAURI_DEV_HOST”

这是最实用的一段,因为它直接决定你真机调试是否顺畅。

5.1 Beta 常见写法(旧)

核心特征:

  • 通过 TAURI_ENV_PLATFORM 判断 mobile
  • mobile 时用 0.0.0.0
  • HMR host 用 internal-ip 算出来
import { defineConfig } from 'vite';
import { svelte } from '@sveltejs/vite-plugin-svelte';
import { internalIpV4Sync } from 'internal-ip';

const mobile = !!/android|ios/.exec(process.env.TAURI_ENV_PLATFORM);

export default defineConfig({
  plugins: [svelte()],
  clearScreen: false,
  server: {
    host: mobile ? '0.0.0.0' : false,
    port: 1420,
    strictPort: true,
    hmr: mobile
      ? {
          protocol: 'ws',
          host: internalIpV4Sync(),
          port: 1421,
        }
      : undefined,
  },
});

5.2 RC 推荐写法(新)

核心特征:

  • 直接读取 TAURI_DEV_HOST
  • 有 host 才暴露网络与启用 HMR host
  • 不再需要 internal-ip 依赖
  • 示例里 HMR 端口从 1421 改到了 1430(按你的内容照搬)
import { defineConfig } from 'vite';
import { svelte } from '@sveltejs/vite-plugin-svelte';

const host = process.env.TAURI_DEV_HOST;

export default defineConfig({
  plugins: [svelte()],
  clearScreen: false,
  server: {
    host: host || false,
    port: 1420,
    strictPort: true,
    hmr: host
      ? {
          protocol: 'ws',
          host: host,
          port: 1430,
        }
      : undefined,
  },
});

迁移验收点:

  • 你的 package.json 里可以去掉 internal-ip
  • 真机调试时,如果 Tauri 注入了 TAURI_DEV_HOST,HMR 也会跟着正确走
  • 如果你之前硬编码了 0.0.0.0,升级后建议先移除,避免无意义扩大暴露面

6. 升级后的快速自检清单(5 分钟验收)

1)Capabilities 是否已迁移

  • 搜索 capability 文件里是否还有 path:default 这种旧值
  • 优先改成 core:defaultcore:*:default

2)前端 dev server 是否已切 TAURI_DEV_HOST

  • 搜索 TAURI_ENV_PLATFORMinternal-ipinternalIpV4Sync
  • 替换为 const host = process.env.TAURI_DEV_HOST

3)真机(尤其 iOS)是否能连上 dev server

  • 若连接失败,按官方提示用 Xcode 建连接后 tauri ios dev --force-ip-prompt 选 TUN ::2 地址
  • 确认此时 TAURI_DEV_HOST 有值,并被 Vite 使用

7. 常见踩坑与解决思路

权限全挂但你以为是代码问题
表现:invoke / core API 提示权限不足或无权限
处理:先别改业务代码,先把 capabilities 的 core 前缀或 core:default 处理好

HMR 能开但页面不热更
表现:能打开页面但热更新没反应
处理:看 Vite server.hmr.host 是否仍然是旧的内网 IP 逻辑;改为 TAURI_DEV_HOST

依赖没删干净导致 lockfile 混乱
表现:internal-ip 还在,或者打包时出现 node 依赖冲突
处理:删除 internal-ip、重新安装依赖,确保 lockfile 与新配置一致

Tauri 1.0 升级到 Tauri 2.0从“能跑”到“跑得稳”的迁移实战指南(含移动端准备、配置重构、插件化 API、权限系统)

1. 为什么 Tauri 2.0 的升级不是“改个版本号”那么简单

Tauri 2.0 的变化核心有三类:

  1. 配置体系大重构
    tauri.conf.json 的结构变化非常大:顶层字段搬家、tauri 键改名为 app、allowlist 被移除、updater/cli 等移动到插件体系。
  2. API 全面插件化
    以前 @tauri-apps/api 里很多模块(fs/http/shell/notification…)属于“内置模块”,2.0 里这些大多变成插件(Rust 侧 + JS 侧都要装)。
  3. 权限系统(Capabilities)取代 allowlist
    v1 的 allowlist 被替换为更精细的 ACL 模型:可以按窗口、域名、命令、scope 精确放行/拒绝,并且通过 src-tauri/capabilities/ 下的能力文件生效。

这意味着:升级并不难,但必须“按模块拆解迁移”,否则就会出现典型的“编译过了但功能没了”。

2. 升级前的准备清单(强烈建议先做)

  1. 锁定当前可工作的 v1 基线
    打一个 tag:v1-stable
    确保你能随时回滚。
  2. 清点你用到的 v1 API/功能点
    至少列出这些关键项(后面会对应到插件迁移):
  • fs / path / http / shell / notification / clipboard / updater / process / os / cli / global-shortcut
  • tray / menu
  • 自定义协议(asset protocol、pattern、scope)
  • allowlist(尤其是 sidecar / fs scope / shell open / process command)
  1. 清点你当前的配置文件(tauri.conf.json)
    把老结构保存一份,迁移时你会频繁对照字段搬家。

3. 移动端准备:把 crate 变成“可共享库”

如果你的目标包含移动端(Android/iOS),2.0 的移动端接口要求项目能输出共享库,所以需要做这些结构调整。

3.1 修改 Cargo.toml:输出库产物

src-tauri/Cargo.toml 追加:

[lib]
name = "app_lib"
crate-type = ["staticlib", "cdylib", "rlib"]

含义很直白:同一套 Rust 入口需要同时服务桌面端可执行文件和移动端的库载入。

3.2 入口文件拆分:main.rs 变薄,lib.rs 变“通用入口”

  1. src-tauri/src/main.rs 重命名为 src-tauri/src/lib.rs
  2. main() 改成通用的 run(),并加上移动端入口宏:
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    // your code here
}

tauri::mobile_entry_point 会让这个入口在移动端以正确方式被调用。

3.3 重建 main.rs:桌面端只负责调用 run()

重新创建 src-tauri/src/main.rs

#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

fn main() {
  app_lib::run();
}

迁移到这里,你就完成了“同一份 Rust 逻辑,多端复用”的基础改造。

4. 自动迁移:migrate 命令能省力,但不能偷懒

官方给的警告很重要:migrate 不是替代指南,它只是帮你自动做“机械改动”,真正的功能迁移(插件、权限、业务代码)仍然要你手动完成。

4.1 安装 v2 CLI 并运行 migrate

cargo install tauri-cli --version "^2.0.0" --locked
cargo tauri migrate

它通常会帮你做:

  • 部分配置字段的迁移
  • 把 v1 allowlist 解析成 capability 文件的“初稿”

但注意:生成出来的 capability 是否符合你真实的窗口/域名/sidecar 场景,需要你自己复核。

5. 配置文件迁移:tauri.conf.json 的字段搬家地图

下面这一段是升级里最容易“看起来没报错,但运行行为变了”的地方。你贴的 Summary of Changes 很全,我把它整理成“迁移动作”。

5.1 顶层结构变化

  • package > productNamepackage > version 移到顶层
  • package 节点整体移除
  • tauri 键改名为 app
  • tauri > bundle 移到顶层 bundle
  • tauri > bundle > identifier 移到顶层(通常与 bundle 标识相关)

5.2 二进制名称不再自动跟随 productName

v1 里经常“产品名改了,生成的可执行文件名也跟着变”。
v2 不会自动改了,你必须加一个 mainBinaryName,并保证它与 productName 对齐(否则打包/运行时可能找不到主程序)。

5.3 allowlist 移除:改成 Permissions / Capabilities

  • tauri > allowlist 被移除
    迁移方向:创建 src-tauri/capabilities/ 下的能力文件(migrate 命令会帮你生成雏形),再按插件逐项放行。

5.4 安全相关字段的移动

  • tauri > allowlist > protocol > assetScopeapp > security > assetProtocol > scope
  • tauri > patternapp > security > pattern
  • tauri > windows > fileDropEnabledapp > windows > dragDropEnabled
  • build > withGlobalTauriapp > withGlobalTauri
  • build > distDirfrontendDist
  • build > devPathdevUrl

5.5 插件配置位置变化

  • tauri > cliplugins > cli
  • tauri > updaterplugins > updater
  • tauri > updater > active/dialog 等旧字段移除(2.0 updater 行为不同,见后文)
  • tauri > systemTrayapp > trayIcon

5.6 bundle 子结构重命名(按 OS 归档)

  • tauri > bundle > dmgbundle > macOS > dmg
  • tauri > bundle > debbundle > linux > deb
  • tauri > bundle > appimagebundle > linux > appimage
  • Windows WebView Runtime 字段变化:
    bundle > windows > webviewFixedRuntimePath 移除 → 用 bundle > windows > webviewInstallMode

5.7 Updater 的兼容性坑:createUpdaterArtifacts 与 v1Compatible

  • 新增 bundle > createUpdaterArtifacts
    如果你之前已经分发过 v1 应用,并且希望用户能“从旧版本升级到新版本”,通常需要设置为 v1Compatible(它影响升级包生成方式)。这是“升级后用户收不到更新”的高发原因之一。

6. Rust 侧变化:API 模块被拆成插件,很多熟悉的路径都没了

6.1 Cargo feature 变化

新增:

  • linux-protocol-body(需要 webkit2gtk 2.40)用于自定义协议 request body 解析

移除/更名(重点):

  • process-command-apishell-open-api:都改用 tauri-plugin-shell
  • updater:Updater 变成插件
  • system-tray:改名为 tray-icon
  • windows7-compat:移动到 notification 插件

6.2 Rust crate API 变化(你最可能遇到的报错来源)

以下是典型“编译报错 → 对应迁移方向”:

  • tauri::api::* 模块整体移除:每个 API 去对应插件里找

    • tauri::api::dialogtauri-plugin-dialog
    • tauri::api::httptauri-plugin-http
    • tauri::api::shell / tauri::api::process::Commandtauri-plugin-shell
    • tauri::updatertauri-plugin-updater
    • tauri::api::notificationtauri-plugin-notification
  • tauri::api::file:直接用 Rust 的 std::fs

  • tauri::api::pathtauri::PathResolver:迁移到 tauri::Manager::path

示例(path 迁移):

use tauri::{path::BaseDirectory, Manager};

tauri::Builder::default()
  .setup(|app| {
      let home = app.path().home_dir().expect("failed to get home dir");
      let p = app.path().resolve("path/to/something", BaseDirectory::Config)?;
      Ok(())
  });

6.3 Menu/Tray 重构(muda + 新 builder)

Menu:改到 tauri::menu,大量旧类型移除,换成 Builder 风格(你贴的 MenuBuilder / MenuItemBuilder / SubmenuBuilder 就是迁移模板)。

Tray:SystemTray 全部改名为 TrayIcon(更一致),事件拆成:

  • on_menu_event
  • on_tray_icon_event

你可以直接套用你贴的 TrayIconBuilder 示例结构去改。

7. JavaScript 侧变化:@tauri-apps/api “瘦身”,其他都去插件包

v2 的结论非常明确:

  • @tauri-apps/api 只保留:core(原 tauri)、path、event、window/webviewWindow
  • 其他模块:fs/http/shell/os/process/notification/dialog/clipboard/updater/cli/global-shortcut 全部变成 @tauri-apps/plugin-*

7.1 最常见的一刀:@tauri-apps/api/tauri → @tauri-apps/api/core

import { invoke } from "@tauri-apps/api/tauri"
import { invoke } from "@tauri-apps/api/core"

这一步非常常见,很多项目第一处报错就来自这里。

7.2 插件迁移的统一模式(记住这个模板就够了)

每个插件基本都要做三件事:

  1. Rust Cargo 加依赖:tauri-plugin-xxx = "2"
  2. Rust Builder 初始化:.plugin(tauri_plugin_xxx::init()) 或 Builder 风格
  3. 前端 npm/pnpm/yarn 加依赖:@tauri-apps/plugin-xxx
  4. 前端 import 改到插件

比如 Dialog:

Rust:

tauri::Builder::default()
  .plugin(tauri_plugin_dialog::init())

前端:

import { save } from '@tauri-apps/plugin-dialog';

你贴的 Clipboard/CLI/FS/HTTP/Notification/Process/Shell/Updater/OS/Global Shortcut 都是同样套路。

7.3 FS 插件的 API 改名坑

v1 常见函数重命名如下(你贴的很关键):

  • createDirmkdir
  • readBinaryFilereadFile
  • writeBinaryFilewriteFile
  • removeDir/removeFileremove
  • renameFilerename
  • Dir 别名移除 → 用 BaseDirectory

如果你迁移后“明明装了插件但类型/函数不存在”,通常就是这里没改干净。

8. 权限系统迁移:Capabilities 取代 allowlist(这是 v2 的安全核心)

v1 allowlist 只是一种“模块级开关”。
v2 Capabilities 是真正的 ACL:可以精确到“允许哪个窗口在什么域名下调用哪个命令,scope 是什么”。

你必须做的事情:

  • src-tauri/capabilities/ 创建能力文件(migrate 会帮你生成初稿)
  • 按插件逐项配置允许项
  • 如果你有 sidecar、remote URL、多窗口(甚至未来 multiwebview),能力文件要同步升级,否则功能会“静默失效”

一句话经验:
迁移成功的标准不是“能编译”,而是“权限文件覆盖了你真实的调用路径”。

9. 事件系统、窗口系统、Windows origin:升级后行为变化最大的三块

9.1 事件系统:emit 默认广播,新增 emit_to / emitTo

  • emit() 现在会发给所有监听器
  • 新增 emit_to/emitTo:定向发给某个 target
  • listen_globallisten_any
  • JS 的 event.listen() 行为更接近 listen_any(除非你设置 target)

如果你原来依赖“按窗口隔离事件”,升级后要重点检查:有没有出现“别的窗口也收到了消息”。

9.2 Window → WebviewWindow(为 multiwebview 做铺垫)

Rust:

  • Window 改名为 WebviewWindow
  • WindowBuilderWebviewWindowBuilder
  • WindowUrlWebviewUrl
  • get_windowget_webview_window

JS:

  • @tauri-apps/api/window 改到 @tauri-apps/api/webviewWindow

9.3 Windows 生产环境 origin 变了:tauri.localhost

v2 在 Windows 生产环境下,前端文件从 https://tauri.localhost 变成 http://tauri.localhost。直接影响:

  • IndexedDB / LocalStorage / Cookies 可能被重置(因为 origin 变了)

解决方案(你贴的官方建议):

  • 设置 app > windows > useHttpsScheme 为 true
    或 Rust 侧用 WebviewWindowBuilder::use_https_scheme 保持 https 方案。

这是那种“用户升级后发现登录态没了”的典型坑,务必提前处理。

10. Updater 迁移:默认自动弹窗没了,你必须自己做更新流程

v2 移除了“内置的自动检查 + 内置弹窗”。
如果你不手动实现检查与安装,你的用户可能永远不会再收到更新。

迁移路径:

  • Rust:tauri-plugin-updater = "2".plugin(tauri_plugin_updater::Builder::new().build())
  • JS:@tauri-apps/plugin-updater,自己调用 check(),然后 downloadAndInstall(),最后用 @tauri-apps/plugin-processrelaunch() 重启。

你贴的示例就是标准实现模板。

11. 环境变量改名:CI/CD、签名、打包脚本要同步更新

这一块特别容易“本地能打包,CI 全挂”。

你贴的改名清单建议直接做两件事:

  1. 搜索你仓库里的旧 env 名称(GitHub Actions / GitLab CI / Jenkinsfile / bat/sh 脚本)
  2. 全量替换为新名称(例如 TAURI_PRIVATE_KEYTAURI_SIGNING_PRIVATE_KEY 等)

如果你有签名/自动发布流水线,这一步是必做项。

12. 迁移路线建议:按“最小可用”分阶段推进

为了避免一次性改太多导致不可控,我建议你按这个顺序做(每一步都能形成可运行状态):

阶段 A:基础可编译 + 前端可打开

  • CLI/依赖升级
  • 配置文件结构迁移(字段搬家、mainBinaryName、devUrl/frontendDist)
  • JS 的 core 模块改名(tauri → core)
  • Rust 入口整理(必要时把 run() 抽到 lib.rs,为移动端留口)

阶段 B:插件逐个恢复功能(每次迁移一个插件就验收一次)

  • dialog / fs / shell / http / notification / os / process / clipboard / cli / global-shortcut / updater

阶段 C:权限系统补齐

  • capability 文件审核与加固
  • sidecar、remote domain、多窗口 target 范围验证

阶段 D:行为变化专项验证

  • 事件系统是否广播导致串扰
  • Windows origin 变化导致存储丢失(useHttpsScheme)
  • tray/menu 行为是否符合预期

13. 最后给你一个“迁移验收 Checklist”(建议复制到你的 PR 描述里)

  • cargo tauri dev 正常启动
  • 生产构建能打包(Windows/macOS/Linux 各自至少跑一遍)
  • mainBinaryNameproductName 配置正确
  • @tauri-apps/api/tauri 全部替换为 @tauri-apps/api/core
  • 用到的 v1 模块全部替换为对应插件,并完成 Rust .plugin(...) 初始化
  • src-tauri/capabilities/ 存在且覆盖你真实调用(特别是 fs/shell/sidecar/updater)
  • Updater 已实现手动检查与安装,否则用户后续无法更新
  • Windows 下如需保留 https origin,已配置 useHttpsScheme
  • 事件系统升级后,窗口间事件不串台(或已改用 emit_to)
❌