【问题标题】:How to implement a secure REST API with node.js如何使用 node.js 实现安全的 REST API
【发布时间】:2013-03-07 23:24:34
【问题描述】:

我开始计划一个带有 node.js、express 和 mongodb 的 REST API。 API 为网站(公共和私人区域)提供数据,以后可能还会为移动应用程序提供数据。前端将使用 AngularJS 开发。

这几天我读了很多关于保护 REST API 的文章,但我没有找到最终的解决方案。据我了解是使用HTTPS来提供基本的安全性。但是在这些用例中我如何保护 API:

  • 仅允许网站/应用的访问者/用户获取网站/应用的公共区域的数据

  • 仅允许经过身份验证和授权的用户获取私有区域的数据(并且仅允许用户授予权限的数据)

目前我考虑只允许具有活动会话的用户使用 API。为了授权用户,我将使用护照并获得许可,我需要为自己实现一些东西。一切都在 HTTPS 之上。

有人可以提供一些最佳实践或经验吗?我的“架构”有什么不足吗?

【问题讨论】:

  • 我猜 API 只能在您提供的前端使用?在这种情况下,使用会话来确保用户有效似乎是一个很好的解决方案。对于权限,您可以查看node-roles
  • 你最后为此做了什么?您可以分享任何样板代码(服务器/移动应用程序客户端)吗?

标签: node.js rest express passport.js


【解决方案1】:

我遇到了你描述的同样的问题。我正在构建的网站可以通过手机和浏览器访问,因此我需要一个 api 来允许用户注册、登录和执行某些特定任务。此外,我需要支持可伸缩性,即在不同的进程/机器上运行相同的代码。

因为用户可以创建资源(也称为 POST/PUT 操作),所以您需要保护您的 api。您可以使用 oauth,也可以构建自己的解决方案,但请记住,如果密码真的很容易发现,所有解决方案都可能被破解。基本思想是使用用户名、密码和令牌(也称为 apitoken)对用户进行身份验证。这个apitoken可以使用node-uuid生成,密码可以使用pbkdf2进行哈希处理

然后,您需要将会话保存在某处。如果您将其保存在内存中的普通对象中,如果您终止服务器并再次重新启动它,则会话将被破坏。此外,这是不可扩展的。如果您使用 haproxy 在机器之间进行负载平衡,或者您只是使用工作人员,则此会话状态将存储在单个进程中,因此如果同一用户被重定向到另一个进程/机器,则需要再次进行身份验证。因此,您需要将会话存储在一个公共位置。这通常使用 redis 完成。

当用户通过身份验证(用户名+密码+apitoken)时,会为会话生成另一个令牌,即 accesstoken。同样,使用 node-uuid。向用户发送访问令牌和用户 ID。 userid (key) 和 accesstoken (value) 存储在 redis 中,并带有过期时间,例如1 小时。

现在,每次用户使用 rest api 进行任何操作时,都需要发送用户 ID 和访问令牌。

如果您允许用户使用 rest api 注册,您需要创建一个带有管理员 apitoken 的管理员帐户并将其存储在移动应用程序中(加密用户名+密码+apitoken),因为新用户不会拥有注册时的 apitoken。

Web 也使用此 api,但您不需要使用 apitokens。您可以将 express 与 redis 存储一起使用,或者使用上述相同的技术,但绕过 apitoken 检查并在 cookie 中向用户返回 userid+accesstoken。

如果您有私人区域,请在验证时将用户名与允许的用户进行比较。您还可以将角色应用于用户。

总结:

没有 apitoken 的替代方法是使用 HTTPS 并在 Authorization 标头中发送用户名和密码,并将用户名缓存在 redis 中。

【讨论】:

  • 我也使用 mongodb,但如果您使用 redis(使用原子操作)保存会话(accesstoken),则非常容易管理。 apitoken 是在用户创建帐户时在服务器中生成并发送回给用户的。然后,当用户想要进行身份验证时,它必须发送用户名+密码+apitoken(将它们放在 http 正文中)。请记住,HTTP 不会加密正文,因此可以嗅探密码和 apitoken。如果您担心,请使用 HTTPS。
  • 使用apitoken 有什么意义?是“二级”密码吗?
  • @TheBronx apitoken 有 2 个用例:1) 使用 apitoken,您可以控制用户对系统的访问,您可以监控和构建每个用户的统计信息。 2) 这是一个额外的安全措施,一个“二级”密码。
  • 认证成功后为什么要一次又一次地发送用户ID。令牌应该是您执行 API 调用所需的唯一秘密。
  • 令牌的想法——除了滥用它来跟踪用户活动——是,用户理想情况下不需要任何用户名和密码来使用应用程序:令牌是唯一的访问密钥。这允许用户随时删除任何密钥,仅影响应用程序而不影响用户帐户。对于 web 服务,令牌非常不方便 - 这就是为什么会话的初始登录是用户获取令牌的地方 - 对于“常规”客户端 ab,令牌没问题:输入一次,你几乎完成了;)
