更好的 Deno 安全性:在运行时请求权限
由 Mux 赞助的 DEV 全球展示挑战赛:展示你的项目!
Deno 是服务器端 TypeScript 和 JavaScript 领域的新秀,它默认具备安全性。这一点显而易见。他们在文档和会议演讲中反复强调这一点,确保用户了解。他们的主页上也提到了这一点,前三句话就提到了。
我很高兴他们选择了默认安全模式。但我不太喜欢他们用来演示安全性的示例。这些示例宣扬了一种理念:你应该预先指定应用程序的权限。这让我想起了早期的安卓模式,当时你必须在安装应用程序时授予其所有权限。最终,安卓系统改进了这一点,采用了类似 iOS 和浏览器使用的更优模式:让程序在运行时请求必要的权限,以便用户能够根据上下文响应请求。
让我们来看一下Deno v1 发布会上的示例代码:
import { serve } from "https://deno.land/std@0.50.0/http/server.ts";
for await (const req of serve({ port: 8000 })) {
req.respond({ body: "Hello World\n" });
}
我已经将此示例复制到 GitHub 仓库中,您可以直接运行它:
$ deno run https://raw.githubusercontent.com/BMorearty/deno-permissions-samples/master/serve-original.ts
你会发现它失败了,这是理所当然的,因为它没有网络权限。v1 版本页面对此解释道:“除非--allow-net提供命令行标志,否则上述示例将失败。”
通过运行以下命令修复--allow-net:
$ deno run --allow-net https://raw.githubusercontent.com/BMorearty/deno-permissions-samples/master/serve-original.ts
(由于它正在静默等待端口,因此不会显示任何输出。)
听起来不错,也很安全。但是,当用户下载并运行脚本,或者像上面的例子那样直接从互联网运行脚本时,他们事先并不知道程序为什么需要网络权限——更糟糕的是,文件权限。因此,他们会在运行程序之前盲目地授予它所需的所有权限。
这和以前安装安卓应用时需要的操作类似。我以前用安卓手机的时候就觉得很麻烦。有时候我想用某个应用,但又不想授予它访问权限,比如我的联系人。Deno 的一个改进之处在于,至少在 Deno 里,权限不是非此即彼的,而是由开发者自行选择授予哪些权限。
更好的选择:在运行时请求权限。
值得庆幸的是,Deno 已经提供了一种比这更好的安全模型,只是没有得到大力推广。该程序可以在需要时请求权限,而不是要求用户预先在命令行中授予权限。
// serve-request.ts
import { serve } from "https://deno.land/std@0.50.0/http/server.ts";
const netPermission = await Deno.permissions.request({ name: "net" });
if (netPermission.state === "granted") {
for await (const req of serve({ port: 8000 })) {
req.respond({ body: "Hello World\n" });
}
} else {
console.log("Can’t serve pages without net permission.");
}
截至撰写本文时,Deno.permissions该功能尚未包含在稳定版 API 中,因此您需要使用以下命令运行它--unstable:
deno run --unstable https://raw.githubusercontent.com/BMorearty/deno-permissions-samples/master/serve-request.ts
以下是授予权限后的样子(我输入g并按下了回车键Enter):
以下是否认时的样子:
“Deno requests”这行代码是Deno本身的一部分,并非由应用程序控制。如上面的代码所示,“Can't serve pages without net permission”这行代码是应用程序的自定义响应。应用程序可以根据用户选择不授予权限的情况,以任何方式做出响应。
不必担心 Deno 会请求已经授予的权限。如果用户以默认方式运行应用--allow-net,Deno.permissions.request则不会重复请求权限,而是会{ "state": "granted" }立即返回。
如果应用在运行时多次请求相同的权限,情况也是如此。如果该权限在运行时已被授予或拒绝过一次,则所有后续权限请求都会记住该响应。
// request-twice.ts
const netPermission1 = await Deno.permissions.request({ name: "net" });
console.log(
`The first permission request returned ${JSON.stringify(netPermission1)}`,
);
const netPermission2 = await Deno.permissions.request({ name: "net" });
console.log(
`The second permission request returned ${JSON.stringify(netPermission2)}`,
);
跑步:
$ deno run --unstable https://raw.githubusercontent.com/BMorearty/deno-permissions-samples/master/request-twice.ts
如您所见,它只请求一次权限:
请求许可时请提供背景信息
iOS人机界面指南中关于权限的规定如下:
请解释您的应用为何需要这些信息。请提供自定义文本(称为用途字符串或使用说明字符串),以便在系统的权限请求提示中显示,并附上示例。文本应简洁明了,使用句子大小写,并保持礼貌,以免用户感到压力。无需包含您的应用名称——系统会自动识别您的应用。
这条建议对 Deno 应用也同样适用。如果你能向用户解释为什么需要权限,他们就更有可能授予权限:
// serve-context.ts
import { serve } from "https://deno.land/std@0.50.0/http/server.ts";
let netPermission = await Deno.permissions.query({ name: "net" });
if (netPermission.state === "prompt") {
console.log("Net permission needed to serve web pages.");
netPermission = await Deno.permissions.request({ name: "net" });
}
if (netPermission.state === "granted") {
for await (const req of serve({ port: 8000 })) {
req.respond({ body: "Hello World\n" });
}
} else {
console.log("Can’t serve pages without net permission.");
}
此版本会在发出请求之前打印请求原因。这为用户提供了足够的上下文信息,以便他们了解 Deno 请求权限的原因。
但是这个调用是做什么用的呢Deno.permissions.query?既然我们在显示权限提示之前要先显示一些上下文信息,那么首先需要检查应用是否已经拥有权限。否则,显示权限上下文信息就毫无意义了。
Deno.permissions.query可以返回三种可能的状态:
{ "state": "granted" }意味着你已经获得许可。{ "state": "denied" }这意味着您的权限已被拒绝。请求权限毫无意义,因为它会立即返回“已拒绝”,而不会显示任何提示。{ "state": "prompt" }意思是,如果您调用该命令request,则会向用户显示提示。
让我们运行一下:
deno run --unstable https://raw.githubusercontent.com/BMorearty/deno-permissions-samples/master/serve-context.ts
然后你会看到提示:
如果你运行它--allow-net,你会发现它没有显示上下文,因为对的调用Deno.permissions.query表明对的调用Deno.permissions.request将返回成功。
我认为这是处理代码权限的最佳方法。
愿望清单:持久保存已安装脚本的权限。
Deno 提供了一个deno install命令,允许您将 Deno 脚本添加到您的机器中:
$ deno install --unstable https://raw.githubusercontent.com/BMorearty/deno-permissions-samples/master/serve-context.ts
此操作会下载、编译并缓存脚本及其依赖项。它还会创建一个可执行脚本。在 Mac 上,它存储在~/.deno/bin/serve-context(您可以将~/.deno/bin其添加到 PATH 环境变量中),其结构如下所示:
#!/bin/sh
# generated by deno install
deno "run" "--unstable" "https://raw.githubusercontent.com/BMorearty/deno-permissions-samples/master/serve-context.ts" "$@"
请注意deno install,您传递给 ` --unstableget_ ...deno rundeno install --allow-net https://my/script--allow-net
再次强调,要求用户预先决定所有权限并非理想之选。但 Deno 并不会持久保存权限。安装后,每次运行 Deno 时,它都会请求网络权限serve-context。--allow-net
我希望Deno能改进一下,允许它将用户对每个应用权限问题的回答进行本地缓存。浏览器在域请求权限(例如相机或地理位置权限)时就是这么做的。当然,还需要一种方法来事后撤销或授予权限。
愿望清单:让脚本将原因传递给request调用
在这个serve-context例子中,我们必须:
- 打电话
Deno.permissions.query询问是否已获得许可。 - 如果不:
- 显示脚本需要权限的原因
- 请用以下方式请求
Deno.permissions.request:
如果所有操作都能通过一次调用完成,那就简单多了Deno.permissions.request。我认为 Deno 应该允许脚本reason向权限请求传递一个简短的字符串。如果脚本尚未获得权限,则在询问用户是否授予权限之前,会显示权限不足的原因。如果脚本已经拥有权限,则不会显示权限不足的原因。
如果 Deno 支持此功能,步骤将简化为:
- 请求权限
Deno.permissions.request。
以下是代码示例(由于reason当前传递的键无效,因此无法运行request):
// serve-reason.ts
import { serve } from "https://deno.land/std@0.50.0/http/server.ts";
const netPermission = await Deno.permissions.request(
{ name: "net", reason: "Net permission needed to serve web pages." },
);
if (netPermission.state === "granted") {
for await (const req of serve({ port: 8000 })) {
req.respond({ body: "Hello World\n" });
}
} else {
console.log("Can’t serve pages without net permission.");
}
参考
- Deno权限文档。权限类型列表位于类型定义中
PermissionDescriptor,但截至撰写本文时,Deno似乎还没有针对此类型的文档。 - 当前值列表
PermissionDescriptor,目前位于 unstable.ts 中。 - 你可以在推特上关注我。




