JS Deno `Permissions` API:运行时安全权限管理

咳咳,各位观众老爷们,晚上好!我是今晚的讲座主持人,代号“码农甲”。咱们今晚聊点刺激的,关于Deno的Permissions API,也就是Deno的运行时安全权限管理。

先说清楚,搞编程的,尤其是搞安全编程的,最怕的就是“未经授权的操作”。这就像你去别人家做客,没经过主人同意,就翻箱倒柜,那肯定是要挨揍的。Deno的Permissions API就是来管这个“挨揍”风险的,它让你在运行时,可以细粒度地控制你的代码能干啥、不能干啥。

一、 啥是Deno的权限模型?为啥需要它?

Deno从一开始就强调安全性。它默认情况下是“安全”的,也就是说,你的Deno程序在启动的时候,啥权限都没有。它不能访问文件系统,不能访问网络,不能运行子进程,啥也不能干。就像被关在笼子里的小鸟,只能唱唱歌,不能飞出去。

这听起来好像很麻烦,啥都干不了,但好处是显而易见的:

  • 安全第一:防止恶意代码搞破坏。想象一下,如果你运行了一个npm包,它偷偷地把你的SSH密钥上传到某个服务器,你就惨了。Deno的权限模型可以避免这种情况发生。
  • 可控性:你可以精确地控制你的代码能访问哪些资源。这对于构建安全的、可信赖的应用至关重要。
  • 透明性:权限是显式声明的,而不是隐式继承的。这意味着你可以清楚地知道你的代码需要哪些权限才能运行。

那为啥要用Permissions API呢?直接用命令行参数不行吗?

命令行参数当然可以控制权限,比如--allow-read--allow-net等等。但是,命令行参数是静态的,也就是说,它们在程序启动的时候就确定了,不能在程序运行的过程中改变。

但有些场景下,我们需要动态地控制权限。比如说:

  • 按需授权:我们只想在某个特定的函数里允许访问网络,而不是整个程序。
  • 用户授权:我们想让用户来决定是否允许某个操作,比如访问摄像头。
  • 权限升级:在特定的情况下,我们需要临时提升权限,比如安装依赖的时候。

Permissions API就是为了解决这些问题而生的。它允许我们在运行时,动态地请求、授予、撤销权限。

二、 Permissions API的核心概念

Permissions API的核心是Deno.permissions对象。它提供了一系列的方法,用于查询和管理权限。

主要涉及几个关键点:

  1. 权限名称:Deno定义了一系列的权限名称,比如readwritenetenvpluginhrtimerunffisys等等。每种权限对应着一种特定的操作。

    权限名称 描述
    read 允许读取文件系统。可以使用 --allow-read 命令行参数或者 Deno.permissions.request({ name: 'read' }) 来授予此权限。
    write 允许写入文件系统。可以使用 --allow-write 命令行参数或者 Deno.permissions.request({ name: 'write' }) 来授予此权限。
    net 允许访问网络。可以使用 --allow-net 命令行参数或者 Deno.permissions.request({ name: 'net' }) 来授予此权限。
    env 允许访问环境变量。可以使用 --allow-env 命令行参数或者 Deno.permissions.request({ name: 'env' }) 来授予此权限。
    plugin 允许加载插件。可以使用 --allow-plugin 命令行参数或者 Deno.permissions.request({ name: 'plugin' }) 来授予此权限。
    hrtime 允许高精度时间测量。可以使用 --allow-hrtime 命令行参数或者 Deno.permissions.request({ name: 'hrtime' }) 来授予此权限。
    run 允许运行子进程。可以使用 --allow-run 命令行参数或者 Deno.permissions.request({ name: 'run' }) 来授予此权限。
    ffi 允许调用外部函数接口 (FFI)。需要 --allow-ffi 命令行参数。
    sys 允许访问系统信息。需要 --allow-sys 命令行参数。
  2. 权限状态:每种权限都有一个状态,可以是granted(已授权)、denied(已拒绝)或者prompt(需要询问用户)。

  3. Deno.permissions.query()方法:用于查询权限的状态。它接受一个包含name属性的对象作为参数,返回一个Promise,resolve的值是一个包含state属性的对象。

    const status = await Deno.permissions.query({ name: "read", path: "/tmp" });
    console.log(status); // 输出:{ state: "granted" } 或者 { state: "denied" } 或者 { state: "prompt" }
  4. Deno.permissions.request()方法:用于请求权限。它接受一个包含name属性的对象作为参数,返回一个Promise,resolve的值是一个包含state属性的对象。如果权限状态是prompt,Deno会询问用户是否允许授予权限。

    const status = await Deno.permissions.request({ name: "net" });
    console.log(status); // 输出:{ state: "granted" } 或者 { state: "denied" }
  5. 权限描述符:权限描述符不仅仅包含name,还可以包含其他属性,比如path(对于readwrite权限)、host(对于net权限)等等。这些属性用于更精确地指定权限的范围。

    // 只允许读取/tmp目录下的文件
    const status = await Deno.permissions.request({ name: "read", path: "/tmp" });
    
    // 只允许访问example.com
    const status = await Deno.permissions.request({ name: "net", host: "example.com" });