【解决方案2】:

根据(我希望如此)接受的答案,我想贡献此代码作为所提出问题的结构解决方案。 (您可以非常轻松地对其进行自定义)。

// ------------------------------------------------------
// server.js 

// .......................................................
// requires
var fs = require('fs');
var express = require('express'); 
var myBusinessLogic = require('../businessLogic/businessLogic.js');

// .......................................................
// security options

/*
1. Generate a self-signed certificate-key pair
openssl req -newkey rsa:2048 -new -nodes -x509 -days 3650 -keyout key.pem -out certificate.pem

2. Import them to a keystore (some programs use a keystore)
keytool -importcert -file certificate.pem -keystore my.keystore
*/

var securityOptions = {
    key: fs.readFileSync('key.pem'),
    cert: fs.readFileSync('certificate.pem'),
    requestCert: true
};

// .......................................................
// create the secure server (HTTPS)

var app = express();
var secureServer = require('https').createServer(securityOptions, app);

// ------------------------------------------------------
// helper functions for auth

// .............................................
// true if req == GET /login 

function isGETLogin (req) {
    if (req.path != "/login") { return false; }
    if ( req.method != "GET" ) { return false; }
    return true;
} // ()

// .............................................
// your auth policy  here:
// true if req does have permissions
// (you may check here permissions and roles 
//  allowed to access the REST action depending
//  on the URI being accessed)

function reqHasPermission (req) {
    // decode req.accessToken, extract 
    // supposed fields there: userId:roleId:expiryTime
    // and check them

    // for the moment we do a very rigorous check
    if (req.headers.accessToken != "you-are-welcome") {
        return false;
    }
    return true;
} // ()

// ------------------------------------------------------
// install a function to transparently perform the auth check
// of incoming request, BEFORE they are actually invoked

app.use (function(req, res, next) {
    if (! isGETLogin (req) ) {
        if (! reqHasPermission (req) ){
            res.writeHead(401);  // unauthorized
            res.end();
            return; // don't call next()
        }
    } else {
        console.log (" * is a login request ");
    }
    next(); // continue processing the request
});

// ------------------------------------------------------
// copy everything in the req body to req.body

app.use (function(req, res, next) {
    var data='';
    req.setEncoding('utf8');
    req.on('data', function(chunk) { 
       data += chunk;
    });
    req.on('end', function() {
        req.body = data;
        next(); 
    });
});

// ------------------------------------------------------
// REST requests
// ------------------------------------------------------

// .......................................................
// authenticating method
// GET /login?user=xxx&password=yyy

app.get('/login', function(req, res){
    var user = req.query.user;
    var password = req.query.password;

    // rigorous auth check of user-passwrod
    if (user != "foobar" || password != "1234") {
        res.writeHead(403);  // forbidden
    } else {
        // OK: create an access token with fields user, role and expiry time, hash it
        // and put it on a response header field
        res.setHeader ('accessToken', "you-are-welcome");
        res.writeHead(200); 
    }
    res.end();
});

// .......................................................
// "regular" methods (just an example)
// newBook()
// PUT /book

app.put('/book', function (req,res){
    var bookData = JSON.parse (req.body);

    myBusinessLogic.newBook(bookData, function (err) {
        if (err) {
            res.writeHead(409);
            res.end();
            return;
        }
        // no error:
        res.writeHead(200);
        res.end();
    });
});

// .......................................................
// "main()"

secureServer.listen (8081);

这个服务器可以用 curl 测试:

echo "----   first: do login "
curl -v "https://localhost:8081/login?user=foobar&password=1234" --cacert certificate.pem

# now, in a real case, you should copy the accessToken received before, in the following request

echo "----  new book"
curl -X POST  -d '{"id": "12341324", "author": "Herman Melville", "title": "Moby-Dick"}' "https://localhost:8081/book" --cacert certificate.pem --header "accessToken: you-are-welcome" 

