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

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

只有网站/应用程序的访问者/用户被允许获取网站/应用程序公共区域的数据 仅允许经过身份验证和授权的用户获取私有区域的数据(并且仅允许用户授予权限的数据)

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

有人能提供一些最佳实践或经验吗?我的“架构”是否存在缺陷?


当前回答

我愿意将此代码作为提出的问题的结构性解决方案,根据(我希望如此)公认的答案。(你可以很容易地自定义它)。

// ------------------------------------------------------
// 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" 

其他回答

在SO上有许多关于REST认证模式的问题。以下是与你的问题最相关的:

保护我的Node.js应用程序的REST API? 宁静的身份验证

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

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

这是你会发现的最安全的设置之一,因为它不涉及用户名/密码,所以没有人可以访问,除非你的用户将密钥文件交给潜在的黑客。

如果你想要保护你的应用程序,那么你肯定应该开始使用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的信息

我愿意将此代码作为提出的问题的结构性解决方案,根据(我希望如此)公认的答案。(你可以很容易地自定义它)。

// ------------------------------------------------------
// 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" 

我刚刚完成了一个示例应用程序,它在一个相当基本的,但清楚的方式做到这一点。它使用mongoose和mongodb存储用户和护照进行认证管理。

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