三、 权限使用示例:代码说话!

光说不练假把式,我们来几个实际的例子,看看Permissions API怎么用。

1. 读取文件(动态权限)

假设我们有一个函数,需要读取一个文件,但是我们不想一开始就授予整个程序的read权限,而是只在调用这个函数的时候才请求权限。

async function readFile(filePath: string): Promise<string> {
  // 先检查是否有读取文件的权限
  const status = await Deno.permissions.query({ name: "read", path: filePath });

  if (status.state === "granted") {
    // 如果已经授权,直接读取文件
    return await Deno.readTextFile(filePath);
  } else if (status.state === "prompt") {
    // 如果需要询问用户,则请求权限
    const requestStatus = await Deno.permissions.request({ name: "read", path: filePath });
    if (requestStatus.state === "granted") {
      // 如果用户同意授权,则读取文件
      return await Deno.readTextFile(filePath);
    } else {
      // 如果用户拒绝授权,则抛出错误
      throw new Error(`没有读取文件的权限:${filePath}`);
    }
  } else {
    // 如果已经被拒绝,则抛出错误
    throw new Error(`没有读取文件的权限:${filePath}`);
  }
}

// 调用函数
try {
  const content = await readFile("/tmp/test.txt");
  console.log(content);
} catch (error) {
  console.error(error.message);
}

在这个例子中,我们先用Deno.permissions.query()检查是否有读取文件的权限。如果没有,就用Deno.permissions.request()请求权限。如果用户同意授权,就读取文件;如果用户拒绝授权,就抛出错误。

2. 访问网络(指定Host)

有时候,我们只想允许程序访问特定的域名,而不是所有的网络。

async function fetchData(url: string): Promise<string> {
  try {
    const urlObj = new URL(url);
    const hostname = urlObj.hostname;

    // 检查是否有访问特定域名的权限
    const status = await Deno.permissions.query({ name: "net", host: hostname });

    if (status.state === "granted") {
      // 如果已经授权,直接访问网络
      const response = await fetch(url);
      return await response.text();
    } else if (status.state === "prompt") {
      // 如果需要询问用户,则请求权限
      const requestStatus = await Deno.permissions.request({ name: "net", host: hostname });
      if (requestStatus.state === "granted") {
        // 如果用户同意授权,则访问网络
        const response = await fetch(url);
        return await response.text();
      } else {
        // 如果用户拒绝授权,则抛出错误
        throw new Error(`没有访问网络的权限:${hostname}`);
      }
    } else {
      // 如果已经被拒绝,则抛出错误
      throw new Error(`没有访问网络的权限:${hostname}`);
    }
  } catch (error) {
    console.error("Error fetching data:", error);
    throw error;
  }
}

// 调用函数
try {
  const data = await fetchData("https://example.com");
  console.log(data.substring(0, 100) + "..."); // 只打印前100个字符
} catch (error) {
  console.error(error.message);
}

try {
    const data = await fetchData("https://www.google.com"); // 没有google的权限
} catch (error) {
    console.error("Second call failed:", error.message);
}