【讨论】:

  • 感谢这个示例,它非常有帮助,但是我尝试遵循这个,当我连接登录它时说:curl: (51) SSL: certificate subject name 'xxxx' does not match target主机名“xxx.net”。我已经硬编码了我的 /etc/hosts 以允许在同一台机器上进行 https 连接
【解决方案3】:

我刚刚完成了一个示例应用程序,它以非常基本但清晰的方式执行此操作。它使用 mongoose 和 mongodb 来存储用户和护照以进行身份​​验证管理。

https://github.com/Khelldar/Angular-Express-Train-Seed

【讨论】:

  • 您正在使用 cookie 来保护 api。我不认为这是正确的。
【解决方案4】:

在 SO 上有很多关于 REST 身份验证模式的问题。这些与您的问题最相关:

基本上,您需要在使用 API 密钥(最不安全,因为密钥可能被未经授权的用户发现)、应用密钥和令牌组合(中等)或完整的 OAuth 实施(最安全)之间进行选择。

【讨论】:

  • 我阅读了很多关于 oauth 1.0 和 oauth 2.0 的内容,这两个版本似乎都不是很安全。维基百科写道,这是 oauth 1.0 中的一些安全漏洞。我还发现一篇关于核心开发人员离开团队的文章,因为 oauth 2.0 是不安全的。
  • @tschiela 您应该添加对您在此处引用的任何内容的引用。
【解决方案5】:

如果您想保护您的应用程序,那么您绝对应该首先使用 HTTPS 而不是 HTTP,这样可以确保在您和用户之间创建一个安全通道防止嗅探来回发送给用户的数据,并有助于对交换的数据保密。

您可以使用 JWT(JSON Web Tokens)来保护 RESTful APIs,与服务器端会话相比,这有很多好处,好处主要是:

1- 更具可扩展性,因为您的 API 服务器不必为每个用户维护会话(当您有很多会话时,这可能是一个很大的负担)

2- JWT 是自包含的,并且具有定义用户角色的声明,例如,他可以访问的内容以及在日期和到期日发布的内容(在此之后 JWT 将无效)

3- 更容易跨负载均衡器处理,如果您有多个 API 服务器,因为您无需共享会话数据,也无需配置服务器以将会话路由到同一服务器,只要带有 JWT 的请求命中它的任何服务器可认证授权

4- 减少对数据库的压力,并且您不必为每个请求不断存储和检索会话 ID 和数据

5- 如果您使用强密钥对 JWT 进行签名,则 JWT 不会被篡改,因此您可以信任随请求发送的 JWT 中的声明,而无需检查用户会话以及他是否是授权与否,您只需检查 JWT 就可以知道该用户可以做什么和做什么。

许多库提供了在大多数编程语言中创建和验证 JWT 的简单方法,例如:在 node.js 中,最流行的一种是 jsonwebtoken

由于 REST API 通常旨在保持服务器无状态,因此 JWT 更符合该概念,因为每个请求都使用自包含 (JWT) 的授权令牌发送,而无需服务器保持与使服务器有状态的会话相比,跟踪用户会话,以便它记住用户及其角色,但是,会话也被广泛使用并有其优点,您可以根据需要进行搜索。

需要注意的重要一点是,您必须使用 HTTPS 将 JWT 安全地传送到客户端并将其保存在安全的地方(例如本地存储中)。

您可以了解更多关于 JWT 的信息from this link

【讨论】:

  • 我喜欢你的回答,这似乎是这个老问题的最佳更新。我问过自己关于同一主题的另一个问题,您也可能会有所帮助。 => stackoverflow.com/questions/58076644/…
  • 谢谢,很高兴能为您提供帮助,我正在为您的问题发布答案
  • 如果您使用带有 redis 存储的会话,为什么“配置服务器以将会话路由到同一服务器”是一个问题?
【解决方案6】:

如果您想拥有一个完全锁定的 Web 应用程序区域,只能由您公司的管理员访问,那么 SSL 授权可能适合您。它将确保没有人可以连接到服务器实例,除非他们的浏览器中安装了授权证书。上周我写了一篇关于如何设置服务器的文章:Article

这是您会发现的最安全的设置之一,因为不涉及用户名/密码,因此除非您的用户将密钥文件交给潜在的黑客,否则任何人都无法获得访问权限。

【讨论】:

  • 好文章。但私人区域是供用户使用的。
  • 谢谢 - 对了,那你应该去寻找另一个解决方案,分发证书会很痛苦。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2014-06-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-11-27
  • 1970-01-01
相关资源
最近更新 更多