咳咳,各位观众老爷们,晚上好!我是今晚的讲座主持人,代号“码农甲”。咱们今晚聊点刺激的,关于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
对象。它提供了一系列的方法,用于查询和管理权限。
主要涉及几个关键点:
-
权限名称:Deno定义了一系列的权限名称,比如
read
、write
、net
、env
、plugin
、hrtime
、run
、ffi
、sys
等等。每种权限对应着一种特定的操作。权限名称 描述 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
命令行参数。 -
权限状态:每种权限都有一个状态,可以是
granted
(已授权)、denied
(已拒绝)或者prompt
(需要询问用户)。 -
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" }
-
Deno.permissions.request()
方法:用于请求权限。它接受一个包含name
属性的对象作为参数,返回一个Promise,resolve的值是一个包含state
属性的对象。如果权限状态是prompt
,Deno会询问用户是否允许授予权限。const status = await Deno.permissions.request({ name: "net" }); console.log(status); // 输出:{ state: "granted" } 或者 { state: "denied" }
-
权限描述符:权限描述符不仅仅包含
name
,还可以包含其他属性,比如path
(对于read
和write
权限)、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,不建议使用。更好的做法是重新启动程序。 - 权限提升的场景:在某些情况下,你可能需要在运行时提升权限。比如,在安装依赖的时候,你需要临时授予程序
read
、write
和net
权限。完成安装后,你应该立即撤销这些权限。 - 错误处理:要妥善处理权限相关的错误。例如,当权限被拒绝时,提供有意义的错误消息,并允许用户重试或取消操作。
- 用户体验:如果你的应用程序需要用户频繁地授予权限,考虑提供一个配置界面,允许用户一次性配置所有需要的权限。
- 安全审查:定期审查你的代码,确保你没有请求不必要的权限,并确保你正确地处理了权限相关的错误。
- 测试:编写单元测试和集成测试,验证你的应用程序在不同的权限状态下都能正常工作。
- 文档:在你的文档中明确地说明你的应用程序需要哪些权限,以及为什么需要这些权限。
五、 动态权限与模块导入
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开发者的必备技能。
好了,今天的讲座就到这里。希望大家有所收获。如果有什么问题,欢迎提问。
记住,安全第一!下次见!