【问题标题】:Node.js resource based ACL基于 Node.js 资源的 ACL
【发布时间】:2019-03-27 10:30:33
【问题描述】:

我正在 Node 中实现一个简单的访问控制系统,我想知道我正在做的最好的方法是什么。

我正在使用Node ACL,但我不清楚如何按资源进行阻止。

让我们看下面的例子: USER ->* PROJECT ->* ENTRY。用户可以有多个项目,其中包含许多条目。用户可以是ADMINUSER

我创建了一个端点/entry/{ID},用户可以在其中访问条目详细信息。每个人都可以访问端点,ADMINs 可以看到所有条目,但是对于User,我需要做类似的事情:

app.get('/entry/{id}', (req, res) => {
    if (user.admin) {
        // Return eveything
    }
    else {
       if (entry.project == user.project) {
           // return it
       }
       else {
           // Unathorized
       }
    }
})

是否有更好的方法/模式来实现对资源所有权的检查?

【问题讨论】:

    标签: node.js express design-patterns acl


    【解决方案1】:

    这是一个非常广泛的问题,所以我会尝试给你一些提示作为我的答案,但是

    javascript 中有 ACL 模式吗?

    有许多解决方案,但我不会将其中任何一个称为模式。我现在会很主观,但是passport.js 和类似模块的方式至少可以说是不透明的——而且它不是真正的 ACL...

    有人可能会说 - 嘿,它是 node.js,必须有模块来做到这一点并使你的 node_modules 更重,但是在 npm 中寻找一个好的 acl 模块,我只发现了一些过时的模块和与快递紧密结合。由于您的问题不是which is the best npm module for acl,因此我在第 3 页放弃了寻找此类问题,这并不意味着没有准备好,因此您可能需要更仔细地查看。

    我认为你的实现可以被认为是可以接受的,正如我提到的一些小的更正或提示:

    将您的请求逻辑与访问控制逻辑分开

    在您的代码中,所有事情都发生在一个回调中 - 这绝对是非常有效的,但从长远来看也很难支持。你看,它会在所有回调中的许多 if 中以相同的代码结束。分离逻辑非常简单——只需在两个回调中实现相同的路径(它们将按照定义的顺序运行),所以:

    app.all('/entry/{id}', (req, res, next) => {
        const {user, entry} = extractFromRequest(req);
        if (user.admin || entry.project === user.project) {
            next();
        } else {
            res.status(403).send("Forbidden");
        }
    });
    
    app.get('/entry/{id}', (req, res) => {
        // simply respond here
    })
    

    这样第一个回调会检查用户是否有访问权限,这不会影响响应的逻辑。 next() 的用法特定于类似 express 的框架,我假设您使用它来查看您的代码 - 当您调用它时,将执行下一个处理程序,否则不会运行其他处理程序。

    Express.js app.all documentation for an acl example

    使用服务范围的 acl

    将基本 ACL 保存在一个地方并且除非必要,否则不要为每个路径定义它会更加安全。这样您就不会省略一条路径,也不会在请求中间的某个地方留下安全漏洞。为此,我们需要将 ACL 拆分为多个部分:

    • URL 访问检查(如果路径是公开的/对所有用户开放)
    • 用户和会话有效性检查(用户已登录,会话未过期)
    • 管理员/用户检查(权限级别)
    • 否则我们不允许任何事情。
        app.all('*', (req, res, next) => {
            if (path.isPublic) next(); // public paths can be unlogged
            else if (user.valid && user.expires > Date.now()) next(); // session and user must be valid
            else if (user.admin) next(); // admin can go anywhere
            else if (path.isOpen && user.valid) next(); // paths for logged in users may also pass
            else throw new Error("Forbidden");
        });
    

    这项检查的限制不是很严格,但我们不需要重复。还要注意底部的 throw Error - 我们将在错误处理程序中处理它:

    app.use(function (err, req, res, next) {
        if (err.message === "Forbidden") res.status(403).send("Forbidden");
        else res.status(500).send("Something broke");
    })
    

    任何带有 4 个参数的处理程序都将被 Express.js 视为错误处理程序。

    在特定的路径级别,如果需要 ACL,只需向处理程序抛出错误:

    app.all('/entry/{id}', (req, res, next) => {
        if (!user.admin && user.project !== entry.project) throw new Error("Forbidden");
        // then respond...
    });
    

    这让我想起了另一个提示......

    不要使用 user.admin

    好吧,好吧,如果你喜欢就使用它。我不。破解您的代码的第一次尝试是尝试在任何具有属性的对象上设置管理员。它是常见安全检查中的常用名称,因此就像将您的 WiFI AP 登录保留为出厂默认设置一样。

    我建议使用角色和权限。一个角色包含一组权限,一个用户有一些角色(或者一个更简单但给你更少选择的角色)。角色也可以分配给项目。

    这很容易成为一篇完整的文章,所以这里有一些further reading on Role-based ACL

    使用标准 HTTP 响应

    上面提到了其中的一些,但最好使用标准 4xx HTTP 代码状态之一作为响应 - 这对客户端来说很有意义。本质上,当用户未登录(或会话过期)时回复401,当没有足够的权限时回复403,当超出使用限制时回复429more codes and what to do when the request is a teapot in Wikipedia.

    至于实现本身,我喜欢创建一个简单的 AuthError 类并使用它从应用程序中抛出错误。

    class AuthError extends Error {
        constructor(status, message = "Access denied") {
            super(message);
            this.status = status;
        }
    }
    

    在代码中处理和抛出这样的错误真的很容易,像这样:

    app.all('*', (req, res, next) => {
        // check if all good, but be more talkative otherwise
        if (!path.isOpen && !user.valid) throw new AuthError(401, "Unauthenticated");
        throw new AuthError(403);
    });
    
    function checkRoles(user, entry) {
        // do some checks or...
        throw new AuthError(403, "Insufficient Priviledges");
    }
    
    app.get('/entry/{id}', (req, res) => {
        checkRoles(user, entry); // throws AuthError
        // or respond...
    })
    

    在您的错误处理程序中,您发送从代码中捕获的状态/消息:

    app.use(function (err, req, res, next) {
        if (err instanceof AuthError) res.send(err.status).send(err.message);
        else res.status(500).send('Something broke!')
    })
    

    不要立即回复

    最后 - 这更像是一个安全功能和安全功能。每次您以错误消息响应时,为什么不睡几秒钟呢?就记忆而言,这会伤害你,但它只会伤害一点点,而且会对可能的攻击者造成很大伤害,因为他们等待结果的时间更长。此外,只需在一个地方实施非常简单:

    app.use(function (err, req, res, next) {
        // some errors from the app can be handled here - you can respond immediately if
        // you think it's better.
        if (err instanceof AppError) return res.send(err.status).send(err.message);
        setTimeout(() => {
            if (err instanceof AuthError) res.send(err.status).send(err.message);
            else res.status(500).send('Something broke!')
        }, 3000);
    })
    

    呼...我认为这份清单并不详尽,但在我看来这是一个明智的开始。

    【讨论】:

      猜你喜欢
      • 2011-11-16
      • 2023-03-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-09-22
      • 2014-10-02
      • 2020-05-07
      相关资源
      最近更新 更多