强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

第 05 章:权限系统

第 05 章:权限系统

5.1 为什么需要权限系统?

在 Node.js 中,任何脚本一旦运行就拥有完整的系统权限。这意味着:

npm install some-package
→ some-package 的 postinstall 脚本可以:
  ├── 读取 ~/.ssh/id_rsa(私钥)
  ├── 读取 ~/.aws/credentials(云凭证)
  ├── 访问所有环境变量
  ├── 连接任意网络地址(数据外泄)
  └── 删除任意文件

Deno 的权限系统从设计层面解决了这个问题——默认没有任何权限

deno run script.ts
→ script.ts 默认只能:
  ├── 执行纯计算(数学、字符串处理等)
  ├── 使用标准 JavaScript API
  └── 控制台输出(console.log)

→ 任何需要系统资源的操作必须显式授权

5.2 权限标志详解

完整权限标志列表

标志缩写说明可限制范围
--allow-read-r文件系统读取指定路径列表
--allow-write-w文件系统写入指定路径列表
--allow-net-n网络访问指定主机/端口
--allow-env-e环境变量访问指定变量名
--allow-run子进程执行指定命令
--allow-ffi动态库加载指定路径
--allow-sys系统信息查询指定 API
--allow-all-A所有权限

读写权限 (--allow-read / --allow-write)

# 允许读取所有文件
deno run --allow-read script.ts

# 允许读取特定目录
deno run --allow-read=/tmp,/home/user/data script.ts

# 允许写入特定文件
deno run --allow-write=/tmp/output.txt script.ts

# 组合使用
deno run --allow-read=/etc --allow-write=/tmp script.ts
// 精细权限演示
// 运行:deno run --allow-read=/tmp --allow-write=/tmp demo.ts

// ✅ 允许:读取 /tmp 下的文件
const data = await Deno.readTextFile("/tmp/test.txt");

// ❌ 拒绝:读取 /etc 下的文件
try {
  await Deno.readTextFile("/etc/hosts");
} catch (e) {
  console.error("权限不足:", e.message);
}

// ✅ 允许:写入 /tmp 下的文件
await Deno.writeTextFile("/tmp/output.txt", "Hello");

网络权限 (--allow-net)

# 允许所有网络访问
deno run --allow-net script.ts

# 允许特定主机
deno run --allow-net=api.github.com,deno.land script.ts

# 允许特定端口
deno run --allow-net=0.0.0.0:8000 script.ts

# 允许主机+端口组合
deno run --allow-net=api.example.com:443,localhost:3000 script.ts
// 运行:deno run --allow-net=api.github.com script.ts

// ✅ 允许:访问 GitHub API
const res1 = await fetch("https://api.github.com/users/denoland");
console.log("GitHub:", res1.status);

// ❌ 拒绝:访问其他域名
try {
  const res2 = await fetch("https://api.example.com/data");
} catch (e) {
  console.error("网络权限不足:", e.message);
}

环境变量权限 (--allow-env)

# 允许所有环境变量
deno run --allow-env script.ts

# 允许特定变量
deno run --allow-env=HOME,PATH,DB_HOST script.ts
// 运行:deno run --allow-env=HOME,USER script.ts

// ✅ 允许:读取 HOME
console.log("HOME:", Deno.env.get("HOME"));

// ❌ 拒绝:读取 DB_PASSWORD
try {
  console.log("DB:", Deno.env.get("DB_PASSWORD"));
} catch (e) {
  console.error("环境变量权限不足:", e.message);
}

子进程权限 (--allow-run)

# 允许运行 git 命令
deno run --allow-run=git script.ts

# 允许运行多个命令
deno run --allow-run=git,npm,ls script.ts
// 运行:deno run --allow-run=ls script.ts

// ✅ 允许:运行 ls 命令
const process = new Deno.Command("ls", { args: ["-la"] });
const output = await process.output();
console.log(new TextDecoder().decode(output.stdout));

// ❌ 拒绝:运行 rm 命令
try {
  const rm = new Deno.Command("rm", { args: ["/tmp/test"] });
  await rm.output();
} catch (e) {
  console.error("子进程权限不足:", e.message);
}

系统信息权限 (--allow-sys)

# 允许获取系统信息
deno run --allow-sys script.ts

# 允许特定 API
deno run --allow-sys=osRelease,memoryUsage script.ts
// 运行:deno run --allow-sys script.ts
console.log("操作系统:", Deno.osRelease());
console.log("内存使用:", Deno.memoryUsage());
console.log("系统架构:", Deno.build.arch);

外部函数接口权限 (--allow-ffi)

# 允许加载动态库
deno run --allow-ffi=/usr/lib/libsqlite3.so script.ts
// 运行:deno run --allow-ffi script.ts
const dylib = Deno.dlopen("/usr/lib/libsqlite3.so", {
  "sqlite3_libversion": { parameters: [], result: "pointer" },
});

5.3 权限检查 API

Deno 提供了运行时 API 来检查权限状态:

// 检查当前权限状态
const readPerm = await Deno.permissions.query({ name: "read", path: "/tmp" });
console.log("读取 /tmp 权限:", readPerm.state);
// "granted" | "denied" | "prompt"