在这个例子中,我们首先解析URL,获取域名。然后,我们用Deno.permissions.query()检查是否有访问该域名的权限。如果没有,就用Deno.permissions.request()请求权限,指定host属性为域名。

3. 环境变量访问控制

async function getEnvVariable(variableName: string): Promise<string | undefined> {
  const status = await Deno.permissions.query({ name: 'env', variable: variableName });

  if (status.state === "granted") {
    return Deno.env.get(variableName);
  } else if (status.state === "prompt") {
    const requestStatus = await Deno.permissions.request({ name: 'env', variable: variableName });
    if (requestStatus.state === "granted") {
      return Deno.env.get(variableName);
    } else {
      throw new Error(`没有访问环境变量的权限: ${variableName}`);
    }
  } else {
    throw new Error(`没有访问环境变量的权限: ${variableName}`);
  }
}

async function main() {
  try {
    const homeDir = await getEnvVariable("HOME");
    console.log("Home directory:", homeDir);

    const nonExistentVar = await getEnvVariable("NON_EXISTENT_VAR");
    console.log("Non-existent variable:", nonExistentVar); // 输出 undefined,如果授权了
  } catch (error) {
    console.error(error.message);
  }
}

main();

四、 最佳实践和注意事项

  • 最小权限原则:只请求你需要的权限,不要过度授权。这就像你去别人家做客,只借用你需要的东西,不要乱翻别人的东西。
  • 显式声明权限:在代码中明确地声明你需要哪些权限,而不是依赖默认权限。这有助于提高代码的可读性和可维护性。
  • 优雅地处理权限拒绝:如果用户拒绝授权,不要直接崩溃,而是应该给出友好的提示,并提供替代方案。
  • 谨慎使用prompt状态:频繁地询问用户授权会降低用户体验。尽量避免在关键路径上请求权限,或者在用户明确需要某个功能的时候才请求权限。
  • 注意权限缓存:Deno会缓存权限状态。如果用户在命令行中改变了权限,程序可能不会立即反映这些变化。可以使用Deno.permissions.revoke()方法来撤销权限,强制Deno重新询问用户。但这个API已经deprecated,不建议使用。更好的做法是重新启动程序。
  • 权限提升的场景:在某些情况下,你可能需要在运行时提升权限。比如,在安装依赖的时候,你需要临时授予程序readwritenet权限。完成安装后,你应该立即撤销这些权限。
  • 错误处理:要妥善处理权限相关的错误。例如,当权限被拒绝时,提供有意义的错误消息,并允许用户重试或取消操作。
  • 用户体验:如果你的应用程序需要用户频繁地授予权限,考虑提供一个配置界面,允许用户一次性配置所有需要的权限。
  • 安全审查:定期审查你的代码,确保你没有请求不必要的权限,并确保你正确地处理了权限相关的错误。
  • 测试:编写单元测试和集成测试,验证你的应用程序在不同的权限状态下都能正常工作。
  • 文档:在你的文档中明确地说明你的应用程序需要哪些权限,以及为什么需要这些权限。

五、 动态权限与模块导入

Deno 的权限模型也会影响模块的导入。如果你的模块需要某些权限,你需要在导入它之前确保已经获得了这些权限。

例如,如果一个模块尝试读取文件,但你没有授予 read 权限,Deno 会抛出一个错误。你可以使用 Deno.permissions.request() 在导入模块之前动态地请求权限。

// 假设 remote_module.ts 需要读取文件
// 导入前检查并请求权限
const readStatus = await Deno.permissions.request({ name: "read" });

if (readStatus.state === "granted") {
  // 导入模块
  const remoteModule = await import("./remote_module.ts");
  // 使用 remoteModule
} else {
  console.error("没有读取文件的权限,无法导入模块");
}

六、 总结

Permissions API是Deno安全模型的核心组成部分。它允许我们在运行时动态地控制程序的权限,提高了程序的安全性和可控性。

掌握Permissions API,是成为一名合格的Deno开发者的必备技能。

好了,今天的讲座就到这里。希望大家有所收获。如果有什么问题,欢迎提问。

记住,安全第一!下次见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注