简体   繁体   English

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

[英]How to implement a secure REST API with node.js

I start planning a REST API with node.js ,express and mongodb. 我开始使用node.js,express和mongodb规划REST API。 The API provides data for a website (public and private area) and maybe later a mobile app. API为网站(公共和私人区域)以及稍后的移动应用程序提供数据。 The frontend will be developed with AngularJS. 该前端将与AngularJS一起开发。

For some days I read a lot about securing REST APIs, but I don't get to a final solution. 有些日子我读了很多关于保护REST API的内容,但我没有找到最终的解决方案。 As far as I understand is to use HTTPS to provide a basic security. 据我所知,使用HTTPS提供基本的安全性。 But how I can protect the API in that use cases: 但是我如何在该用例中保护API:

  • Only visitors/users of the website/app are allowed to get data for the public area of the website/app 只允许网站/应用的访问者/用户获取网站/应用的公共区域的数据

  • Only authenticated and authorized users are allowed to get data for private area (and only data, where the user granted permissions) 只允许经过身份验证和授权的用户获取私有区域的数据(并且只有用户授予权限的数据)

At the moment I think about to only allow users with a active session to use the API. 目前我认为只允许具有活动会话的用户使用API​​。 To authorize the users I will use passport and for permission I need to implement something for myself. 为了授权用户,我将使用护照并获得许可,我需要为自己实施一些内容。 All on the top of HTTPS. 全部在HTTPS之上。

Can somebody provide some best practice or experiences? 有人可以提供一些最佳实践或经验吗? Is there a lack in my “architecture”? 我的“架构”是否缺乏?

I've had the same problem you describe. 我有同样的问题你描述。 The web site I'm building can be accessed from a mobile phone and from the browser so I need an api to allow users to signup, login and do some specific tasks. 我正在构建的网站可以通过手机和浏览器访问,因此我需要一个api来允许用户注册,登录和执行一些特定的任务。 Furthermore, I need to support scalability, the same code running on different processes/machines. 此外,我需要支持可伸缩性,在不同的进程/机器上运行相同的代码。

Because users can CREATE resources (aka POST/PUT actions) you need to secure your api. 因为用户可以创建资源(也就是POST / PUT操作),所以您需要保护您的api。 You can use oauth or you can build your own solution but keep in mind that all the solutions can be broken if the password it's really easy to discover. 您可以使用oauth或者您可以构建自己的解决方案,但请记住,如果密码很容易被发现,所有解决方案都可能被破坏。 The basic idea is to authenticate users using the username, password and a token, aka the apitoken. 基本思想是使用用户名,密码和令牌(即apitoken)对用户进行身份验证。 This apitoken can be generated using node-uuid and the password can be hashed using pbkdf2 可以使用node-uuid生成此apitoken,并且可以使用pbkdf2对密码进行哈希处理

Then, you need to save the session somewhere. 然后,您需要在某处保存会话。 If you save it in memory in a plain object, if you kill the server and reboot it again the session will be destroyed. 如果将其保存在普通对象的内存中,如果您终止服务器并再次重新启动它,会话将被销毁。 Also, this is not scalable. 此外,这不可扩展。 If you use haproxy to load balance between machines or if you simply use workers, this session state will be stored in a single process so if the same user is redirected to another process/machine it will need to authenticate again. 如果您使用haproxy在计算机之间进行负载平衡,或者您只是使用工作程序,则此会话状态将存储在单个进程中,因此如果同一用户被重定向到另一个进程/计算机,则需要再次进行身份验证。 Therefore you need to store the session in a common place. 因此,您需要将会话存储在一个公共位置。 This is typically done using redis. 这通常使用redis完成。

When the user is authenticated (username+password+apitoken) generate another token for the session, aka accesstoken. 当用户通过身份验证(用户名+密码+ apitoken)时,会为会话生成另一个令牌,即accessstoken。 Again, with node-uuid. 再次,使用node-uuid。 Send to the user the accesstoken and the userid. 向用户发送accesstoken和userid。 The userid (key) and the accesstoken (value) are stored in redis with and expire time, eg 1h. userid(key)和accesstoken(value)以redis存储,并且到期时间为1h。

Now, every time the user does any operation using the rest api it will need to send the userid and the accesstoken. 现在,每次用户使用其余的api进行任何操作时,都需要发送用户标识和accessstoken。

If you allow the users to signup using the rest api, you'll need to create an admin account with an admin apitoken and store them in the mobile app (encrypt username+password+apitoken) because new users won't have an apitoken when they sign up. 如果您允许用户使用其余api进行注册,则需要创建一个带有管理员apitoken的管理员帐户并将其存储在移动应用程序中(加密用户名+密码+ apitoken),因为新用户将无法使用apitoken他们报名参加。

The web also uses this api but you don't need to use apitokens. 网络也使用这个api,但你不需要使用apitokens。 You can use express with a redis store or use the same technique described above but bypassing the apitoken check and returning to the user the userid+accesstoken in a cookie. 您可以将express与redis商店一起使用,或者使用上述相同的技术,但绕过apitoken检查并在cookie中返回用户ID + accesstoken。

If you have private areas compare the username with the allowed users when they authenticate. 如果您有私有区域,则在进行身份验证时将用户名与允许的用户进行比较。 You can also apply roles to the users. 您还可以将角色应用于用户。

Summary: 摘要:

序列图

An alternative without apitoken would be to use HTTPS and to send the username and password in the Authorization header and cache the username in redis. 没有apitoken的替代方案是使用HTTPS并在Authorization标头中发送用户名和密码,并在redis中缓存用户名。