// 检查网络权限
const netPerm = await Deno.permissions.query({ name: "net", host: "example.com" });
console.log("访问 example.com 权限:", netPerm.state);

// 检查环境变量权限
const envPerm = await Deno.permissions.query({ name: "env", variable: "HOME" });
console.log("读取 HOME 权限:", envPerm.state);

运行时请求权限

// 运行时动态请求权限(会弹出交互提示)
async function readFile(path: string): Promise<string | null> {
  // 先检查
  const status = await Deno.permissions.query({ name: "read", path });
  
  if (status.state === "granted") {
    return await Deno.readTextFile(path);
  }
  
  if (status.state === "prompt") {
    // 请求权限
    const result = await Deno.permissions.request({ name: "read", path });
    if (result.state === "granted") {
      return await Deno.readTextFile(path);
    }
  }
  
  return null;
}

const content = await readFile("/tmp/data.txt");
if (content) {
  console.log(content);
} else {
  console.error("无法读取文件:权限不足");
}

撤销权限

// 运行时撤销已授予的权限
await Deno.permissions.revoke({ name: "read", path: "/tmp" });

// 之后再访问 /tmp 将被拒绝
try {
  await Deno.readTextFile("/tmp/test.txt");
} catch (e) {
  console.error("权限已被撤销:", e.message);
}

5.4 权限配置文件

在 deno.json 中配置权限

// deno.json
{
  "tasks": {
    "dev": "deno run --watch --allow-net --allow-read=./data server.ts",
    "test": "deno test --allow-read --allow-write",
    "start": "deno run --allow-net --allow-env server.ts"
  }
}

权限配置最佳实践

// deno.json — 生产级配置
{
  "tasks": {
    "dev": "deno run --watch --allow-net=localhost:8000,api.example.com --allow-read=./src --allow-env=PORT,NODE_ENV server.ts",
    "test": "deno test --allow-read=./testdata --allow-write=./testdata/tmp",
    "lint": "deno lint",
    "fmt": "deno fmt"
  }
}

5.5 安全模型实践

场景 1:Web 服务器最小权限

// 只需要网络权限和少量环境变量
{
  "tasks": {
    "serve": "deno run --allow-net=0.0.0.0:8000 --allow-env=PORT,HOST server.ts"
  }
}

场景 2:文件处理脚本

{
  "tasks": {
    "process": "deno run --allow-read=./input --allow-write=./output process.ts"
  }
}

场景 3:数据库操作

{
  "tasks": {
    "db": "deno run --allow-net=localhost:5432 --allow-env=DB_URL --allow-read=./migrations db.ts"
  }
}

场景 4:自动化脚本(受信环境)

{
  "tasks": {
    "script": "deno run --allow-all script.ts"
  }
}

⚠️ 警告--allow-all 跳过所有安全检查,只在受信环境中使用。


5.6 权限与 npm 包

使用 npm 包时要特别注意权限:

// npm 包可能需要额外权限
import express from "npm:[email protected]";

const app = express();
app.get("/", (req, res) => res.send("Hello"));

// 需要网络权限
app.listen(3000);
# npm 包可能暗中读写文件、访问网络
# 建议先用最小权限测试,逐步放宽
deno run --allow-net=localhost:3000 app.ts

审计 npm 包权限需求

# 查看模块依赖图
deno info npm:[email protected]

# 查看具体文件列表
deno info --json npm:[email protected] | jq '.modules'

5.7 权限系统原理

┌─────────────────────────────────────────┐
│          你的代码                         │
│  await Deno.readTextFile("/etc/hosts")   │
├─────────────────────────────────────────┤
│        Deno Runtime API 层               │
│  1. 解析调用参数                          │
│  2. 检查权限状态                          │
│  3a. granted → 执行操作                   │
│  3b. prompt  → 弹出交互提示              │
│  3c. denied  → 抛出 PermissionDenied     │
├─────────────────────────────────────────┤
│        Rust 核心层                        │
│  实际执行系统调用                          │
└─────────────────────────────────────────┘

每个需要权限的 Deno API 调用都会经过权限检查器。检查器维护一个权限表:

// 权限表(运行时内部)
PermissionTable = {
  read: [{ granted: true, path: "/tmp" }],
  write: [{ granted: true, path: "/tmp" }],
  net: [{ granted: true, host: "api.example.com" }],
  env: [{ granted: true, variable: "HOME" }],
  // 未列出的权限默认 denied
}

5.8 本章小结

要点说明
默认安全Deno 默认没有任何系统权限
精细控制每个权限都可以限制到具体资源
运行时 API可以动态查询、请求和撤销权限
最佳实践始终使用最小必要权限
npm 兼容npm 包也需要遵守权限系统

权限标志速查表

deno run \
  --allow-read=/tmp,/data \          # 文件读取
  --allow-write=/tmp \               # 文件写入
  --allow-net=api.example.com \      # 网络访问
  --allow-env=DB_HOST,DB_PORT \      # 环境变量
  --allow-run=git \                  # 子进程
  --allow-sys \                      # 系统信息
  script.ts

📖 扩展阅读


下一章第 06 章:模块系统 → 了解 Deno 的 URL 导入、npm 兼容与导入映射。