如何使用node.js实现安全的REST API


204

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

几天来,我读了很多有关保护REST API的知识,但是我没有一个最终的解决方案。据我了解是使用HTTPS提供基本的安全性。但是在那种情况下我如何保护API:

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

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

目前,我考虑只允许具有活动会话的用户使用API​​。要授权用户,我将使用护照,并且需要获得许可,我需要自己实现一些功能。全部位于HTTPS的顶部。

有人可以提供一些最佳实践或经验吗?我的“体系结构”是否缺乏?


2
我猜该API仅在您提供的前端中使用?在那种情况下,使用会话来确保用户有效似乎是一个不错的解决方案。对于权限,您可以看一下node-roles
robertklep

2
您最终为此做了什么?您可以共享任何样板代码(服务器/移动应用程序客户端)吗?
Morteza Shahriari Nia 2014年

Answers:


175

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

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

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

验证用户身份(用户名+密码+ apitoken)后,为会话生成另一个令牌,也称为accesstoken。同样,使用node-uuid。向用户发送访问令牌和用户标识。用户标识(密钥)和访问令牌(值)存储在redis中,带有到期时间,例如1h。

现在,每次用户使用rest api进行任何操作时,都需要发送userid和accesstoken。

如果您允许用户使用其余的api注册,则需要使用admin apitoken创建一个管理员帐户并将其存储在移动应用中(加密用户名+密码+ apitoken),因为新用户在以下情况下将没有apitoken他们注册。

网络上也使用此api,但您无需使用apitokens。您可以对redis存储区使用express,也可以使用与上述相同的技术,但是绕过apitoken检查,并向用户返回cookie中的userid + accesstoken。

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

摘要:

顺序图

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


1
我也使用mongodb,但是如果使用redis(使用原子操作)保存会话(访问令牌),则非常容易管理。当用户创建帐户并将其发送回用户时,会在服务器中生成apitoken。然后,当用户想要进行身份验证时,必须发送用户名+密码+ apitoken(将其放入http正文中)。请记住,HTTP不会对正文进行加密,因此可以嗅探密码和apitoken。如果您担心此问题,请使用HTTPS。
加布里埃尔·拉马斯

1
使用的意义apitoken何在?是“辅助”密码吗?
Salvatorelab

2
@TheBronx apitoken有2个用例:1)使用apitoken,您可以控制用户对系统的访问,并且可以监视和建立每个用户的统计信息。2)这是一项额外的安全措施,即“辅助”密码。
加布里埃尔·拉马斯

1
成功通过身份验证后,为什么要一次又一次发送用户ID。令牌应该是执行API调用所需的唯一秘密。
Axel Napolitano 2014年

1
令牌的概念(除了滥用令牌来跟踪用户活动之外)还在于,理想情况下,用户不需要任何用户名和密码即可使用应用程序:令牌是唯一的访问密钥。这样,用户可以随时删除任何键,这些键只会影响应用程序,而不会影响用户帐户。对于Web服务,令牌非常不便-这就是为什么会话的初始登录是用户获取该令牌的地方-对于“常规”客户端ab来说,令牌是没有问题的:一次输入就可以完成;)
Axel Napolitano

22

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

// ------------------------------------------------------
// 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:证书使用者名称“ xxxx”与目标主机名称“ xxx.net”不匹配。我已经将我的/ etc / hosts硬编码为允许https连接在同一台机器上
mastervv


9

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

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


我阅读了很多有关oauth 1.0和oauth 2.0的信息,这两个版本似乎都不是很安全。维基百科写道,oauth 1.0中存在一些安全漏洞。我也发现了一篇文章,关于一位核心开发人员离开团队,因为oauth 2.0是不安全的。
tschiela 2013年

12
@tschiela您应该添加对此处引用的内容的引用。
mikemaccana 2014年

3

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

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

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的更多信息


1
我喜欢您的回答,这似乎是这个旧问题的最佳更新。我问自己另一个关于同一主题的问题,您可能也会有所帮助。=> stackoverflow.com/questions/58076644/…–
pbonnefoi

谢谢,很高兴能为您提供帮助,我正在为您的问题发布答案
Ahmed Elkoussy 19/09/24

2

如果您希望Web应用程序具有完全锁定的区域,而该区域只能由公司的管理员访问,则可以为您提供SSL授权。这将确保没有人可以连接到服务器实例,除非他们在浏览器中安装了授权证书。上周,我写了一篇有关如何设置服务器的文章

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


好文章。但是私有区域是给用户的。
tschiela

谢谢-对,那么您应该寻求另一种解决方案,分发证书会很麻烦。
ExxKA 2013年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.