I would like to contribute this code as an structural solution for the question posed, according (I hope so) to the accepted answer. 根据(我希望如此)我接受的答案,我想提供这个代码作为提出问题的结构解决方案。 (You can very easily customize it). (您可以非常轻松地自定义它)。

// ------------------------------------------------------
// 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);

This server can be tested with curl: 可以使用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" 

I just finished a sample app that does this in a pretty basic, but clear way. 我刚刚完成了一个示例应用程序,它以一种非常基本但清晰的方式执行此操作。 It uses mongoose with mongodb to store users and passport for auth management. 它使用mongoose和mongodb来存储用户和护照以进行身份​​验证管理。

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

There are many questions about REST auth patterns here on SO. 在SO上有很多关于REST auth模式的问题。 These are the most relevant for your question: 这些与您的问题最相关:

Basically you need to choose between using API keys (least secure as the key may be discovered by an unauthorized user), an app key and token combo (medium), or a full OAuth implementation (most secure). 基本上,您需要选择使用API​​密钥(最不安全,因为密钥可能被未经授权的用户发现),应用密钥和令牌组合(中等),或完整的OAuth实施(最安全)。

If you want to have a completely locked down area of your webapplication which can only be accessed by administrators from your company, then SSL authorization maybe for you. 如果您想拥有一个完全锁定的Web应用程序区域,该区域只能由您公司的管理员访问,那么SSL授权可能适合您。 It will insure that no one can make a connection to the server instance unless they have an authorized certificate installed in their browser. 它将确保没有人可以建立与服务器实例的连接,除非他们在浏览器中安装了授权证书。 Last week I wrote an article on how to setup the server: Article 上周我写了一篇关于如何设置服务器的文章文章

This is one of the most secure setups you will find as there are no username/passwords involved so no one can gain access unless one of your users hands the key files to a potential hacker. 这是您将找到的最安全的设置之一,因为没有涉及用户名/密码,因此除非您的一个用户将密钥文件交给潜在的黑客,否则没有人可以访问。

If you want to secure your application, then you should definitely start by using HTTPS instead of HTTP , this ensures a creating secure channel between you & the users that will prevent sniffing the data sent back & forth to the users & will help keep the data exchanged confidential. 如果您想保护您的应用程序, 那么您一定要先使用HTTPS而不是HTTP ,这样可以确保在您和用户之间建立安全通道,防止嗅探发送给用户的数据并帮助保存数据交换机密。

You can use JWTs (JSON Web Tokens) to secure RESTful APIs , this has many benefits when compared to the server-side sessions, the benefits are mainly: 您可以使用JWT(JSON Web令牌)来保护RESTful API ,与服务器端会话相比,这有许多好处,其好处主要在于:

1- More scalable, as your API servers will not have to maintain sessions for each user (which can be a big burden when you have many sessions) 1-更具可扩展性,因为您的API服务器不必为每个用户维护会话(当您有多个会话时,这可能是一个很大的负担)

2- JWTs are self contained & have the claims which define the user role for example & what he can access & issued at date & expiry date (after which JWT won't be valid) 2- JWT是自包含的并且具有定义用户角色的权利要求,例如他在日期和到期日期间可以访问和发布的内容(之后JWT将无效)

3- Easier to handle across load-balancers & if you have multiple API servers as you won't have to share session data nor configure server to route the session to same server, whenever a request with a JWT hit any server it can be authenticated & authorized 3-更容易处理负载均衡器并且如果您有多个API服务器,因为您不必共享会话数据也不需要配置服务器将会话路由到同一服务器,只要有JWT的请求命中任何服务器,就可以对其进行身份验证并授权

4- Less pressure on your DB as well as you won't have to constantly store & retrieve session id & data for each request 4-减少数据库压力,您不必经常存储和检索每个请求的会话ID和数据

5- The JWTs can't be tampered with if you use a strong key to sign the JWT, so you can trust the claims in the JWT that is sent with the request without having to check the user session & whether he is authorized or not, you can just check the JWT & then you are all set to know who & what this user can do. 5-如果您使用强密钥对JWT进行签名,则JWT不会被篡改,因此您可以信任JWT中随请求一起发送的声明,而无需检查用户会话以及他是否已获得授权,你可以查看JWT,然后你就可以知道这个用户可以做什么和做什么了。

Many libraries provide easy ways to create & validate JWTs in most programming languages, for example: in node.js one of the most popular is jsonwebtoken 许多库提供了在大多数编程语言中创建和验证JWT的简单方法,例如:在node.js中最流行的一个是jsonwebtoken

Since REST APIs generally aims to keep the server stateless, so JWTs are more compatible with that concept as each request is sent with Authorization token that is self contained (JWT) without the server having to keep track of user session compared to sessions which make the server stateful so that it remembers the user & his role, however, sessions are also widely used & have their pros, which you can search for if you want. 由于REST API通常旨在使服务器保持无状态,因此JWT与该概念更兼容,因为每个请求都是使用自包含的授权令牌(JWT)发送的,而服务器不必跟踪用户会话,而不是使用服务器状态,以便它记住用户和他的角色,但是,会话也被广泛使用并拥有他们的专业人员,您可以根据需要搜索。

One important thing to note is that you have to securely deliver the JWT to the client using HTTPS & save it in a secure place (for example in local storage). 需要注意的一件重要事情是,您必须使用HTTPS将JWT安全地传送到客户端并将其保存在安全的地方(例如在本地存储中)。

You can learn more about JWTs from this link 您可以从此链接了解有关JWT的更多信息

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM